feat(app): 添加多模板关于我们页面及相关路由和404页面

- 新增404页面,优化未找到页面体验,避免被搜索引擎索引
- 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理
- 实现/article、/case、/product及/page动态路由兼容列表与详情展示
- 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置
- 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持
- 模板增强支持CMS单页内容加载及SEO信息动态设置
- 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
This commit is contained in:
2026-09-08 12:13:44 +08:00
commit 2b69686795
381 changed files with 59891 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
<template>
<form class="space-y-5" novalidate @submit.prevent="handleSubmit">
<div>
<label :class="labelClass">姓名</label>
<input
v-model.trim="form.name"
type="text"
required
:class="inputClass"
placeholder="请输入您的姓名"
>
</div>
<div>
<label :class="labelClass">电话</label>
<input
v-model.trim="form.phone"
type="tel"
required
:class="inputClass"
placeholder="请输入联系电话"
>
<p v-if="phoneError" class="mt-1 text-xs text-red-500">{{ phoneError }}</p>
</div>
<div>
<label :class="labelClass">{{ contentLabel }}</label>
<textarea
v-model.trim="form.content"
rows="4"
:required="contentRequired"
:class="inputClass"
:placeholder="type === 'consult' ? '请简要描述您的需求(选填)' : '请输入您想咨询的内容'"
/>
</div>
<!-- 蜜罐对真人隐藏机器人常自动填写 -->
<div aria-hidden="true" style="position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden;opacity:0;">
<label>请勿填写此字段</label>
<input v-model="hp" type="text" tabindex="-1" autocomplete="off">
</div>
<!-- 滑块验证码仅当后台网站设置开启 requireCaptcha 时显示仅客户端渲染 -->
<ClientOnly v-if="showCaptcha">
<SliderCaptcha ref="sliderRef" :accent="accent" @verified="onVerified" />
<template #fallback>
<div class="h-[46px] animate-pulse rounded-lg bg-gray-100" />
</template>
</ClientOnly>
<button type="submit" :disabled="submitting" :class="submitClass">
{{ submitting ? '提交中...' : submitText }}
</button>
<p v-if="submitMessage" class="text-center text-sm" :class="submitSuccess ? 'text-green-600' : 'text-red-600'">
{{ submitMessage }}
</p>
</form>
</template>
<script setup lang="ts">
/**
* 共享「在线留言」表单(9 套模板统一复用)。
* 内置:手机号段校验(港澳台/海外)、蜜罐、滑块验证码、提交。
* 校验/防护逻辑集中于此,改一处即可全站生效。
*/
const props = withDefaults(
defineProps<{
inputClass?: string
submitClass?: string
labelClass?: string
accent?: string
/** 表单类型:contact(默认)或 consult */
type?: 'contact' | 'consult'
/** 内容/需求字段标签 */
contentLabel?: string
/** 提交按钮文案 */
submitText?: string
/** 内容/需求字段是否必填(默认 true) */
contentRequired?: boolean
/** 预填内容(如咨询弹窗带入的产品名/需求) */
presetContent?: string
/** 来源标记 */
source?: string
}>(),
{
inputClass: 'w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:border-blue-500',
submitClass:
'w-full px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed',
labelClass: 'block text-sm font-medium text-gray-700 mb-1',
accent: '#1a6dff',
type: 'contact',
contentLabel: '留言内容',
submitText: '提交留言',
contentRequired: true,
presetContent: '',
source: ''
}
)
const emit = defineEmits<{
success: []
error: [message: string]
}>()
const { submitForm } = useCms()
const sliderRef = ref<InstanceType<typeof SliderCaptcha> | null>(null)
const { siteSetting } = useSite()
const runtimeConfig = useRuntimeConfig()
const form = reactive({ name: '', phone: '', content: props.presetContent })
watch(
() => props.presetContent,
(v) => {
if (v && !form.content) form.content = v
}
)
// 弹窗场景:每次挂载(弹窗打开)时用最新预填内容重置
onMounted(() => {
if (props.presetContent) form.content = props.presetContent
})
const hp = ref('')
const submitting = ref(false)
const submitSuccess = ref(false)
const submitMessage = ref('')
const phoneError = ref('')
const captchaTicket = ref('')
/**
* 是否显示滑块验证码。
* 优先级:后台「网站设置」setting.requireCaptcha(权威)> 运行时配置 public.contactCaptcha(部署级覆盖)> 默认 false。
* 与后端 submit 的判定口径一致(见 server/utils/site.ts getContactCaptchaRequired)。
*/
const showCaptcha = computed<boolean>(() => {
const fromSetting = siteSetting.value?.requireCaptcha
if (typeof fromSetting === 'boolean') return fromSetting
const fromEnv = runtimeConfig.public.contactCaptcha
if (typeof fromEnv === 'boolean') return fromEnv
return false
})
function onVerified(ticket: string) {
captchaTicket.value = ticket
}
async function handleSubmit() {
phoneError.value = ''
submitMessage.value = ''
if (!form.name || !form.phone || (props.contentRequired && !form.content)) {
submitSuccess.value = false
submitMessage.value = props.type === 'consult'
? '请填写姓名和电话'
: '请填写完整的姓名、电话和留言内容'
return
}
if (!validatePhone(form.phone)) {
phoneError.value = '请输入有效的联系电话(支持 +区号 海外/港澳台,如 +852 61234567'
return
}
if (showCaptcha.value && !captchaTicket.value) {
submitSuccess.value = false
submitMessage.value = '请先完成滑块验证'
return
}
submitting.value = true
try {
await submitForm({
type: props.type,
name: form.name,
phone: form.phone,
content: form.content,
captchaTicket: captchaTicket.value,
website: hp.value,
source: props.source || undefined
})
submitSuccess.value = true
submitMessage.value = '提交成功,我们会尽快与您联系!'
form.name = ''
form.phone = ''
form.content = ''
captchaTicket.value = ''
sliderRef.value?.reset()
emit('success')
} catch (err: any) {
submitSuccess.value = false
submitMessage.value = err?.data?.statusMessage || err?.statusMessage || '提交失败,请稍后重试'
// 票据一次性消费,失败后需重新验证
captchaTicket.value = ''
sliderRef.value?.reset()
emit('error', submitMessage.value)
} finally {
submitting.value = false
}
}
</script>