Smail/frontend/src/views/Setup/Setup.vue

478 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div id="main" :class="['setup-page', {
'database-page': active === 1,
'dns-page': active === 4,
'ssl-page': active === 5
}]">
<el-steps :active="active" align-center finish-status="success" id="status">
<el-step :title="lang.welcome"/>
<el-step :title="lang.setDatabase"/>
<el-step :title="lang.setAdminPassword"/>
<el-step :title="lang.SetDomail"/>
<el-step :title="lang.setDNS"/>
<el-step :title="lang.setSSL"/>
</el-steps>
<div v-if="active === 0" class="ctn">
<div class="desc">
<h2>{{ lang.tks_smail }}</h2>
<div style="margin-top: 10px;">{{ lang.guid_desc }}</div>
</div>
</div>
<div v-if="active === 1" class="ctn setup-form-panel">
<div class="desc">
<h2>{{ lang.select_db }}</h2>
<div style="margin-top: 10px;">{{ lang.db_desc }}</div>
</div>
<div class="form database-form">
<el-form label-position="top">
<el-form-item :label="lang.type">
<el-select :placeholder="lang.db_select_ph" v-model="dbSettings.type"
:disabled="databaseLoading" @change="handleDbTypeChange">
<el-option label="MySQL" value="mysql"/>
<el-option label="SQLite3" value="sqlite"/>
<el-option label="PostgreSQL" value="postgres"/>
</el-select>
</el-form-item>
<el-alert
v-if="databaseError"
:title="databaseError"
type="error"
show-icon
:closable="false"
class="database-error"
/>
<div v-if="dbSettings.type === 'mysql' || dbSettings.type === 'postgres'" class="mysql-fields">
<div class="database-hint">
{{ dbSettings.type === 'mysql' ? lang.mysql_create_desc : lang.postgres_create_desc }}
</div>
<el-form-item class="database-user-field" :label="lang.db_user">
<el-input v-model="dbSettings.user" @input="clearDatabaseError"
:disabled="databaseLoading"
:placeholder="dbSettings.type === 'mysql' ? lang.db_user_ph : lang.pg_user_ph"
autocomplete="username"></el-input>
</el-form-item>
<el-form-item class="database-host-field" :label="lang.db_host">
<el-input v-model="dbSettings.host" @input="clearDatabaseError"
:disabled="databaseLoading"
:placeholder="lang.db_host_ph"></el-input>
</el-form-item>
<el-form-item class="database-port-field" :label="lang.db_port">
<el-input v-model="dbSettings.port" @input="clearDatabaseError"
:disabled="databaseLoading"
type="number"
min="1"
max="65535"
step="1"
inputmode="numeric"
:placeholder="lang.db_port_ph"></el-input>
</el-form-item>
<el-form-item class="database-name-field" :label="lang.db_name">
<el-input v-model="dbSettings.name" @input="clearDatabaseError"
:disabled="databaseLoading"
:placeholder="lang.db_name_ph"></el-input>
</el-form-item>
<el-form-item class="database-password-field" :label="lang.db_password">
<el-input v-model="dbSettings.password" @input="clearDatabaseError"
:disabled="databaseLoading"
type="password"
:placeholder="dbSettings.type === 'mysql' ? lang.db_password_ph : lang.pg_password_ph" autocomplete="new-password"
show-password></el-input>
<div class="database-field-tip">{{ lang.db_password_desc }}</div>
</el-form-item>
</div>
<div class="database-retry-tip">{{ lang.db_init_retry_tip }}</div>
<el-form-item :label="lang.sqlite_db_path" v-if="dbSettings.type === 'sqlite'">
<el-input v-model="dbSettings.dsn" @input="clearDatabaseError"
:disabled="databaseLoading"
placeholder="请输入 SQLite 数据文件路径"></el-input>
</el-form-item>
</el-form>
</div>
</div>
<div v-if="active === 2" class="ctn">
<div class="desc">
<h2>{{ lang.setAdminPassword }}</h2>
<!-- <div style="margin-top: 10px;">{{ lang.domain_desc }}</div> -->
</div>
<div class="form narrow-form">
<el-form label-width="120px">
<el-form-item :label="lang.admin_account">
<el-input v-bind:disabled="adminSettings.hadSeted" placeholder="请输入管理员用户名"
v-model="adminSettings.account"></el-input>
</el-form-item>
<el-form-item :label="lang.password">
<el-input type="password" v-bind:disabled="adminSettings.hadSeted"
placeholder="请输入 8 到 12 个字节的密码"
v-model="adminSettings.password"
minlength="8"
maxlength="12"
autocomplete="new-password"
show-password></el-input>
<div class="password-rule-tip">{{ lang.admin_pwd_desc }}</div>
</el-form-item>
<el-form-item :label="lang.enter_again">
<el-input type="password" v-bind:disabled="adminSettings.hadSeted"
placeholder="请再次输入密码"
v-model="adminSettings.password2"
minlength="8"
maxlength="12"
autocomplete="new-password"
show-password></el-input>
</el-form-item>
</el-form>
</div>
</div>
<div v-if="active === 3" class="ctn">
<div class="desc">
<h2>{{ lang.SetDomail }}</h2>
<!-- <div style="margin-top: 10px;">{{ lang.domain_desc }}</div> -->
</div>
<div class="form narrow-form">
<el-form label-width="120px">
<el-form-item :label="lang.smtp_domain">
<el-input placeholder="请输入 SMTP 域名" v-model="domainSettings.smtp_domain">
<template #prepend>smtp.</template>
</el-input>
</el-form-item>
<el-form-item :label="lang.web_domain">
<el-input placeholder="请输入 Web 管理域名" v-model="domainSettings.web_domain"></el-input>
</el-form-item>
<el-form-item label="公网 HTTPS 端口">
<el-input
v-model="domainSettings.public_https_port"
inputmode="numeric"
maxlength="5"
placeholder="留空时使用当前部署的默认端口"
></el-input>
<p class="field-hint">
填写浏览器访问后台使用的端口。Nginx 对外提供标准 HTTPS 时填写 443;直连部署填写实际 HTTPS 端口。此设置不会改变 Smail 内部监听端口。
</p>
</el-form-item>
<el-form-item :label="lang.multi_domain_setting">
<span>{{ lang.multi_domain_setting_desc }}
<el-button @click="addDomain" size="small"
type="success" :icon="Plus"
circle>
</el-button>
</span>
<el-input :placeholder="'请输入第 ' + (i + 1) + ' 个域名'" v-for="(item, i) in domainSettings.multi_domain "
v-model="domainSettings.multi_domain[i]" :key="item"></el-input>
</el-form-item>
</el-form>
</div>
</div>
<div v-if="active === 4" class="dns-panel">
<div class="dns-header">
<div class="desc dns-heading">
<div class="dns-eyebrow">{{ lang.dns_setup_step }}</div>
<h2>{{ lang.setDNS }}</h2>
<div class="dns-description">{{ lang.dns_desc }}</div>
</div>
<div class="dns-header-notes">
<div class="dns-header-note">{{ lang.dns_record_format }}</div>
<div class="dns-header-note dns-header-note-secondary">{{ lang.dns_ip_source }}</div>
<div v-if="!hasDNSIPv6" class="dns-header-note dns-header-note-ipv6">
{{ lang.dns_ipv6_unavailable }}
</div>
</div>
</div>
<div class="dns-address-controls">
<div class="dns-address-form">
<label for="dns-mail-ipv4">邮件服务器公网 IPv4</label>
<el-input
id="dns-mail-ipv4"
v-model="dnsMailIPv4"
class="dns-address-input"
placeholder="留空自动探测,失败时可手工填写"
:disabled="dnsLoading"
clearable
@keyup.enter="getDNSConfig()"
/>
<el-button type="primary" :loading="dnsLoading" @click="getDNSConfig()">生成记录</el-button>
<el-button :disabled="dnsLoading" @click="detectDNSConfig()">重新自动探测</el-button>
</div>
<p class="dns-address-note">仅生成解析建议,不会修改真实 DNS;后台地址独立解析,MX、SPF、DKIM、DMARC 仍需保留。</p>
<p v-if="dnsError" class="dns-address-warning" role="alert">{{ dnsError }}</p>
<p v-else-if="dnsInputPending" class="dns-address-warning" role="status">地址已修改,请生成记录后再复制。</p>
<p v-else-if="hasUnavailableDNS" class="dns-address-warning" role="status">部分记录尚未就绪,已禁用复制。请补充公网 IP 或检查后台域名解析;已自行配置 DNS 的用户仍可继续。</p>
<p v-else-if="dnsAppliedIPv4" class="dns-address-note" role="status">当前建议使用手工地址:{{ dnsAppliedIPv4 }}。请确认它是邮件服务器实际使用的公网地址。</p>
</div>
<div v-if="Object.keys(dnsInfos).length > 0" class="dns-domain-list" :aria-busy="dnsLoading">
<section class="dns-domain-card" v-for="(info, domain) in dnsInfos" :key="domain">
<div class="dns-domain-header">
<div>
<h3>{{ domain }}</h3>
<span class="dns-domain-suffix">{{ lang.dns_domain_suffix }}</span>
</div>
<el-tag class="dns-domain-count" effect="plain" round>{{ info.length }} {{ lang.dns_record_count }}</el-tag>
</div>
<el-table :data="info" border class="dns-table" table-layout="auto">
<el-table-column
prop="host"
:label="lang.dns_host"
width="190px"
align="center"
header-align="center"
>
<template #default="scope">
<el-tooltip :content="lang.dns_root_desc" placement="top" v-if="scope.row.host === '' || scope.row.host === '@'">
<span class="dns-host dns-host-root">{{ scope.row.host || '@' }}</span>
</el-tooltip>
<span v-else class="dns-host">{{ scope.row.host }}</span>
</template>
</el-table-column>
<el-table-column
prop="type"
:label="lang.dns_type"
width="110px"
align="center"
header-align="center"
>
<template #default="scope">
<span :class="['dns-type', 'dns-type-' + scope.row.type.toLowerCase()]">
{{ scope.row.type }}
</span>
</template>
</el-table-column>
<el-table-column
prop="value"
:label="lang.dns_value"
min-width="420px"
header-align="center"
>
<template #default="scope">
<div class="dns-value-cell">
<span v-if="scope.row.unavailableReason || !scope.row.value" class="dns-value dns-value-unavailable">
{{ scope.row.unavailableReason || '记录尚未就绪' }}
</span>
<el-tooltip :content="scope.row.tips" placement="top" v-else-if="scope.row.tips">
<span :class="['dns-value', { 'dns-value-long': scope.row.value.length > 80 }]">
{{ scope.row.value }}
</span>
</el-tooltip>
<span v-else :class="['dns-value', { 'dns-value-long': scope.row.value.length > 80 }]">
{{ scope.row.value }}
</span>
<el-button
class="dns-copy-button"
link
type="primary"
:disabled="dnsLoading || dnsInputPending || !!scope.row.unavailableReason || !scope.row.value"
@click.stop="copyDNSRecord(scope.row)"
>
{{ lang.dns_copy }}
</el-button>
</div>
</template>
</el-table-column>
<el-table-column
prop="ttl"
:label="lang.dns_ttl"
width="120px"
align="center"
header-align="center"
>
<template #default="scope">
<span class="dns-ttl">{{ formatDNSRecordTTL(scope.row.ttl) }}</span>
</template>
</el-table-column>
</el-table>
</section>
</div>
<el-empty v-else :description="lang.dns_empty" />
</div>
<div v-if="active === 5" class="ctn setup-form-panel">
<div class="desc">
<div class="ssl-title-row">
<h2>{{ lang.setSSL }}</h2>
<div v-if="!proxyHTTPS && sslSettings.type === '0' && sslSettings.challenge === 'http' && port !== 80"
class="ssl-port-warning" role="alert">
<strong>{{ lang.warning }}</strong>
<span>{{ lang.autoSSLWarn }}</span>
</div>
</div>
<div class="ssl-title-description">{{ lang.setSSL }}</div>
</div>
<div class="form ssl-form">
<p v-if="proxyHTTPS" class="ssl-manual-desc" role="note">
HTTPS 证书的申请和续期由反向代理负责,Smail 不再自动处理。SMTP、IMAP、POP3 仍需本地证书,请导入由外部维护的证书与私钥;外部续期后也需重新导入并应用。
</p>
<el-form label-position="top">
<el-form-item :label="lang.type">
<el-select class="ssl-type-select" :placeholder="proxyHTTPS ? lang.ssl_manuallyf : lang.ssl_auto" v-model="sslSettings.type"
:disabled="fullscreenLoading" @change="handleSSLTypeChange">
<el-option v-if="!proxyHTTPS" :label="lang.ssl_auto" value="0"/>
<el-option :label="lang.ssl_manuallyf" value="1"/>
</el-select>
</el-form-item>
<el-form-item :label="lang.ssl_challenge_type" v-if="!proxyHTTPS && sslSettings.type === '0'">
<div class="ssl-challenge-select-row">
<el-select class="ssl-type-select" :placeholder="lang.ssl_challenge_type" v-model="sslSettings.challenge"
:disabled="fullscreenLoading" @change="handleSSLChallengeChange">
<el-option :label="lang.ssl_auto_http" value="http"/>
<el-option :label="lang.ssl_auto_dns" value="dns"/>
</el-select>
<el-tooltip class="ssl-challenge-help" effect="dark" :content="lang.challenge_typ_desc"
placement="top-start">
<span class="ssl-help-mark">?</span>
</el-tooltip>
</div>
</el-form-item>
<div v-if="!proxyHTTPS && sslSettings.type === '0'" class="ssl-storage-card">
<div class="ssl-storage-title">{{ lang.ssl_auto_storage_title }}</div>
<div class="ssl-storage-desc">{{ lang.ssl_auto_storage_desc }}</div>
<div v-if="sslSettings.challenge === 'dns'" class="ssl-dns-intro">
{{ sslSettings.paramsList.length === 0 ? lang.ssl_dns_waiting : lang.ssl_dns_ready }}
</div>
<div class="ssl-storage-path">{{ lang.ssl_auto_certificate_path }}</div>
<div class="ssl-storage-path">{{ lang.ssl_auto_private_key_path }}</div>
<div class="ssl-storage-path">{{ lang.ssl_auto_account_key_path }}</div>
</div>
<el-form-item :label="lang.ssl_key_path" v-if="sslSettings.type === '1'">
<el-input placeholder="/etc/letsencrypt/live/example.com/privkey.pem" v-model="sslSettings.key_path"></el-input>
<div class="ssl-manual-desc">{{ lang.ssl_manual_key_desc }}</div>
</el-form-item>
<el-form-item :label="lang.ssl_crt_path" v-if="sslSettings.type === '1'">
<el-input placeholder="/etc/letsencrypt/live/example.com/fullchain.pem" v-model="sslSettings.crt_path"></el-input>
<div class="ssl-manual-desc">{{ lang.ssl_manual_crt_desc }}</div>
</el-form-item>
</el-form>
</div>
<div v-if="dnsChecking" class="ssl-dns-challenge">
<div class="ssl-dns-challenge-header">
<div>
<h3>{{ lang.ssl_dns_title }}</h3>
</div>
<span v-if="sslSettings.paramsList.length > 0" class="ssl-dns-record-count">
{{ sslSettings.paramsList.length }} 条记录
</span>
</div>
<el-alert
v-if="dnsChallengeNotice"
:title="dnsChallengeNotice"
type="warning"
show-icon
:closable="false"
class="ssl-dns-refresh-notice"
/>
<el-table v-if="sslSettings.paramsList.length > 0" :data="sslSettings.paramsList" border>
<el-table-column prop="host" label="主机名" width="180px"/>
<el-table-column prop="type" label="类型" width="100px"/>
<el-table-column prop="value" label="记录值" min-width="360px">
<template #default="scope">
<div class="ssl-dns-value-cell">
<div class="ssl-dns-value">{{ scope.row.value }}</div>
<el-button
class="ssl-dns-copy-button"
link
type="primary"
@click.stop="copyDNSValue(scope.row.value)"
>
{{ lang.dns_copy }}
</el-button>
</div>
</template>
</el-table-column>
<el-table-column prop="ttl" label="TTL" width="100px"/>
</el-table>
</div>
</div>
<div class="setup-actions">
<el-button v-if="active > 0" id="previous" @click="previous">
{{ lang.previous }}
</el-button>
<el-button :loading="databaseLoading || stepLoading" :disabled="databaseLoading || stepLoading || fullscreenLoading"
:element-loading-text="waitDesc"
v-loading.fullscreen.lock="fullscreenLoading" id="next"
@click="next">
{{ dnsChecking ? lang.ssl_verify : lang.next }}
</el-button>
</div>
</div>
</template>
<script setup>
import { usePage } from './Setup.js'
const {
active,
lang,
dbSettings,
databaseLoading,
stepLoading,
databaseError,
adminSettings,
domainSettings,
addDomain,
Plus,
dnsInfos,
hasDNSIPv6,
sslSettings,
dnsChecking,
port,
proxyHTTPS,
dnsChallengeNotice,
waitDesc,
next,
previous,
handleDbTypeChange,
handleSSLTypeChange,
handleSSLChallengeChange,
clearDatabaseError,
formatDNSRecordTTL,
copyDNSValue,
fullscreenLoading
} = usePage()
</script>
<style scoped src="./Setup.css"></style>