feat(SendPage): 重构评估表单页面并增加多表单类型支持

重构发送页面UI,增加背景装饰和卡片式布局。新增支持发送两种不同类型的评估表单(家庭教育档案和入营测评卷),并优化了倒计时和状态管理逻辑。同时改进了企微消息发送功能,根据不同类型显示不同标题和描述。
This commit is contained in:
2026-03-04 19:32:27 +08:00
parent 9ea35dc399
commit c1618d9050

View File

@@ -1,29 +1,60 @@
<template> <template>
<div class="page-wrapper">
<!-- 背景装饰圆点 -->
<div class="bg-dot dot-1"></div>
<div class="bg-dot dot-2"></div>
<div class="container"> <div class="container">
<div class="header"> <!-- 头部区域 -->
<h1>🌟 青少年心理健康评估</h1> <div class="header-section">
<div class="sparkle-icon"></div>
<h1 class="main-title">青少年心理健康评估</h1>
<p class="subtitle">专业科学个性化的心理健康分析</p> <p class="subtitle">专业科学个性化的心理健康分析</p>
</div> </div>
<div class="cta-section"> <!-- 主体操作卡片 -->
<h2>开始您的专业评估之旅</h2> <div class="action-card">
<p class="cta-description"> <h2 class="card-headline">选择并发送评估工具</h2>
只需5-10分钟即可获得专业的家庭教育档案信息报告
</p>
<!-- 修改部分按钮增加了 disabled 属性动态 class 和动态文本 --> <div class="info-bar">
<button 只需 <span class="time-badge">5-10 分钟</span>即可获得专业的评估分析报告
class="cta-button" </div>
:class="{ 'disabled': isCoolingDown }"
@click="handleSendForm" <div class="button-list">
:disabled="isCoolingDown" <!-- 按钮 1: 家庭教育档案 -->
> <button class="nav-button" :class="{ 'is-active': activeType === 'archive', 'is-disabled': isCoolingDown }"
{{ isCoolingDown ? `📝 请等待 ${countdown} 秒...` : '📝 发送评估表' }} @click="handleSend('archive')">
<div class="btn-left">
<span class="btn-icon-box">📝</span>
<span class="btn-label">发送家庭教育档案</span>
</div>
<div class="btn-loading-ring" v-if="isCoolingDown && activeType === 'archive'"></div>
<span class="btn-arrow" v-else></span>
</button> </button>
<p class="note"> <!-- 按钮 2: 入营测评卷 -->
💡 评估完全免费结果仅供参考如需专业帮助请咨询心理医生 <button class="nav-button" :class="{ 'is-active': activeType === 'campTest', 'is-disabled': isCoolingDown }"
</p> @click="handleSend('campTest')">
<div class="btn-left">
<span class="btn-icon-box">🎯</span>
<span class="btn-label">发送入营测评卷</span>
</div>
<div class="btn-loading-ring" v-if="isCoolingDown && activeType === 'campTest'"></div>
<span class="btn-arrow" v-else></span>
</button>
</div>
<!-- 倒计时提示 -->
<div v-if="isCoolingDown" class="cooldown-text">
已发送请在聊天窗口查看 ({{ countdown }}s)
</div>
<!-- 底部免责声明 -->
<div class="disclaimer">
<span class="light-bulb">💡</span>
评估完全免费结果仅供参考如需专业帮助请咨询心理医生
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -32,303 +63,305 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import * as ww from '@wecom/jssdk' import * as ww from '@wecom/jssdk'
// --- 状态定义 ---
type FormType = 'archive' | 'campTest'
const isCoolingDown = ref(false)
const countdown = ref(0)
const activeType = ref<FormType | null>(null)
const isWWReady = ref(false) const isWWReady = ref(false)
const errorMessage = ref<string | null>(null)
// --- 防重复点击逻辑 --- // --- 配置表 ---
const isCoolingDown = ref(false) // 是否处于冷却期 const CONFIG = {
const countdown = ref(0) // 倒计时秒数 archive: {
id: 0,
const handleSendForm = () => { title: '家庭教育档案信息表',
// 如果正在冷却中,直接阻断 desc: '通过专业的评估工具,了解孩子的成长需求。'
if (isCoolingDown.value) return },
campTest: {
// 启动10秒倒计时 id: 2,
startCooldown(10) title: '青少年入营测评卷',
desc: '入营前专属专业测评,帮助导师全方位了解营员特质。'
// 执行业务逻辑 }
GetFormUrl()
} }
// 倒计时工具函数 // --- 核心逻辑 ---
const startCooldown = (seconds: number) => { const handleSend = async (type: FormType) => {
if (isCoolingDown.value) return
activeType.value = type
startTimer(8) // 8秒冷却
try {
// 1. 获取外部联系人ID
const contact = await ww.getCurExternalContact()
const userId = contact.userId
// 2. 请求后端获取表单URL
const apiUrl = `https://liaison.nycjy.cn/api/v1/archive/form-url?user_id=${userId}&form_id=${CONFIG[type].id}`
const res = await fetch(apiUrl).then(r => r.json())
if (res.code === 200 && res.data?.form_url) {
// 3. 调用企微原生接口发送消息卡片
await ww.sendChatMessage({
msgtype: 'news',
news: {
title: CONFIG[type].title,
desc: CONFIG[type].desc,
imgUrl: 'https://forms.nycjy.cn/favicon.ico',
link: res.data.form_url
}
})
} else {
alert('获取链接失败:' + (res.message || '未知错误'))
}
} catch (err) {
console.error('发送流程出错:', err)
}
}
const startTimer = (seconds: number) => {
isCoolingDown.value = true isCoolingDown.value = true
countdown.value = seconds countdown.value = seconds
const timer = setInterval(() => { const timer = setInterval(() => {
countdown.value-- countdown.value--
if (countdown.value <= 0) { if (countdown.value <= 0) {
clearInterval(timer) clearInterval(timer)
isCoolingDown.value = false // 倒计时结束,恢复按钮可用 isCoolingDown.value = false
activeType.value = null
} }
}, 1000) }, 1000)
} }
// --------------------
async function getConfigSignature(url: string) { // --- 初始化企业微信SDK ---
const initSDK = async () => {
try { try {
const response = await fetch('https://sidebar.wx.nycjy.cn/api/v1/wecom/config-signature', { const url = window.location.href.split('#')[0]
// 签名函数 (对应你后端接口)
const getSig = async (apiUrl: string) => {
return await fetch(apiUrl, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url }) body: JSON.stringify({ url })
}) }).then(r => r.json())
if (!response.ok) throw new Error('获取企业签名失败')
return await response.json()
} catch (error) {
console.error('[getConfigSignature]', error)
throw error
} }
}
async function getAgentConfigSignature(url: string) {
try {
const response = await fetch('https://sidebar.wx.nycjy.cn/api/v1/wecom/agent-config-signature', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
})
if (!response.ok) throw new Error('获取应用签名失败')
return await response.json()
} catch (error) {
console.error('[getAgentConfigSignature]', error)
throw error
}
}
const initWeWork = async () => {
try {
await ww.register({ await ww.register({
corpId: 'wwf72acc5a681dca93', corpId: 'wwf72acc5a681dca93',
agentId: 1000105, agentId: 1000105,
jsApiList: ['sendChatMessage', 'getCurExternalContact', 'getContext'], jsApiList: ['sendChatMessage', 'getCurExternalContact', 'getContext'],
getConfigSignature, getConfigSignature: () => getSig('https://sidebar.wx.nycjy.cn/api/v1/wecom/config-signature'),
getAgentConfigSignature getAgentConfigSignature: () => getSig('https://sidebar.wx.nycjy.cn/api/v1/wecom/agent-config-signature')
}) })
isWWReady.value = true isWWReady.value = true
} catch (error) { console.log('企微SDK初始化成功')
isWWReady.value = false } catch (e) {
errorMessage.value = '企业微信JS-SDK初始化失败请刷新页面重试。' console.error('SDK初始化失败', e)
} }
} }
const getUserInfo = async () => { onMounted(() => initSDK())
try {
const response = await ww.getCurExternalContact()
return response.userId
} catch (error) {
console.error('[getCurExternalContact] 失败', error)
throw error
}
}
const GetFormUrl = async () => {
try {
const userId = await getUserInfo()
const response = await fetch(`https://liaison.nycjy.cn/api/v1/archive/form-url?user_id=${userId}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
})
const formUrl = await response.json()
console.log('[GetFormUrl]', formUrl.data)
sendFormLink(formUrl.data.form_url)
if (!response.ok) throw new Error('获取表单URL失败')
} catch (error) {
console.error('[GetFormUrl]', error)
// 如果希望出错时允许立即重试,可以在这里取消冷却:
// isCoolingDown.value = false
alert('请求失败,请检查网络或稍后重试。')
}
}
const sendFormLink = async (formUrl: string) => {
if (!isWWReady.value) {
alert('企业微信功能尚未准备好,请稍等片刻...')
return
}
try {
await ww.sendChatMessage({
msgtype: 'news',
news: {
title: '家庭教育档案信息表',
desc: '通过专业的评估工具,了解孩子的成长需求,为每个孩子制定个性化的成长方案',
imgUrl: 'https://forms.nycjy.cn/favicon.ico',
link: formUrl
}
})
} catch (error) {
console.error('发送消息失败:', error)
alert('发送失败,详情请查看控制台日志。')
}
}
onMounted(async () => {
await initWeWork()
})
</script> </script>
<style scoped> <style scoped>
/* 全局基础设置 */
.page-wrapper {
min-height: 100vh;
background-color: #f4f7ff;
/* 淡蓝色背景 */
background: linear-gradient(180deg, #f0f4ff 0%, #ffffff 100%);
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
position: relative;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
}
.container { .container {
max-width: 1200px; width: 100%;
margin: 0 auto; max-width: 420px;
padding: 2rem; z-index: 10;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
} }
.header { /* 装饰元素 */
.bg-dot {
position: absolute;
border-radius: 50%;
filter: blur(60px);
z-index: 1;
}
.dot-1 {
width: 200px;
height: 200px;
background: #dce4ff;
top: -50px;
left: -50px;
}
.dot-2 {
width: 250px;
height: 250px;
background: #eef2ff;
bottom: -50px;
right: -50px;
}
/* 头部样式 */
.header-section {
text-align: center; text-align: center;
margin-bottom: 3rem; margin-bottom: 30px;
} }
.header h1 { .sparkle-icon {
font-size: 2.5rem; font-size: 44px;
font-weight: 700; margin-bottom: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
-webkit-background-clip: text;
-webkit-text-fill-color: transparent; .main-title {
background-clip: text; color: #5c6be5;
margin-bottom: 1rem; /* 核心蓝紫色 */
font-size: 26px;
font-weight: 800;
margin: 0 0 10px 0;
letter-spacing: 1px;
} }
.subtitle { .subtitle {
font-size: 1.2rem; color: #8c96a8;
color: #666; font-size: 15px;
margin: 0; margin: 0;
} }
.card-grid { /* 主操作卡片 */
display: grid; .action-card {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); background: #ffffff;
gap: 2rem; border-radius: 24px;
margin-bottom: 4rem; padding: 35px 24px;
} box-shadow: 0 10px 30px rgba(92, 107, 229, 0.08);
.card {
background: white;
border-radius: 16px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
text-align: center; text-align: center;
} }
.card:hover { .card-headline {
transform: translateY(-8px); color: #333c4d;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.15); font-size: 22px;
}
.card.highlight {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.card-icon {
font-size: 3rem;
margin-bottom: 1rem;
display: block;
}
.card h3 {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 1rem;
color: inherit;
}
.card p {
color: inherit;
opacity: 0.9;
line-height: 1.6;
margin: 0;
}
.cta-section {
text-align: center;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 20px;
padding: 3rem 2rem;
margin-top: 2rem;
}
.cta-section h2 {
font-size: 2rem;
font-weight: 700; font-weight: 700;
color: #2c3e50; margin: 0 0 15px 0;
margin-bottom: 1rem;
} }
.cta-description { /* 时间标签栏 */
font-size: 1.1rem; .info-bar {
color: #5a6c7d; font-size: 14px;
margin-bottom: 2rem; color: #64748b;
max-width: 600px; margin-bottom: 35px;
margin-left: auto;
margin-right: auto;
} }
.cta-button { .time-badge {
background: linear-gradient(135deg, #4CAF50, #45a049); background: #edf0ff;
color: white; /* 淡紫色背景 */
border: none; color: #5c6be5;
padding: 1rem 2.5rem; padding: 3px 10px;
font-size: 1.2rem; border-radius: 6px;
font-weight: 600; font-weight: 600;
border-radius: 50px; margin: 0 4px;
}
/* 按钮列表 */
.button-list {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 25px;
}
.nav-button {
background: #ffffff;
border: 1px solid #eef0f5;
border-radius: 16px;
padding: 18px 20px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.2s ease;
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3); width: 100%;
margin-bottom: 1.5rem; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02);
} }
.cta-button:hover { .nav-button:active:not(.is-disabled) {
background: linear-gradient(135deg, #45a049, #3d8b40); transform: scale(0.97);
transform: translateY(-3px); background-color: #f8faff;
box-shadow: 0 8px 25px rgba(76, 175, 80, 0.4);
} }
/* 禁用状态样式 */ .btn-left {
.cta-button.disabled { display: flex;
background: #bdc3c7; /* 灰色背景 */ align-items: center;
gap: 14px;
}
.btn-icon-box {
font-size: 20px;
}
.btn-label {
color: #5c6be5;
font-size: 16px;
font-weight: 600;
}
.btn-arrow {
color: #c0c7d1;
font-size: 18px;
font-weight: bold;
}
/* 冷却状态样式 */
.is-disabled {
opacity: 0.6;
cursor: not-allowed; cursor: not-allowed;
transform: none; /* 移除悬浮效果 */ background-color: #f5f7fa;
box-shadow: none;
opacity: 0.8;
} }
.cta-button.disabled:hover { .cooldown-text {
background: #bdc3c7; font-size: 13px;
transform: none; color: #5c6be5;
margin-bottom: 20px;
font-weight: 500;
} }
.note { .btn-loading-ring {
font-size: 0.9rem; width: 18px;
color: #7f8c8d; height: 18px;
margin: 0; border: 2px solid #5c6be5;
font-style: italic; border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
} }
/* 响应式设计 */ @keyframes spin {
@media (max-width: 768px) { to {
.container { transform: rotate(360deg);
padding: 1rem;
} }
}
.card-grid { /* 底部声明 */
grid-template-columns: 1fr; .disclaimer {
gap: 1rem; background: #f8f9fc;
} border-radius: 12px;
padding: 12px 16px;
font-size: 12px;
color: #94a3b8;
line-height: 1.6;
display: flex;
align-items: flex-start;
gap: 8px;
text-align: left;
}
.card { .light-bulb {
padding: 1.5rem; font-size: 16px;
} filter: grayscale(0.2);
.header h1 {
font-size: 2rem;
}
.cta-section {
padding: 2rem 1rem;
}
} }
</style> </style>