Root-cause fix for mixed CRLF/LF in the repo (no .gitattributes previously). Adds '* text=auto eol=lf' and explicit binary rules so all future checkouts are LF. Renormalizes all tracked text files (no logical content change). Backup tags: image qihuaiflow:backup-lineend-20260725-121632, git backup-lineend-pre-20260725-121632.
380 lines
16 KiB
TypeScript
380 lines
16 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { adminAPI } from '@/api';
|
|
|
|
const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 16 }) => (
|
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
{children}
|
|
</svg>
|
|
);
|
|
|
|
const SiteSettings: React.FC = () => {
|
|
const [brand, setBrand] = useState<Record<string, string>>({
|
|
logo: '', siteName: '', announcement: '', icpNumber: '', contactPhone: '',
|
|
});
|
|
const [mail, setMail] = useState({
|
|
smtp_host: '', smtp_port: 587, username: '', password: '', from_address: '', use_tls: true,
|
|
});
|
|
const [savedMsg, setSavedMsg] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [testResult, setTestResult] = useState<string | null>(null);
|
|
const [showEmailTest, setShowEmailTest] = useState(false);
|
|
const [emailTestTo, setEmailTestTo] = useState('');
|
|
const [emailTestSubject, setEmailTestSubject] = useState('');
|
|
const [emailTestBody, setEmailTestBody] = useState('');
|
|
const [emailGenerating, setEmailGenerating] = useState(false);
|
|
|
|
// AI生成测试邮件内容
|
|
const handleAIGenerateContent = async () => {
|
|
setEmailGenerating(true);
|
|
try {
|
|
const res = await adminAPI.generateEmailContent('测试邮件');
|
|
const data = (res as any).data || res;
|
|
setEmailTestSubject(data.subject || '');
|
|
setEmailTestBody(data.body || '');
|
|
} catch (e) {
|
|
// 降级:使用客户端预设内容
|
|
const subjects = [
|
|
'【测试】AIFlow邮件服务配置验证',
|
|
'AIFlow平台 - 邮件投递测试',
|
|
'测试邮件:您的SMTP服务已就绪',
|
|
];
|
|
const bodies = [
|
|
'您好!\n\n这是一封来自 AIFlow 智能自动化平台的测试邮件。\n\n如果您收到此邮件,说明SMTP邮件服务已配置成功。\n\n此致\nAIFlow 平台团队',
|
|
'Hi,\n\nThis is a test email from AIFlow Platform.\n\nIf you received this, the SMTP service works fine.\n\nBest regards,\nAIFlow Team',
|
|
'【系统通知】\n邮件服务连通性测试通过。\n\n发件服务器配置正确。\n\n—— AIFlow',
|
|
];
|
|
setEmailTestSubject(subjects[Math.floor(Math.random() * subjects.length)]);
|
|
setEmailTestBody(bodies[Math.floor(Math.random() * bodies.length)]);
|
|
} finally {
|
|
setEmailGenerating(false);
|
|
}
|
|
};
|
|
|
|
// 加载真实配置
|
|
useEffect(() => {
|
|
async function loadConfig() {
|
|
try {
|
|
const [siteRes, emailRes, smsRes] = await Promise.all([
|
|
adminAPI.getSiteSettings(),
|
|
adminAPI.getEmailConfig(),
|
|
adminAPI.getSmsConfig(),
|
|
]);
|
|
if (siteRes?.data) {
|
|
setBrand({
|
|
logo: siteRes.data.logo || '',
|
|
siteName: siteRes.data.site_name || siteRes.data.siteName || '',
|
|
announcement: siteRes.data.announcement || '',
|
|
icpNumber: siteRes.data.icp_number || siteRes.data.icpNumber || '',
|
|
contactPhone: siteRes.data.contact_phone || siteRes.data.contactPhone || '',
|
|
});
|
|
}
|
|
if (emailRes?.data) {
|
|
setMail({
|
|
smtp_host: emailRes.data.smtp_host || '',
|
|
smtp_port: emailRes.data.smtp_port || 587,
|
|
username: emailRes.data.username || '',
|
|
password: emailRes.data.password || '',
|
|
from_address: emailRes.data.from_address || '',
|
|
use_tls: emailRes.data.use_tls !== false,
|
|
});
|
|
}
|
|
if (smsRes?.data) {
|
|
setSms({
|
|
provider: smsRes.data.provider || '',
|
|
api_key: smsRes.data.api_key || '',
|
|
api_secret: smsRes.data.api_secret || '',
|
|
sign_name: smsRes.data.sign_name || '',
|
|
template_code: smsRes.data.template_code || '',
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.error('加载配置失败:', e);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
loadConfig();
|
|
}, []);
|
|
|
|
const showSaved = (block: string) => {
|
|
setSavedMsg(block + ' 保存成功');
|
|
setTimeout(() => setSavedMsg(null), 2000);
|
|
};
|
|
|
|
const showTestResult = (result: string) => {
|
|
setTestResult(result);
|
|
setTimeout(() => setTestResult(null), 5000);
|
|
};
|
|
|
|
// 保存品牌设置
|
|
const saveSiteSettings = async () => {
|
|
try {
|
|
await adminAPI.updateSiteSettings({
|
|
logo: brand.logo,
|
|
site_name: brand.siteName,
|
|
announcement: brand.announcement,
|
|
icp_number: brand.icpNumber,
|
|
contact_phone: brand.contactPhone,
|
|
});
|
|
showSaved('品牌设置');
|
|
} catch (e: any) {
|
|
alert('保存失败: ' + (e.detail || e.message));
|
|
}
|
|
};
|
|
|
|
// 保存邮件配置
|
|
const saveEmailConfig = async () => {
|
|
try {
|
|
await adminAPI.updateEmailConfig({
|
|
smtp_host: mail.smtp_host,
|
|
smtp_port: mail.smtp_port,
|
|
username: mail.username,
|
|
password: mail.password,
|
|
from_address: mail.from_address,
|
|
use_tls: mail.use_tls,
|
|
});
|
|
showSaved('邮件配置');
|
|
} catch (e: any) {
|
|
alert('保存失败: ' + (e.detail || e.message));
|
|
}
|
|
};
|
|
|
|
// 保存短信配置
|
|
// 发送测试邮件 - 先保存配置再发送
|
|
const sendTestEmail = async () => {
|
|
if (!emailTestTo.trim()) {
|
|
showTestResult('请输入收件邮箱地址');
|
|
return;
|
|
}
|
|
try {
|
|
// 测试前先保存邮件配置到数据库,确保使用最新配置
|
|
await adminAPI.updateEmailConfig({
|
|
smtp_host: mail.smtp_host,
|
|
smtp_port: mail.smtp_port,
|
|
username: mail.username,
|
|
password: mail.password,
|
|
from_address: mail.from_address,
|
|
use_tls: mail.use_tls,
|
|
});
|
|
const res = await adminAPI.testEmail({
|
|
test_to: emailTestTo.trim(),
|
|
test_subject: emailTestSubject || undefined,
|
|
test_body: emailTestBody || undefined,
|
|
});
|
|
showTestResult('测试邮件已发送至 ' + emailTestTo + ': ' + ((res as any)?.message || 'OK'));
|
|
setShowEmailTest(false);
|
|
} catch (e: any) {
|
|
showTestResult('测试邮件发送失败: ' + (e.detail || e.message));
|
|
}
|
|
};
|
|
|
|
const openEmailTest = () => {
|
|
setEmailTestTo('');
|
|
setEmailTestSubject('');
|
|
setEmailTestBody('');
|
|
setShowEmailTest(true);
|
|
};
|
|
|
|
if (loading) {
|
|
return <div style={{ padding: 40, textAlign: 'center' }}>加载中...</div>;
|
|
}
|
|
|
|
return (
|
|
<div style={{ animation: 'fadeIn var(--transition-normal)' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 'var(--space-2xl)' }}>
|
|
<h1 className="admin-page-title" style={{ marginBottom: 0 }}>网站基础设置</h1>
|
|
{savedMsg && (
|
|
<span className="admin-badge badge-success" style={{ padding: '6px 14px', fontSize: 'var(--text-sm)' }}>
|
|
<SVGIcon><polyline points="20 6 9 17 4 12"/></SVGIcon>
|
|
{' '}{savedMsg}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Brand Settings */}
|
|
<div className="admin-card">
|
|
<div className="admin-card-header">
|
|
<h3 className="admin-card-title">
|
|
<SVGIcon size={18}><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></SVGIcon>
|
|
{' '}品牌设置
|
|
</h3>
|
|
</div>
|
|
|
|
<div className="admin-form-row">
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">LOGO URL</label>
|
|
<input className="admin-form-input" value={brand.logo} onChange={e => setBrand({ ...brand, logo: e.target.value })} />
|
|
</div>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">网站名称</label>
|
|
<input className="admin-form-input" value={brand.siteName} onChange={e => setBrand({ ...brand, siteName: e.target.value })} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">首页公告</label>
|
|
<textarea
|
|
className="admin-form-input admin-form-textarea"
|
|
value={brand.announcement}
|
|
onChange={e => setBrand({ ...brand, announcement: e.target.value })}
|
|
style={{ minHeight: 60 }}
|
|
/>
|
|
</div>
|
|
|
|
<div className="admin-form-row">
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">备案号</label>
|
|
<input className="admin-form-input" value={brand.icpNumber} onChange={e => setBrand({ ...brand, icpNumber: e.target.value })} />
|
|
</div>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">客服联系方式</label>
|
|
<input className="admin-form-input" value={brand.contactPhone} onChange={e => setBrand({ ...brand, contactPhone: e.target.value })} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="admin-form-actions">
|
|
<button className="admin-btn admin-btn-primary" onClick={saveSiteSettings}>
|
|
<SVGIcon><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></SVGIcon>
|
|
保存设置
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mail Config */}
|
|
<div className="admin-card">
|
|
<div className="admin-card-header">
|
|
<h3 className="admin-card-title">
|
|
<SVGIcon size={18}><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></SVGIcon>
|
|
{' '}邮件配置
|
|
</h3>
|
|
</div>
|
|
|
|
<div className="admin-form-row">
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">SMTP服务器</label>
|
|
<input className="admin-form-input" value={mail.smtp_host} onChange={e => setMail({ ...mail, smtp_host: e.target.value })} />
|
|
</div>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">端口</label>
|
|
<input className="admin-form-input" type="number" value={mail.smtp_port} onChange={e => setMail({ ...mail, smtp_port: Number(e.target.value) })} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="admin-form-row">
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">用户名</label>
|
|
<input className="admin-form-input" value={mail.username} onChange={e => setMail({ ...mail, username: e.target.value })} />
|
|
</div>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">密码</label>
|
|
<input className="admin-form-input" type="password" value={mail.password} onChange={e => setMail({ ...mail, password: e.target.value })} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="admin-form-group" style={{ maxWidth: '50%' }}>
|
|
<label className="admin-form-label">发件地址</label>
|
|
<input className="admin-form-input" value={mail.from_address} onChange={e => setMail({ ...mail, from_address: e.target.value })} />
|
|
</div>
|
|
|
|
<div className="admin-form-actions">
|
|
<button className="admin-btn admin-btn-primary" onClick={saveEmailConfig}>
|
|
<SVGIcon><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></SVGIcon>
|
|
保存设置
|
|
</button>
|
|
<button className="admin-btn admin-btn-outline" onClick={openEmailTest}>
|
|
<SVGIcon><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></SVGIcon>
|
|
发送测试邮件
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SMS Config */}
|
|
{/* Email Test Modal */}
|
|
{showEmailTest && (
|
|
<div style={{
|
|
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
|
|
background: 'rgba(0,0,0,0.4)', zIndex: 'var(--z-modal)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
}} onClick={() => setShowEmailTest(false)}>
|
|
<div style={{
|
|
background: '#fff', borderRadius: 'var(--radius-2xl)', width: 520, maxHeight: '90vh', overflow: 'auto',
|
|
padding: 'var(--space-3xl)', boxShadow: 'var(--shadow-xl)',
|
|
}} onClick={e => e.stopPropagation()}>
|
|
<h3 style={{ fontSize: 'var(--text-lg)', fontWeight: 700, marginBottom: 'var(--space-2xl)' }}>
|
|
<SVGIcon size={20}><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></SVGIcon>
|
|
{' '}发送测试邮件
|
|
</h3>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">收件邮箱 <span style={{ color: 'var(--color-error)' }}>*</span></label>
|
|
<input
|
|
className="admin-form-input"
|
|
type="email"
|
|
value={emailTestTo}
|
|
onChange={e => setEmailTestTo(e.target.value)}
|
|
placeholder="例如: admin@example.com"
|
|
/>
|
|
</div>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">邮件主题</label>
|
|
<input
|
|
className="admin-form-input"
|
|
value={emailTestSubject}
|
|
onChange={e => setEmailTestSubject(e.target.value)}
|
|
placeholder="测试邮件标题"
|
|
/>
|
|
</div>
|
|
<div className="admin-form-group">
|
|
<label className="admin-form-label">邮件内容</label>
|
|
<textarea
|
|
className="admin-form-input admin-form-textarea"
|
|
value={emailTestBody}
|
|
onChange={e => setEmailTestBody(e.target.value)}
|
|
style={{ minHeight: 120 }}
|
|
placeholder="测试邮件内容..."
|
|
/>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 'var(--space-sm)', justifyContent: 'space-between', marginBottom: 'var(--space-lg)' }}>
|
|
<button
|
|
className="admin-btn admin-btn-outline admin-btn-sm"
|
|
onClick={handleAIGenerateContent}
|
|
disabled={emailGenerating}
|
|
style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}
|
|
>
|
|
<SVGIcon size={12}><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></SVGIcon>
|
|
{emailGenerating ? '生成中...' : 'AI生成测试内容'}
|
|
</button>
|
|
</div>
|
|
<div className="admin-form-actions">
|
|
<button
|
|
className="admin-btn admin-btn-primary"
|
|
onClick={sendTestEmail}
|
|
disabled={!emailTestTo.trim()}
|
|
>
|
|
<SVGIcon><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></SVGIcon>
|
|
发送测试邮件
|
|
</button>
|
|
<button className="admin-btn admin-btn-outline" onClick={() => setShowEmailTest(false)}>
|
|
取消
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Test Result Toast */}
|
|
{testResult && (
|
|
<div style={{
|
|
position: 'fixed', bottom: 24, right: 24, padding: '12px 20px',
|
|
background: testResult.includes('失败') ? 'var(--color-error)' : 'var(--color-success)',
|
|
color: '#fff', borderRadius: 8, boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
zIndex: 9999, fontSize: 'var(--text-sm)',
|
|
}}>
|
|
{testResult}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SiteSettings;
|