Smail/frontend/src/views/Setup/Setup.vue
smail a78f369e23
Some checks are pending
Security Checks / security (push) Waiting to run
补齐初始化公网HTTPS端口配置并修正反代入口跳转
2026-09-24 23:11:47 +08:00

447 lines
17 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 v-if="Object.keys(dnsInfos).length > 0" class="dns-domain-list">
<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">
<el-tooltip :content="scope.row.tips" placement="top" v-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" @click.stop="copyDNSValue(scope.row.value)">
{{ 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>