Files
gxwebsoft 2b69686795 feat(app): 添加多模板关于我们页面及相关路由和404页面
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引
- 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理
- 实现/article、/case、/product及/page动态路由兼容列表与详情展示
- 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置
- 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持
- 模板增强支持CMS单页内容加载及SEO信息动态设置
- 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
2026-09-08 12:13:44 +08:00

202 lines
6.2 KiB
Vue
Raw Permalink 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>
<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>