feat(app): 添加多模板关于我们页面及相关路由和404页面
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引 - 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理 - 实现/article、/case、/product及/page动态路由兼容列表与详情展示 - 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置 - 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持 - 模板增强支持CMS单页内容加载及SEO信息动态设置 - 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user