| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696 |
- <template>
- <!-- eslint-disable vue/no-v-model-argument -->
- <div class="proxy-settings-container">
- <p class="proxy-subtitle">选择代理品牌后,在下方配置该品牌的代理信息。所有配置仅保存在本机,不上传服务器。</p>
- <!-- 品牌卡片:上方图片,下方品牌名称 + 网址 -->
- <div class="settings-section">
- <div class="section-header">
- <span class="section-title">选择代理品牌</span>
- </div>
- <div class="brand-cards">
- <div
- v-for="brand in brands"
- :key="brand.id"
- class="brand-card"
- :class="{ active: selectedBrandId === brand.id }"
- @click="selectBrand(brand.id)"
- >
- <div class="brand-card-cover">
- <img :src="brand.logo" :alt="brand.name" class="brand-logo" @error="onLogoError" />
- </div>
- <div class="brand-card-body">
- <div class="brand-name">{{ brand.name }}</div>
- <a :href="brand.url" target="_blank" rel="noopener noreferrer" class="brand-url" @click.stop>{{ brand.url }}</a>
- <a-tag v-if="effectiveBrandId === brand.id" color="green" class="effective-tag">
- {{ effectiveMode === 'api' ? 'API方式有效' : effectiveMode === 'account' ? '账号密码方式有效' : '有效' }}
- </a-tag>
- </div>
- </div>
- </div>
- </div>
- <!-- 配置区域:选择品牌后显示 -->
- <template v-if="selectedBrandId">
- <div class="settings-section">
- <div class="section-header">
- <span class="section-title">配置该品牌代理</span>
- </div>
- <a-radio-group v-model:value="form.mode" class="proxy-mode-group">
- <a-radio value="api">API 方式</a-radio>
- <a-radio value="account">账号密码方式</a-radio>
- </a-radio-group>
- </div>
- <!-- API 方式:HTTP/HTTPS 代理 API -->
- <div v-if="form.mode === 'api'" class="settings-section">
- <div class="form-group">
- <div class="label-with-action">
- <label class="form-label">API 地址(URL)- HTTP/HTTPS</label>
- <a-button
- size="small"
- :loading="testingKey === 'api-http'"
- @click="testApiProxy('http')"
- >
- 测试
- </a-button>
- </div>
- <a-input
- v-model:value="form.apiUrl"
- placeholder="请输入获取 HTTP/HTTPS 代理的 API 地址"
- allow-clear
- />
- </div>
- </div>
- <!-- 账号密码方式:HTTP/HTTPS 代理 + SOCKS5 代理 -->
- <div v-if="form.mode === 'account'" class="settings-section">
- <div class="proxy-type-header">
- <div class="proxy-type-title">HTTP/HTTPS 代理</div>
- <a-button
- size="small"
- :loading="testingKey === 'account-http'"
- @click="testAccountProxy('http')"
- >
- 测试
- </a-button>
- </div>
- <div class="form-row">
- <div class="form-group half">
- <label class="form-label">代理服务器</label>
- <a-input
- v-model:value="form.serverHost"
- placeholder="例如: proxy.example.com"
- allow-clear
- />
- </div>
- <div class="form-group half">
- <label class="form-label">端口</label>
- <a-input-number
- v-model:value="form.serverPort"
- placeholder="8080"
- :min="1"
- :max="65535"
- style="width: 100%"
- />
- </div>
- </div>
- <div class="form-row">
- <div class="form-group half">
- <label class="form-label">账号</label>
- <a-input
- v-model:value="form.serverUsername"
- placeholder="请输入账号"
- allow-clear
- />
- </div>
- <div class="form-group half">
- <label class="form-label">密码</label>
- <a-input-password
- v-model:value="form.serverPassword"
- placeholder="请输入密码"
- allow-clear
- />
- </div>
- </div>
- </div>
- <!-- 当前有效配置:全局唯一,同一品牌下也只能有一种方式(API 或 账号密码)为有效 -->
- <div class="settings-section">
- <div class="section-header">
- <span class="section-title">当前有效配置</span>
- </div>
- <div class="effective-option-hint">调用 Steam 接口时仅使用被设为「有效」的配置;全局只能有一个有效配置,同一品牌下也只能有一种方式(API 或 账号密码)为有效。</div>
- <a-radio-group v-model:value="effectiveOption" class="effective-radio-group" @change="onEffectiveOptionChange">
- <a-radio value="none">无(不启用任何代理配置)</a-radio>
- <a-radio value="api">API 方式 为当前有效配置</a-radio>
- <a-radio value="account">账号密码方式 为当前有效配置</a-radio>
- </a-radio-group>
- </div>
- <div class="proxy-actions">
- <a-button type="primary" :loading="saving" @click="save">保存配置</a-button>
- </div>
- </template>
- <a-modal
- v-model:open="testDialog.open"
- :title="testDialog.title"
- width="760px"
- :mask-closable="false"
- :footer="null"
- >
- <div class="test-status-line">
- <a-tag v-if="testDialog.running" color="processing">测试中</a-tag>
- <a-tag v-else-if="testDialog.success" color="success">测试通过</a-tag>
- <a-tag v-else color="error">测试失败</a-tag>
- </div>
- <pre class="test-log-panel">{{ testDialog.logs.join('\n') }}</pre>
- <div class="test-dialog-footer">
- <a-button type="primary" @click="testDialog.open = false">关闭</a-button>
- </div>
- </a-modal>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, reactive, computed, onMounted, watch } from 'vue'
- import { invoke } from '@tauri-apps/api/tauri'
- import { message } from 'ant-design-vue'
- import { useAuthStore } from '../stores/authStore'
- const authStore = useAuthStore()
- interface Brand {
- id: string
- name: string
- url: string
- logo: string
- }
- const brands: Brand[] = [
- {
- id: 'haiwaiip',
- name: '海外代理IP',
- url: 'https://haiwaiip.net/cn/register?icode=481',
- logo: '/youdangdang/haiwaiip.png'
- }
- ]
- interface BrandConfig {
- mode: 'api' | 'account'
- // HTTP/HTTPS 代理 - API 方式
- apiUrl: string
- // SOCKS5 代理 - API 方式
- socks5ApiUrl: string
- // HTTP/HTTPS 代理 - 账号密码方式
- serverHost: string
- serverPort: number | null
- serverUsername: string
- serverPassword: string
- // SOCKS5 代理 - 账号密码方式
- socks5ServerHost: string
- socks5ServerPort: number | null
- socks5ServerUsername: string
- socks5ServerPassword: string
- // 是否跳过 TLS 证书校验(仅用于代理链路容错)
- insecureSkipTlsVerify?: boolean
- }
- const defaultBrandConfig = (): BrandConfig => ({
- mode: 'api',
- apiUrl: '',
- socks5ApiUrl: '',
- serverHost: '',
- serverPort: null,
- serverUsername: '',
- serverPassword: '',
- socks5ServerHost: '',
- socks5ServerPort: null,
- socks5ServerUsername: '',
- socks5ServerPassword: '',
- insecureSkipTlsVerify: false
- })
- type EffectiveMode = 'api' | 'account'
- interface StoredConfig {
- selectedBrandId: string
- effectiveBrandId: string | null
- effectiveMode: EffectiveMode | null
- configs: Record<string, BrandConfig>
- }
- interface ProxyTestResponse {
- success: boolean
- logs: string[]
- error?: string
- }
- const selectedBrandId = ref<string>(brands[0]?.id ?? '')
- const effectiveBrandId = ref<string | null>(null)
- const effectiveMode = ref<EffectiveMode | null>(null)
- const form = reactive<BrandConfig>(defaultBrandConfig())
- const configsByBrand = reactive<Record<string, BrandConfig>>({})
- const saving = ref(false)
- const testingKey = ref<string | null>(null)
- const testDialog = reactive({
- open: false,
- title: '代理测试',
- running: false,
- success: false,
- logs: [] as string[]
- })
- /** 当前选中的「有效」选项:无 / API 方式 / 账号密码方式(仅对当前品牌而言),默认选中 API 方式 */
- const effectiveOption = computed({
- get() {
- if (effectiveBrandId.value !== selectedBrandId.value) return 'api'
- return effectiveMode.value === 'api' ? 'api' : effectiveMode.value === 'account' ? 'account' : 'api'
- },
- set(v: 'none' | 'api' | 'account') {
- if (v === 'none') {
- if (effectiveBrandId.value === selectedBrandId.value) {
- effectiveBrandId.value = null
- effectiveMode.value = null
- }
- } else {
- effectiveBrandId.value = selectedBrandId.value
- effectiveMode.value = v
- }
- }
- })
- function onEffectiveOptionChange() {
- save()
- }
- function selectBrand(id: string) {
- // 当前表单写回当前品牌
- if (selectedBrandId.value) {
- configsByBrand[selectedBrandId.value] = { ...form }
- }
- selectedBrandId.value = id
- // 加载该品牌已保存的配置
- const saved = configsByBrand[id]
- if (saved) {
- form.mode = saved.mode
- form.apiUrl = saved.apiUrl ?? ''
- form.socks5ApiUrl = saved.socks5ApiUrl ?? ''
- form.serverHost = saved.serverHost ?? ''
- form.serverPort = saved.serverPort ?? null
- form.serverUsername = saved.serverUsername ?? ''
- form.serverPassword = saved.serverPassword ?? ''
- form.socks5ServerHost = saved.socks5ServerHost ?? ''
- form.socks5ServerPort = saved.socks5ServerPort ?? null
- form.socks5ServerUsername = saved.socks5ServerUsername ?? ''
- form.socks5ServerPassword = saved.socks5ServerPassword ?? ''
- } else {
- Object.assign(form, defaultBrandConfig())
- }
- }
- function loadFromLocal() {
- invoke<string>('read_proxy_config')
- .then((raw) => {
- if (!raw || raw === '{}') return
- try {
- const data = JSON.parse(raw) as StoredConfig & Partial<BrandConfig> & { proxyUrl?: string }
- if (data.selectedBrandId) selectedBrandId.value = data.selectedBrandId
- if (data.effectiveBrandId != null) effectiveBrandId.value = data.effectiveBrandId
- if (data.effectiveMode != null) effectiveMode.value = data.effectiveMode
- if (data.configs && typeof data.configs === 'object') {
- Object.keys(data.configs).forEach((id) => {
- configsByBrand[id] = { ...defaultBrandConfig(), ...data.configs![id] }
- })
- }
- // 兼容旧版单表单格式:无 configs 但有 mode/apiUrl 等时,写入当前品牌
- if (!data.configs && (data.mode || data.proxyUrl != null || data.apiUrl != null)) {
- const migrated: BrandConfig = {
- mode: (data.mode as BrandConfig['mode']) || 'api',
- apiUrl: data.apiUrl ?? data.proxyUrl ?? '',
- socks5ApiUrl: '',
- serverHost: data.serverHost ?? '',
- serverPort: data.serverPort ?? null,
- serverUsername: data.serverUsername ?? '',
- serverPassword: data.serverPassword ?? '',
- socks5ServerHost: '',
- socks5ServerPort: null,
- socks5ServerUsername: '',
- socks5ServerPassword: ''
- }
- configsByBrand[selectedBrandId.value] = migrated
- }
- const cur = configsByBrand[selectedBrandId.value]
- if (cur) Object.assign(form, cur)
- } catch (_) {
- // ignore
- }
- })
- .catch((err) => {
- console.warn('读取代理配置失败:', err)
- })
- }
- function save() {
- if (!selectedBrandId.value) return
- configsByBrand[selectedBrandId.value] = { ...form }
- saving.value = true
- // 如果没有设置有效配置,默认使用当前品牌的 API 方式
- if (!effectiveBrandId.value) {
- effectiveBrandId.value = selectedBrandId.value
- effectiveMode.value = 'api'
- }
- const persistedConfigs: Record<string, BrandConfig> = {}
- Object.keys(configsByBrand).forEach((brandId) => {
- const config = { ...configsByBrand[brandId] }
- // 按需默认:海外代理IP保存时强制开启跳过 TLS 校验
- if (brandId === 'haiwaiip') {
- config.insecureSkipTlsVerify = true
- }
- persistedConfigs[brandId] = config
- })
- const payload: StoredConfig = {
- selectedBrandId: selectedBrandId.value,
- effectiveBrandId: effectiveBrandId.value,
- effectiveMode: effectiveBrandId.value ? effectiveMode.value : null,
- configs: persistedConfigs
- }
- invoke('write_proxy_config', { json: JSON.stringify(payload) })
- .then(() => {
- message.success('配置已保存到本机')
- saving.value = false
- })
- .catch((err) => {
- message.error(err?.toString?.() || '保存失败')
- saving.value = false
- })
- }
- async function runProxyTest(
- title: string,
- key: string,
- payload: Record<string, unknown>
- ) {
- if (testingKey.value) return
- testingKey.value = key
- testDialog.open = true
- testDialog.title = title
- testDialog.running = true
- testDialog.success = false
- testDialog.logs = ['开始测试,请稍候...']
- try {
- const raw = await invoke<string>('test_proxy_configuration', { payload: JSON.stringify(payload) })
- const result = JSON.parse(raw) as ProxyTestResponse
- testDialog.logs = Array.isArray(result.logs) && result.logs.length > 0 ? result.logs : ['未返回测试日志']
- testDialog.success = !!result.success
- testDialog.running = false
- if (result.success) {
- message.success('代理测试通过')
- } else {
- message.error(result.error || '代理测试失败')
- }
- } catch (err: any) {
- testDialog.logs = [...testDialog.logs, `测试异常: ${err?.toString?.() || '未知错误'}`]
- testDialog.success = false
- testDialog.running = false
- message.error('代理测试异常,请重试')
- } finally {
- testingKey.value = null
- }
- }
- function testApiProxy(protocol: 'http' | 'socks5') {
- if (protocol === 'http') {
- runProxyTest('API 方式 - HTTP/HTTPS 测试', 'api-http', {
- mode: 'api',
- protocol: 'http',
- apiUrl: form.apiUrl
- })
- return
- }
- runProxyTest('API 方式 - SOCKS5 测试', 'api-socks5', {
- mode: 'api',
- protocol: 'socks5',
- apiUrl: form.socks5ApiUrl
- })
- }
- function testAccountProxy(protocol: 'http' | 'socks5') {
- if (protocol === 'http') {
- runProxyTest('账号密码方式 - HTTP/HTTPS 测试', 'account-http', {
- mode: 'account',
- protocol: 'http',
- serverHost: form.serverHost,
- serverPort: form.serverPort,
- serverUsername: form.serverUsername,
- serverPassword: form.serverPassword
- })
- return
- }
- runProxyTest('账号密码方式 - SOCKS5 测试', 'account-socks5', {
- mode: 'account',
- protocol: 'socks5',
- serverHost: form.socks5ServerHost,
- serverPort: form.socks5ServerPort,
- serverUsername: form.socks5ServerUsername,
- serverPassword: form.socks5ServerPassword
- })
- }
- function onLogoError(e: Event) {
- const img = e.target as HTMLImageElement
- if (img) img.style.display = 'none'
- }
- // 初次加载后若当前品牌没有缓存,用默认空表单
- watch(
- selectedBrandId,
- (id) => {
- if (!id) return
- if (!configsByBrand[id]) {
- configsByBrand[id] = defaultBrandConfig()
- Object.assign(form, defaultBrandConfig())
- }
- },
- { immediate: true }
- )
- onMounted(() => {
- loadFromLocal()
- })
- </script>
- <style scoped>
- .proxy-settings-container {
- min-height: 100%;
- min-width: 0;
- padding: 0;
- text-align: center;
- box-sizing: border-box;
- }
- .proxy-title {
- font-size: 22px;
- font-weight: 600;
- color: #1a1a1a;
- margin: 0 0 8px 0;
- }
- .proxy-subtitle {
- font-size: 13px;
- color: #666;
- margin: 0 0 16px 0;
- text-align: center;
- }
- .settings-section {
- margin-bottom: 24px;
- padding-bottom: 20px;
- border-bottom: 1px solid #f0f0f0;
- }
- .settings-section:last-of-type {
- border-bottom: none;
- }
- .section-header {
- margin-bottom: 16px;
- }
- .section-title {
- font-size: 15px;
- font-weight: 600;
- color: #333;
- text-align: left;
- }
- .brand-cards {
- display: flex;
- flex-wrap: nowrap;
- gap: 16px;
- overflow-x: auto;
- overflow-y: hidden;
- padding-bottom: 8px;
- }
- .brand-cards::-webkit-scrollbar {
- height: 6px;
- }
- .brand-cards::-webkit-scrollbar-thumb {
- background: #d9d9d9;
- border-radius: 3px;
- }
- .brand-card {
- flex-shrink: 0;
- width: 140px;
- border-radius: 12px;
- overflow: hidden;
- border: 2px solid #f0f0f0;
- cursor: pointer;
- transition: all 0.2s;
- background: #fff;
- }
- .brand-card:hover {
- border-color: #667eea;
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
- }
- .brand-card.active {
- border-color: #667eea;
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.25);
- }
- .brand-card-cover {
- height: 56px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #fafafa;
- }
- .brand-logo {
- max-width: 72px;
- max-height: 40px;
- object-fit: contain;
- }
- .brand-card-body {
- padding: 8px 10px;
- text-align: center;
- }
- .brand-name {
- font-size: 13px;
- font-weight: 600;
- color: #333;
- margin-bottom: 4px;
- }
- .brand-url {
- font-size: 11px;
- color: #667eea;
- word-break: break-all;
- text-decoration: none;
- }
- .brand-url:hover {
- text-decoration: underline;
- }
- .effective-tag {
- display: block;
- margin-top: 8px;
- font-size: 11px;
- text-align: center;
- }
- .effective-option-hint {
- font-size: 12px;
- color: #999;
- margin-bottom: 12px;
- }
- .effective-radio-group {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- gap: 16px 24px;
- }
- .proxy-mode-group {
- display: flex;
- gap: 24px;
- }
- .form-row {
- display: flex;
- gap: 16px;
- margin-bottom: 16px;
- }
- .form-row .form-group.half {
- flex: 1;
- margin-bottom: 0;
- }
- .form-group {
- margin-bottom: 16px;
- }
- .form-group:last-child,
- .form-group.half {
- margin-bottom: 0;
- }
- .form-label {
- display: block;
- font-size: 14px;
- font-weight: 500;
- color: #333;
- margin-bottom: 8px;
- }
- .label-with-action {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- }
- .proxy-type-title {
- font-size: 14px;
- font-weight: 600;
- color: #1a1a1a;
- }
- .proxy-type-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- margin-bottom: 16px;
- padding-bottom: 8px;
- border-bottom: 1px solid #f0f0f0;
- }
- .proxy-actions {
- padding-top: 16px;
- border-top: 1px solid #f0f0f0;
- display: flex;
- justify-content: center;
- }
- .proxy-actions :deep(.ant-btn) {
- width: 200px;
- }
- .test-status-line {
- margin-bottom: 12px;
- }
- .test-log-panel {
- margin: 0;
- min-height: 260px;
- max-height: 420px;
- overflow: auto;
- background: #fafafa;
- border: 1px solid #f0f0f0;
- border-radius: 8px;
- padding: 12px;
- font-size: 12px;
- line-height: 1.55;
- white-space: pre-wrap;
- word-break: break-word;
- }
- .test-dialog-footer {
- margin-top: 16px;
- text-align: right;
- }
- </style>
|