feat(ui): 添加友链申请表单中想说的话字段

在 `FriendsSection.vue` 中添加了新的表单字段“想说的话”,用户可以输入最多50字的信息。同时更新了相关的样式,以支持 `textarea` 的显示和交互,包括输入框的样式和字符计数的显示。

在 `send-mail.ts` 中添加了对新字段 `message` 的处理,确保其在邮件内容中正确显示,并在邮件的HTML部分增加了对 `message` 字段的引用。

在生成的邮件内容中增加了友链申请者的“想说的话”信息,方便接收者查看。
This commit is contained in:
2025-12-12 23:29:42 +08:00
parent 99ffc73e76
commit 55f4307b13
4 changed files with 61 additions and 19 deletions

View File

@@ -11,12 +11,12 @@ export default defineNuxtConfig({
link: [{ rel: "icon", href: siteConfig.siteMeta.icon }], link: [{ rel: "icon", href: siteConfig.siteMeta.icon }],
}, },
}, },
nitro: { // nitro: {
prerender: { // prerender: {
crawlLinks: true, // crawlLinks: true,
routes: ["/sitemap.xml", "/rss.xml"], // routes: ["/sitemap.xml", "/rss.xml"],
}, // },
}, // },
runtimeConfig: { runtimeConfig: {
smtpHost: process.env.SMTP_HOST ?? "", smtpHost: process.env.SMTP_HOST ?? "",
smtpPort: Number(process.env.SMTP_PORT ?? 465), smtpPort: Number(process.env.SMTP_PORT ?? 465),

View File

@@ -26,6 +26,13 @@
头像链接 头像链接
<input v-model="form.avatar" type="url" placeholder="可选,展示头像" /> <input v-model="form.avatar" type="url" placeholder="可选,展示头像" />
</label> </label>
<label>
想说的话
<div class="textarea-wrapper">
<textarea v-model="form.message" placeholder="可选最多50字" maxlength="50"></textarea>
<span class="char-count">{{ form.message?.length || 0 }}/50</span>
</div>
</label>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="primary" :disabled="loading"> <button type="submit" class="primary" :disabled="loading">
{{ loading ? "提交中..." : "提交申请" }} {{ loading ? "提交中..." : "提交申请" }}
@@ -75,6 +82,7 @@ const form = reactive({
desc: "", desc: "",
email: "", email: "",
avatar: "", avatar: "",
message: "",
}); });
const displayedFriends = ref([]); const displayedFriends = ref([]);
@@ -108,6 +116,7 @@ const submitForm = async () => {
desc: form.desc, desc: form.desc,
email: form.email, email: form.email,
avatar: form.avatar, avatar: form.avatar,
message: form.message,
}), }),
}); });
if (!resp.ok) throw new Error("send failed"); if (!resp.ok) throw new Error("send failed");
@@ -192,15 +201,41 @@ h2 {
color: inherit; color: inherit;
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
} }
.friend-form input::placeholder { .textarea-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
.friend-form textarea {
flex: 1;
padding: 8px 10px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.18);
background: rgba(255, 255, 255, 0.06);
color: inherit;
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
font-family: inherit;
height: 36px;
resize: none;
overflow: auto;
}
.friend-form input::placeholder,
.friend-form textarea::placeholder {
color: rgba(232, 238, 252, 0.7); color: rgba(232, 238, 252, 0.7);
} }
.friend-form input:focus { .friend-form input:focus,
.friend-form textarea:focus {
outline: none; outline: none;
border-color: rgba(124, 193, 255, 0.8); border-color: rgba(124, 193, 255, 0.8);
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
box-shadow: 0 0 0 2px rgba(124, 193, 255, 0.25); box-shadow: 0 0 0 2px rgba(124, 193, 255, 0.25);
} }
.char-count {
font-size: 12px;
color: rgba(232, 238, 252, 0.6);
white-space: nowrap;
flex-shrink: 0;
}
.form-actions { .form-actions {
grid-column: 1 / -1; grid-column: 1 / -1;
display: flex; display: flex;

View File

@@ -19,6 +19,7 @@ type SendMailPayload = {
desc?: string; desc?: string;
email?: string; email?: string;
avatar?: string; avatar?: string;
message?: string;
}; };
const ensureValue = (value?: string, fallback = "未填写") => (value?.trim() ? value.trim() : fallback); const ensureValue = (value?: string, fallback = "未填写") => (value?.trim() ? value.trim() : fallback);
@@ -35,7 +36,7 @@ export default defineEventHandler(async (event) => {
} }
const payload = (await readBody<SendMailPayload>(event)) || {}; const payload = (await readBody<SendMailPayload>(event)) || {};
const { name, url, desc, email, avatar } = payload; const { name, url, desc, email, avatar, message } = payload;
if (!name?.trim() || !url?.trim() || !email?.trim()) { if (!name?.trim() || !url?.trim() || !email?.trim()) {
throw createError({ throw createError({
@@ -45,15 +46,7 @@ export default defineEventHandler(async (event) => {
} }
const config = useRuntimeConfig() as MailConfig; const config = useRuntimeConfig() as MailConfig;
const { const { smtpHost, smtpPort: configSmtpPort, smtpUser, smtpPass, senderEmail, adminEmail, smtpSecure } = config;
smtpHost,
smtpPort: configSmtpPort,
smtpUser,
smtpPass,
senderEmail,
adminEmail,
smtpSecure,
} = config;
const smtpPort = Number(configSmtpPort ?? 465); const smtpPort = Number(configSmtpPort ?? 465);
if (!smtpHost || !smtpUser || !smtpPass || !senderEmail || !adminEmail) { if (!smtpHost || !smtpUser || !smtpPass || !senderEmail || !adminEmail) {
@@ -72,12 +65,26 @@ export default defineEventHandler(async (event) => {
}; };
const transporter = nodemailer.createTransport(smtpOptions); const transporter = nodemailer.createTransport(smtpOptions);
const friendEntry = `{
name: "${ensureValue(name).replace(/"/g, '\\"')}",
url: "${ensureValue(url).replace(/"/g, '\\"')}",
desc: "${ensureValue(desc).replace(/"/g, '\\"')}",
avatar: "${ensureValue(avatar).replace(/"/g, '\\"')}",
},`;
const htmlMessage = ` const htmlMessage = `
<p>一个新的友链申请已提交,以下是可直接复制到项目中的配置:</p>
<pre style="background: #f5f5f5; padding: 12px; border-radius: 4px; overflow: auto;">
<code>${friendEntry}</code>
</pre>
<hr style="margin: 20px 0;" />
<p><strong>申请者信息:</strong></p>
<p><strong>名称:</strong>${ensureValue(name)}</p> <p><strong>名称:</strong>${ensureValue(name)}</p>
<p><strong>邮箱:</strong>${ensureValue(email)}</p> <p><strong>邮箱:</strong>${ensureValue(email)}</p>
<p><strong>站点:</strong><a href="${ensureValue(url)}">${ensureValue(url)}</a></p> <p><strong>站点:</strong><a href="${ensureValue(url)}">${ensureValue(url)}</a></p>
<p><strong>描述:</strong>${ensureValue(desc)}</p> <p><strong>描述:</strong>${ensureValue(desc)}</p>
<p><strong>头像:</strong>${ensureValue(avatar)}</p> <p><strong>头像:</strong>${ensureValue(avatar)}</p>
<p><strong>想说的话:</strong>${ensureValue(message)}</p>
<p><strong>时间:</strong>${new Date().toISOString()}</p> <p><strong>时间:</strong>${new Date().toISOString()}</p>
`; `;

View File

@@ -1,6 +1,6 @@
{ {
"version": 2, "version": 2,
"framework": "vite", "buildCommand": "nuxt generate",
"routes": [ "routes": [
{ {
"src": "/api/(.*)", "src": "/api/(.*)",