mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
style(ui): redesign outbounds table for visual consistency
This commit is contained in:
parent
7792715dc4
commit
6166a406b3
2 changed files with 462 additions and 87 deletions
|
|
@ -1,8 +1,8 @@
|
|||
{{define "settings/xray/outbounds"}}
|
||||
<a-space direction="vertical" size="middle">
|
||||
<a-row>
|
||||
<a-col :xs="12" :sm="12" :lg="12">
|
||||
<a-space direction="horizontal" size="small">
|
||||
<a-space direction="vertical" size="middle" class="outbounds-modern">
|
||||
<a-row :gutter="[12, 12]" align="middle" justify="space-between">
|
||||
<a-col :xs="24" :sm="14" :lg="14">
|
||||
<a-space direction="horizontal" size="small" class="outbounds-toolbar">
|
||||
<a-button type="primary" icon="plus" @click="addOutbound">
|
||||
<span v-if="!isMobile">{{ i18n
|
||||
"pages.xray.outbound.addOutbound" }}</span>
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
@click="showNord()">NordVPN</a-button>
|
||||
</a-space>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="12" :lg="12" :style="{ textAlign: 'right' }">
|
||||
<a-col :xs="24" :sm="10" :lg="10" class="outbounds-toolbar-right">
|
||||
<a-button-group>
|
||||
<a-button icon="sync" @click="refreshOutboundTraffic()"
|
||||
:loading="refreshing"></a-button>
|
||||
|
|
@ -24,105 +24,140 @@
|
|||
ok-text='{{ i18n "reset"}}'
|
||||
cancel-text='{{ i18n "cancel"}}'>
|
||||
<a-icon slot="icon" type="question-circle-o"
|
||||
:style="{ color: themeSwitcher.isDarkTheme ? '#008771' : '#008771' }"></a-icon>
|
||||
:style="{ color: '#008771' }"></a-icon>
|
||||
<a-button icon="retweet"></a-button>
|
||||
</a-popconfirm>
|
||||
</a-button-group>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-table :columns="outboundColumns" bordered :row-key="r => r.key"
|
||||
<a-table :columns="outboundColumns" :row-key="r => r.key"
|
||||
:data-source="outboundData"
|
||||
:scroll="isMobile ? {} : { x: 800 }" :pagination="false"
|
||||
:scroll="isMobile ? { x: 720 } : {}"
|
||||
:pagination="false"
|
||||
:indent-size="0"
|
||||
class="outbounds-table"
|
||||
:locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}` }'>
|
||||
<template slot="action" slot-scope="text, outbound, index">
|
||||
<span>[[ index+1 ]]</span>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-icon @click="e => e.preventDefault()" type="more"
|
||||
:style="{ fontSize: '16px', textDecoration: 'bold' }"></a-icon>
|
||||
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item v-if="index>0"
|
||||
@click="setFirstOutbound(index)">
|
||||
<a-icon type="vertical-align-top"></a-icon>
|
||||
<span>{{ i18n "pages.xray.rules.first"}}</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="editOutbound(index)">
|
||||
<a-icon type="edit"></a-icon>
|
||||
<span>{{ i18n "edit" }}</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="resetOutboundTraffic(index)">
|
||||
<span>
|
||||
<div class="outbound-action-cell">
|
||||
<span class="outbound-index">[[ index+1 ]]</span>
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button shape="circle" size="small" class="outbound-action-btn"
|
||||
@click="e => e.preventDefault()">
|
||||
<a-icon type="more"></a-icon>
|
||||
</a-button>
|
||||
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item v-if="index>0"
|
||||
@click="setFirstOutbound(index)">
|
||||
<a-icon type="vertical-align-top"></a-icon>
|
||||
<span>{{ i18n "pages.xray.rules.first"}}</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="editOutbound(index)">
|
||||
<a-icon type="edit"></a-icon>
|
||||
<span>{{ i18n "edit" }}</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="resetOutboundTraffic(index)">
|
||||
<a-icon type="retweet"></a-icon>
|
||||
<span>{{ i18n "pages.inbounds.resetTraffic"}}</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="deleteOutbound(index)">
|
||||
<span :style="{ color: '#FF4D4F' }">
|
||||
<a-icon type="delete"></a-icon>
|
||||
<span>{{ i18n "delete"}}</span>
|
||||
</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="identity" slot-scope="text, outbound">
|
||||
<div class="outbound-identity-cell">
|
||||
<a-tooltip :title="outbound.tag" :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<span class="outbound-tag">[[ outbound.tag ]]</span>
|
||||
</a-tooltip>
|
||||
<div class="outbound-protocol-cell">
|
||||
<span class="outbound-pill"
|
||||
:class="outboundProtocolTone(outbound.protocol)">
|
||||
[[ outbound.protocol ]]
|
||||
</span>
|
||||
<template
|
||||
v-if="[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
|
||||
<span class="outbound-pill"
|
||||
:class="outboundNetworkTone(outbound.streamSettings.network)">
|
||||
[[ outbound.streamSettings.network ]]
|
||||
</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="deleteOutbound(index)">
|
||||
<span :style="{ color: '#FF4D4F' }">
|
||||
<a-icon type="delete"></a-icon>
|
||||
<span>{{ i18n "delete"}}</span>
|
||||
<span class="outbound-pill"
|
||||
:class="outboundSecurityTone(outbound.streamSettings.security)"
|
||||
v-if="isOutboundSecurityVisible(outbound.streamSettings.security)">
|
||||
[[ outbound.streamSettings.security ]]
|
||||
</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="address" slot-scope="text, outbound, index">
|
||||
<p :style="{ margin: '0 5px' }"
|
||||
v-for="addr in findOutboundAddress(outbound)">[[ addr ]]</p>
|
||||
<template slot="address" slot-scope="text, outbound">
|
||||
<div class="outbound-address-list">
|
||||
<a-tooltip
|
||||
v-for="addr in outboundAddresses(outbound)"
|
||||
:key="addr"
|
||||
:title="addr"
|
||||
:overlay-class-name="themeSwitcher.currentTheme">
|
||||
<span class="outbound-address-pill">[[ addr ]]</span>
|
||||
</a-tooltip>
|
||||
<span class="outbound-address-empty"
|
||||
v-if="outboundAddresses(outbound).length === 0">—</span>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="protocol" slot-scope="text, outbound, index">
|
||||
<a-tag :style="{ margin: '0' }" color="purple">[[ outbound.protocol
|
||||
]]</a-tag>
|
||||
<template
|
||||
v-if="[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
|
||||
<a-tag :style="{ margin: '0' }" color="blue">[[
|
||||
outbound.streamSettings.network ]]</a-tag>
|
||||
<a-tag :style="{ margin: '0' }"
|
||||
v-if="outbound.streamSettings.security=='tls'"
|
||||
color="green">tls</a-tag>
|
||||
<a-tag :style="{ margin: '0' }"
|
||||
v-if="outbound.streamSettings.security=='reality'"
|
||||
color="green">reality</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template slot="traffic" slot-scope="text, outbound, index">
|
||||
<a-tag color="green">[[ findOutboundTraffic(outbound) ]]</a-tag>
|
||||
<template slot="traffic" slot-scope="text, outbound">
|
||||
<div class="outbound-traffic-cell">
|
||||
<span class="outbound-traffic-up" :title='`{{ i18n "pages.inbounds.upload" }}`'>
|
||||
<a-icon type="arrow-up"></a-icon>
|
||||
[[ SizeFormatter.sizeFormat(findOutboundUp(outbound)) ]]
|
||||
</span>
|
||||
<span class="outbound-traffic-sep" aria-hidden="true"></span>
|
||||
<span class="outbound-traffic-down" :title='`{{ i18n "pages.inbounds.download" }}`'>
|
||||
<a-icon type="arrow-down"></a-icon>
|
||||
[[ SizeFormatter.sizeFormat(findOutboundDown(outbound)) ]]
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="test" slot-scope="text, outbound, index">
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.xray.outbound.test"
|
||||
}}</template>
|
||||
<template slot="title">{{ i18n "pages.xray.outbound.test" }}</template>
|
||||
<a-button
|
||||
type="primary"
|
||||
shape="circle"
|
||||
icon="thunderbolt"
|
||||
:loading="outboundTestStates[index] && outboundTestStates[index].testing"
|
||||
class="outbound-test-btn"
|
||||
:loading="isOutboundTesting(index)"
|
||||
@click="testOutbound(index)"
|
||||
:disabled="(outbound.protocol === 'blackhole' || outbound.tag === 'blocked') || (outboundTestStates[index] && outboundTestStates[index].testing)">
|
||||
:disabled="isOutboundUntestable(outbound) || isOutboundTesting(index)">
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template slot="testResult" slot-scope="text, outbound, index">
|
||||
<div
|
||||
v-if="outboundTestStates[index] && outboundTestStates[index].result">
|
||||
<a-tag v-if="outboundTestStates[index].result.success"
|
||||
color="green">
|
||||
[[ outboundTestStates[index].result.delay ]]ms
|
||||
<span v-if="outboundTestStates[index].result.statusCode">
|
||||
([[ outboundTestStates[index].result.statusCode
|
||||
]])</span>
|
||||
</a-tag>
|
||||
<div class="outbound-result-cell" v-if="outboundTestResult(index)">
|
||||
<span v-if="outboundTestResult(index).success"
|
||||
class="outbound-result-pill outbound-result-ok">
|
||||
<a-icon type="check-circle" theme="filled"></a-icon>
|
||||
[[ outboundTestResult(index).delay ]] ms
|
||||
<span class="outbound-result-status"
|
||||
v-if="outboundTestResult(index).statusCode">
|
||||
· [[ outboundTestResult(index).statusCode ]]
|
||||
</span>
|
||||
</span>
|
||||
<a-tooltip v-else
|
||||
:title="outboundTestStates[index].result.error">
|
||||
<a-tag color="red">
|
||||
Failed
|
||||
</a-tag>
|
||||
:title="outboundTestResult(index).error"
|
||||
:overlay-class-name="themeSwitcher.currentTheme">
|
||||
<span class="outbound-result-pill outbound-result-fail">
|
||||
<a-icon type="close-circle" theme="filled"></a-icon>
|
||||
{{ i18n "pages.xray.outbound.testFailed" }}
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<span
|
||||
v-else-if="outboundTestStates[index] && outboundTestStates[index].testing">
|
||||
<a-icon type="loading" />
|
||||
<span class="outbound-result-loading" v-else-if="isOutboundTesting(index)">
|
||||
<a-icon type="loading"></a-icon>
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
<span class="outbound-result-idle" v-else>—</span>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-space>
|
||||
|
|
|
|||
|
|
@ -202,13 +202,16 @@
|
|||
];
|
||||
|
||||
const outboundColumns = [
|
||||
{ title: "#", align: 'center', width: 60, scopedSlots: { customRender: 'action' } },
|
||||
{ title: '{{ i18n "pages.xray.outbound.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
|
||||
{ title: '{{ i18n "protocol"}}', align: 'center', width: 50, scopedSlots: { customRender: 'protocol' } },
|
||||
{ title: '{{ i18n "pages.xray.outbound.address"}}', align: 'center', width: 50, scopedSlots: { customRender: 'address' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', align: 'center', width: 180, scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.xray.outbound.testResult" }}', align: 'center', width: 120, scopedSlots: { customRender: 'testResult' } },
|
||||
{ title: '{{ i18n "pages.xray.outbound.test" }}', align: 'center', width: 60, scopedSlots: { customRender: 'test' } },
|
||||
{ title: '#', align: 'center', width: 70, scopedSlots: { customRender: 'action' } },
|
||||
// Combined "Tag / Protocol" — saves a column. Tag stays on top, protocol +
|
||||
// network + security pills sit underneath it. Width chosen so the three
|
||||
// longest tonal pills (e.g. vless + httpupgrade + reality) fit on a
|
||||
// single line without wrapping.
|
||||
{ title: '{{ i18n "pages.xray.outbound.tag"}}', align: 'left', width: 280, scopedSlots: { customRender: 'identity' } },
|
||||
{ title: '{{ i18n "pages.xray.outbound.address"}}', align: 'left', scopedSlots: { customRender: 'address' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', align: 'left', width: 190, scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.xray.outbound.testResult" }}', align: 'left', width: 130, scopedSlots: { customRender: 'testResult' } },
|
||||
{ title: '{{ i18n "pages.xray.outbound.test" }}', align: 'center', width: 70, scopedSlots: { customRender: 'test' } },
|
||||
];
|
||||
|
||||
const reverseColumns = [
|
||||
|
|
@ -556,13 +559,79 @@
|
|||
}
|
||||
return true;
|
||||
},
|
||||
findOutboundTraffic(o) {
|
||||
for (const otraffic of this.outboundsTraffic) {
|
||||
if (otraffic.tag == o.tag) {
|
||||
return `↑ ${SizeFormatter.sizeFormat(otraffic.up)} / ${SizeFormatter.sizeFormat(otraffic.down)} ↓`
|
||||
}
|
||||
// outboundTrafficFor returns {up, down} for an outbound by tag,
|
||||
// defaulting to zeros when no traffic row has been reported yet.
|
||||
// Templates use the up/down accessors below — keeping the lookup in
|
||||
// one place avoids drift if the data shape changes.
|
||||
outboundTrafficFor(o) {
|
||||
const t = this.outboundsTraffic.find(t => t.tag == o.tag);
|
||||
return { up: t ? t.up : 0, down: t ? t.down : 0 };
|
||||
},
|
||||
findOutboundUp(o) { return this.outboundTrafficFor(o).up; },
|
||||
findOutboundDown(o) { return this.outboundTrafficFor(o).down; },
|
||||
// Visual class for the protocol pill — keeps the colour palette consistent
|
||||
// across rows and matches the tone used elsewhere in the panel.
|
||||
outboundProtocolTone(protocol) {
|
||||
switch ((protocol || '').toLowerCase()) {
|
||||
case 'freedom': return 'tone-emerald';
|
||||
case 'blackhole': return 'tone-violet';
|
||||
case 'wireguard': return 'tone-cyan';
|
||||
case 'vmess': return 'tone-blue';
|
||||
case 'vless': return 'tone-sky';
|
||||
case 'trojan': return 'tone-amber';
|
||||
case 'shadowsocks': return 'tone-pink';
|
||||
case 'http':
|
||||
case 'socks': return 'tone-slate';
|
||||
case 'dns': return 'tone-indigo';
|
||||
default: return 'tone-slate';
|
||||
}
|
||||
return `${SizeFormatter.sizeFormat(0)} / ${SizeFormatter.sizeFormat(0)}`
|
||||
},
|
||||
// Distinct palette for the transport (network) pill so it never collides
|
||||
// with the protocol tone in the same row.
|
||||
outboundNetworkTone(network) {
|
||||
switch ((network || '').toLowerCase()) {
|
||||
case 'tcp': return 'tone-slate';
|
||||
case 'ws': return 'tone-cyan';
|
||||
case 'xhttp': return 'tone-amber';
|
||||
case 'h2':
|
||||
case 'http': return 'tone-indigo';
|
||||
case 'grpc': return 'tone-pink';
|
||||
case 'quic': return 'tone-violet';
|
||||
case 'kcp': return 'tone-emerald';
|
||||
case 'httpupgrade': return 'tone-rose';
|
||||
default: return 'tone-slate';
|
||||
}
|
||||
},
|
||||
// TLS / reality / none tone — visually tied to the security model used.
|
||||
outboundSecurityTone(security) {
|
||||
switch ((security || '').toLowerCase()) {
|
||||
case 'tls': return 'tone-emerald';
|
||||
case 'reality': return 'tone-mint';
|
||||
default: return 'tone-slate';
|
||||
}
|
||||
},
|
||||
// Whether the security label is one we render as a pill in the table.
|
||||
isOutboundSecurityVisible(security) {
|
||||
return security === 'tls' || security === 'reality';
|
||||
},
|
||||
// Null-safe accessor for the address list — collapses null/undefined
|
||||
// returns from findOutboundAddress() into an empty array so the template
|
||||
// can rely on .length and v-for without extra guards.
|
||||
outboundAddresses(o) {
|
||||
return this.findOutboundAddress(o) || [];
|
||||
},
|
||||
// Test-state accessors — sparse arrays + per-row state make raw checks
|
||||
// verbose; these helpers keep the template readable and consistent.
|
||||
isOutboundTesting(index) {
|
||||
const s = this.outboundTestStates[index];
|
||||
return !!(s && s.testing);
|
||||
},
|
||||
outboundTestResult(index) {
|
||||
const s = this.outboundTestStates[index];
|
||||
return s ? s.result : null;
|
||||
},
|
||||
isOutboundUntestable(outbound) {
|
||||
return outbound.protocol === 'blackhole' || outbound.tag === 'blocked';
|
||||
},
|
||||
findOutboundAddress(o) {
|
||||
serverObj = null;
|
||||
|
|
@ -1626,4 +1695,275 @@
|
|||
},
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
/* ───────── Modern outbounds table ─────────
|
||||
Visual goals:
|
||||
• flat surface, no inner cell borders, only subtle row dividers
|
||||
• rounded pill badges for protocol / tag / addresses
|
||||
• dual-arrow traffic widget that aligns across rows
|
||||
• consistent hover/loading/result states
|
||||
Scoped under .xray-page .outbounds-modern so it doesn't bleed into other tables. */
|
||||
|
||||
.xray-page .outbounds-modern { width: 100%; }
|
||||
.xray-page .outbounds-toolbar-right { text-align: right; }
|
||||
|
||||
/* Table chrome */
|
||||
.xray-page .outbounds-table .ant-table {
|
||||
background: transparent;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.xray-page .outbounds-table .ant-table-thead > tr > th {
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding: 14px 18px;
|
||||
}
|
||||
.light .xray-page .outbounds-table .ant-table-thead > tr > th {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
color: rgba(0, 0, 0, 0.55);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.xray-page .outbounds-table .ant-table-tbody > tr > td {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||||
padding: 16px 18px;
|
||||
transition: background-color 0.15s ease;
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* Force every cell to honour its column width — long content (especially
|
||||
long tags) must clip via cell-level ellipsis instead of pushing the row
|
||||
taller. */
|
||||
.xray-page .outbounds-table .ant-table-tbody > tr > td,
|
||||
.xray-page .outbounds-table .ant-table-thead > tr > th {
|
||||
overflow: hidden;
|
||||
}
|
||||
.light .xray-page .outbounds-table .ant-table-tbody > tr > td {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.xray-page .outbounds-table .ant-table-tbody > tr:last-child > td {
|
||||
border-bottom: none;
|
||||
}
|
||||
.xray-page .outbounds-table .ant-table-tbody > tr:hover > td {
|
||||
background: rgba(255, 255, 255, 0.035) !important;
|
||||
}
|
||||
.light .xray-page .outbounds-table .ant-table-tbody > tr:hover > td {
|
||||
background: rgba(0, 0, 0, 0.025) !important;
|
||||
}
|
||||
|
||||
/* Index + actions column */
|
||||
.xray-page .outbound-action-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.xray-page .outbound-index {
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-variant-numeric: tabular-nums;
|
||||
min-width: 18px;
|
||||
text-align: end;
|
||||
}
|
||||
.light .xray-page .outbound-index { color: rgba(0, 0, 0, 0.7); }
|
||||
.xray-page .outbound-action-btn {
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
.xray-page .outbound-action-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #fff;
|
||||
}
|
||||
.light .xray-page .outbound-action-btn {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
.light .xray-page .outbound-action-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Identity cell — tag on top, protocol/network/security pills underneath.
|
||||
Combining the two columns lets the table fit common viewports without
|
||||
a horizontal scrollbar. */
|
||||
.xray-page .outbound-identity-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
/* Tag — monospace, single line, ellipsis on overflow.
|
||||
A long tag (e.g. "vless_jphttp-ksjpnggl") would otherwise wrap and inflate
|
||||
the row's height; the inline tooltip surfaces the full value on hover. */
|
||||
.xray-page .outbound-tag {
|
||||
font-family: ui-monospace, SFMono-Regular, "JetBrains Mono", Menlo, monospace;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.light .xray-page .outbound-tag { color: rgba(0, 0, 0, 0.85); }
|
||||
|
||||
/* Address pills (monospace, monoline) */
|
||||
.xray-page .outbound-address-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
.xray-page .outbound-address-pill {
|
||||
font-family: ui-monospace, SFMono-Regular, "JetBrains Mono", Menlo, monospace;
|
||||
font-size: 12px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.045);
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
line-height: 1.5;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
display: inline-block;
|
||||
max-width: 240px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.light .xray-page .outbound-address-pill {
|
||||
background: rgba(0, 0, 0, 0.035);
|
||||
color: rgba(0, 0, 0, 0.78);
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.xray-page .outbound-address-empty {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Protocol/network/tls pills — shared "outbound-pill" with tonal modifiers.
|
||||
The pill row stays on a single line; if the column is somehow too narrow
|
||||
for all pills it overflows out of view (rare — column width is sized to
|
||||
fit the worst case) but never pushes the row taller. */
|
||||
.xray-page .outbound-protocol-cell {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.xray-page .outbound-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 22px;
|
||||
padding: 0 9px;
|
||||
border-radius: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.01em;
|
||||
border: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.xray-page .outbound-pill.tone-emerald { background: rgba(0, 191, 165, 0.14); color: #4dd4be; border-color: rgba(0, 191, 165, 0.28); }
|
||||
.xray-page .outbound-pill.tone-mint { background: rgba(72, 222, 128, 0.14); color: #7ee2a4; border-color: rgba(72, 222, 128, 0.30); }
|
||||
.xray-page .outbound-pill.tone-violet { background: rgba(155, 89, 219, 0.16); color: #b489e8; border-color: rgba(155, 89, 219, 0.32); }
|
||||
.xray-page .outbound-pill.tone-cyan { background: rgba(34, 184, 207, 0.16); color: #6ed3e3; border-color: rgba(34, 184, 207, 0.32); }
|
||||
.xray-page .outbound-pill.tone-sky { background: rgba(56, 189, 248, 0.16); color: #7dd3fc; border-color: rgba(56, 189, 248, 0.32); }
|
||||
.xray-page .outbound-pill.tone-blue { background: rgba(56, 116, 230, 0.16); color: #82a7ee; border-color: rgba(56, 116, 230, 0.32); }
|
||||
.xray-page .outbound-pill.tone-amber { background: rgba(231, 154, 47, 0.16); color: #f0b56a; border-color: rgba(231, 154, 47, 0.32); }
|
||||
.xray-page .outbound-pill.tone-pink { background: rgba(225, 79, 153, 0.16); color: #ec85b6; border-color: rgba(225, 79, 153, 0.32); }
|
||||
.xray-page .outbound-pill.tone-rose { background: rgba(244, 63, 94, 0.14); color: #fb7185; border-color: rgba(244, 63, 94, 0.30); }
|
||||
.xray-page .outbound-pill.tone-slate { background: rgba(160, 174, 192, 0.14); color: #b8c2d0; border-color: rgba(160, 174, 192, 0.26); }
|
||||
.xray-page .outbound-pill.tone-indigo { background: rgba(99, 102, 241, 0.16); color: #9ea0ee; border-color: rgba(99, 102, 241, 0.32); }
|
||||
|
||||
/* Traffic — dual arrow widget, fixed columns so all rows align */
|
||||
.xray-page .outbound-traffic-cell {
|
||||
display: inline-grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 5px 12px;
|
||||
border-radius: 100px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 13px;
|
||||
min-width: 0;
|
||||
}
|
||||
.light .xray-page .outbound-traffic-cell {
|
||||
background: rgba(0, 0, 0, 0.035);
|
||||
}
|
||||
.xray-page .outbound-traffic-up,
|
||||
.xray-page .outbound-traffic-down {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.xray-page .outbound-traffic-up { justify-content: flex-end; color: #4dd4be; }
|
||||
.xray-page .outbound-traffic-down { justify-content: flex-start; color: #82a7ee; }
|
||||
.xray-page .outbound-traffic-up .anticon,
|
||||
.xray-page .outbound-traffic-down .anticon { font-size: 11px; }
|
||||
.xray-page .outbound-traffic-sep {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-radius: 1px;
|
||||
}
|
||||
.light .xray-page .outbound-traffic-sep { background: rgba(0, 0, 0, 0.12); }
|
||||
|
||||
/* Test result pills */
|
||||
.xray-page .outbound-result-cell { display: inline-flex; }
|
||||
.xray-page .outbound-result-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 100px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.xray-page .outbound-result-pill .anticon { font-size: 12px; }
|
||||
.xray-page .outbound-result-ok {
|
||||
background: rgba(0, 191, 165, 0.14);
|
||||
color: #4dd4be;
|
||||
border-color: rgba(0, 191, 165, 0.28);
|
||||
}
|
||||
.xray-page .outbound-result-fail {
|
||||
background: rgba(255, 77, 79, 0.14);
|
||||
color: #ff7a7c;
|
||||
border-color: rgba(255, 77, 79, 0.32);
|
||||
}
|
||||
.xray-page .outbound-result-status { opacity: 0.75; }
|
||||
.xray-page .outbound-result-loading,
|
||||
.xray-page .outbound-result-idle {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 13px;
|
||||
}
|
||||
.light .xray-page .outbound-result-loading,
|
||||
.light .xray-page .outbound-result-idle { color: rgba(0, 0, 0, 0.4); }
|
||||
|
||||
/* Test button — sleek circular with subtle glow */
|
||||
.xray-page .outbound-test-btn {
|
||||
box-shadow: 0 2px 8px rgba(0, 191, 165, 0.18);
|
||||
transition: transform 0.12s ease, box-shadow 0.18s ease;
|
||||
}
|
||||
.xray-page .outbound-test-btn:hover:not([disabled]) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 14px rgba(0, 191, 165, 0.32);
|
||||
}
|
||||
.xray-page .outbound-test-btn[disabled] {
|
||||
box-shadow: none;
|
||||
opacity: 0.45;
|
||||
}
|
||||
</style>
|
||||
{{ template "page/body_end" .}}
|
||||
Loading…
Reference in a new issue