Spaces:
Paused
Paused
File size: 5,445 Bytes
462f5f6 1f4cfcb 462f5f6 a4ea50b 1f4cfcb a4ea50b 1f4cfcb a4ea50b 462f5f6 f82773a 1f4cfcb f82773a 1f4cfcb f82773a 1f4cfcb f82773a 1f4cfcb f82773a 1f4cfcb 462f5f6 f82773a 462f5f6 a4ea50b 462f5f6 f82773a a4ea50b f82773a 462f5f6 f82773a 1f4cfcb f82773a a4ea50b f82773a 1f4cfcb a4ea50b 1f4cfcb a4ea50b f82773a a4ea50b 1f4cfcb a4ea50b f82773a 1f4cfcb f82773a 462f5f6 f82773a 1f4cfcb 462f5f6 1f4cfcb 462f5f6 f82773a 1f4cfcb f82773a 462f5f6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | <template>
<div class="min-h-screen flex items-center justify-center p-4">
<div class="glass rounded-lg p-6 w-full max-w-2xl">
<h1 class="text-xl font-bold text-ink-950 text-center mb-2">AutoTeam 初始配置</h1>
<p class="text-sm text-ink-500 text-center mb-6">首次使用请按步骤填写以下配置</p>
<div v-if="message" class="mb-4 px-4 py-3 rounded-lg text-sm border" :class="messageClass">
{{ message }}
</div>
<!-- 卡片 1+2+3:Mail Provider 选择 / 连接 / 域名(Round 7 P2.2 抽 MailProviderCard) -->
<MailProviderCard
:model-value="form"
mode="setup"
@update:model-value="updateMailForm"
@state-change="onMailStateChange"
@verified="onMailVerified"
@error="onMailError" />
<!-- 卡片 4:其他配置 -->
<div class="mb-4 p-4 rounded-lg border" :class="cardClass('SAVE')">
<h2 class="text-sm font-semibold text-ink-950 mb-3">4. 其他配置</h2>
<div class="space-y-3" :class="state !== 'SAVE' ? 'opacity-40 pointer-events-none' : ''">
<div v-for="field in otherFields" :key="field.key">
<label class="block text-xs text-ink-600 mb-1">
{{ field.prompt }}
<span v-if="!field.optional" class="text-red-400">*</span>
<span v-if="field.key === 'API_KEY'" class="text-ink-500 text-[10px] ml-1">(留空自动生成)</span>
</label>
<input
v-model="form[field.key]"
:type="field.key.includes('PASSWORD') || field.key.includes('KEY') ? 'password' : 'text'"
:placeholder="field.default || ''"
class="w-full px-3 py-2 bg-surface border border-hairline rounded text-sm text-ink-950 focus-ring" />
</div>
</div>
</div>
<button
@click="save"
:disabled="saving || state !== 'SAVE'"
class="w-full px-4 py-2.5 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-on-accent text-sm font-medium rounded-lg transition focus-ring">
{{ saving ? '验证并保存中...' : '保存配置' }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { api, setApiKey } from '../api.js'
import MailProviderCard from './MailProviderCard.vue'
const emit = defineEmits(['configured'])
// 状态机:PROVIDER → CONNECTION → DOMAIN → SAVE (SPEC-1 §4.1)
// Round 7 P2.2 — 状态机已下沉到 MailProviderCard,父组件只持有 state 联动 SAVE 卡片解锁。
const state = ref('PROVIDER')
const form = reactive({
MAIL_PROVIDER: 'cf_temp_email',
CLOUDMAIL_BASE_URL: '',
CLOUDMAIL_PASSWORD: '',
CLOUDMAIL_DOMAIN: '',
MAILLAB_API_URL: '',
MAILLAB_USERNAME: '',
MAILLAB_PASSWORD: '',
MAILLAB_DOMAIN: '',
CPA_URL: 'http://127.0.0.1:8317',
CPA_KEY: '',
PLAYWRIGHT_PROXY_URL: '',
PLAYWRIGHT_PROXY_BYPASS: '',
API_KEY: '',
})
const fields = ref([])
const saving = ref(false)
const message = ref('')
const messageClass = ref('')
const otherFields = computed(() => fields.value.filter(f =>
!['MAIL_PROVIDER', 'CLOUDMAIL_BASE_URL', 'CLOUDMAIL_EMAIL', 'CLOUDMAIL_PASSWORD', 'CLOUDMAIL_DOMAIN',
'MAILLAB_API_URL', 'MAILLAB_USERNAME', 'MAILLAB_PASSWORD', 'MAILLAB_DOMAIN'].includes(f.key)
))
function cardClass(targetState) {
const order = ['PROVIDER', 'CONNECTION', 'DOMAIN', 'SAVE']
const cur = order.indexOf(state.value)
const tgt = order.indexOf(targetState)
if (tgt > cur) return 'bg-ink-50 border-hairline'
if (tgt < cur) return 'bg-emerald-50 border-emerald-200'
return 'bg-indigo-50 border-indigo-200'
}
function onMailStateChange(s) {
state.value = s
}
function updateMailForm(value) {
Object.assign(form, value || {})
}
function onMailVerified(payload) {
if (payload?.leakedProbe) {
message.value = `提醒: 探测邮箱回收失败,请到后台手动删除: ${payload.leakedProbe.email}`
messageClass.value = 'bg-amber-50 text-amber-800 border-amber-200'
}
}
function onMailError(resp) {
if (resp?.soft && resp.warnings) {
message.value = '提醒: ' + resp.warnings.join('; ')
messageClass.value = 'bg-amber-50 text-amber-800 border-amber-200'
return
}
message.value = `${resp.error_code || 'ERROR'}: ${resp.message || '未知错误'}` +
(resp.hint ? ` — ${resp.hint}` : '')
messageClass.value = 'bg-rose-50 text-rose-700 border-rose-200'
}
async function save() {
saving.value = true
message.value = ''
try {
const result = await api.saveSetup({ ...form })
if (result.api_key) setApiKey(result.api_key)
message.value = result.message || '保存成功'
messageClass.value = 'bg-emerald-50 text-emerald-700 border-emerald-200'
setTimeout(() => emit('configured'), 1000)
} catch (e) {
message.value = e.message
messageClass.value = 'bg-rose-50 text-rose-700 border-rose-200'
} finally {
saving.value = false
}
}
onMounted(async () => {
try {
const result = await api.getSetupStatus()
fields.value = result.fields
for (const f of result.fields) {
if (form[f.key] === '' || form[f.key] === undefined) {
form[f.key] = f.default || ''
}
}
if (result.provider) form.MAIL_PROVIDER = result.provider
} catch (e) {
message.value = '获取配置状态失败: ' + e.message
messageClass.value = 'bg-rose-50 text-rose-700 border-rose-200'
}
})
</script>
|