|
|
@@ -4,8 +4,7 @@
|
|
|
<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>
|
|
|
+ <span v-if="!isMobile">{{ i18n "pages.xray.outbound.addOutbound" }}</span>
|
|
|
</a-button>
|
|
|
<a-button type="primary" icon="cloud" @click="showWarp()">WARP</a-button>
|
|
|
<a-button type="primary" icon="api" @click="showNord()">NordVPN</a-button>
|
|
|
@@ -25,9 +24,114 @@
|
|
|
</a-button-group>
|
|
|
</a-col>
|
|
|
</a-row>
|
|
|
- <a-table :columns="outboundColumns" :row-key="r => r.key"
|
|
|
+
|
|
|
+ <!-- Mobile: card list -->
|
|
|
+ <template v-if="isMobile">
|
|
|
+ <div v-if="outboundData.length === 0" class="outbound-card-empty">—</div>
|
|
|
+ <div v-for="(outbound, index) in outboundData" :key="outbound.key" class="outbound-card">
|
|
|
+ <!-- card header: number + tag + protocol pills + action menu -->
|
|
|
+ <div class="outbound-card-header">
|
|
|
+ <div class="outbound-card-identity">
|
|
|
+ <div class="outbound-card-title">
|
|
|
+ <span class="outbound-card-num">[[ index + 1 ]]</span>
|
|
|
+ <a-tooltip :title="outbound.tag" :overlay-class-name="themeSwitcher.currentTheme">
|
|
|
+ <span class="outbound-tag">[[ outbound.tag ]]</span>
|
|
|
+ </a-tooltip>
|
|
|
+ </div>
|
|
|
+ <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>
|
|
|
+ <span class="outbound-pill" :class="outboundSecurityTone(outbound.streamSettings.security)"
|
|
|
+ v-if="isOutboundSecurityVisible(outbound.streamSettings.security)">
|
|
|
+ [[ outbound.streamSettings.security ]]
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <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>
|
|
|
+ <!-- address pills -->
|
|
|
+ <div class="outbound-address-list" v-if="outboundAddresses(outbound).length > 0">
|
|
|
+ <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>
|
|
|
+ </div>
|
|
|
+ <!-- card footer: traffic + test -->
|
|
|
+ <div class="outbound-card-footer">
|
|
|
+ <div class="outbound-traffic-cell">
|
|
|
+ <span class="outbound-traffic-up">
|
|
|
+ <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">
|
|
|
+ <a-icon type="arrow-down"></a-icon>
|
|
|
+ [[ SizeFormatter.sizeFormat(findOutboundDown(outbound)) ]]
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div class="outbound-card-test">
|
|
|
+ <div 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>
|
|
|
+ <a-tooltip v-else :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>
|
|
|
+ <a-icon type="loading" class="outbound-result-loading"
|
|
|
+ v-else-if="isOutboundTesting(index)"></a-icon>
|
|
|
+ <a-button type="primary" shape="circle" size="small" icon="thunderbolt"
|
|
|
+ class="outbound-test-btn"
|
|
|
+ :loading="isOutboundTesting(index)"
|
|
|
+ @click="testOutbound(index)"
|
|
|
+ :disabled="isOutboundUntestable(outbound) || isOutboundTesting(index)">
|
|
|
+ </a-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- Desktop: table -->
|
|
|
+ <a-table v-if="!isMobile" :columns="outboundColumns" :row-key="r => r.key"
|
|
|
:data-source="outboundData"
|
|
|
- :scroll="isMobile ? { x: 720 } : {}"
|
|
|
+ :scroll="{}"
|
|
|
:pagination="false"
|
|
|
:indent-size="0"
|
|
|
class="outbounds-table"
|