hookehuyr

chore: 将品牌名称从“美乐爱觉”更新为“生命力教育联盟”

更新所有页面、组件和文档中的品牌名称,包括用户协议、页面标题、分享标题、海报文案等,以反映品牌名称变更
......@@ -12,35 +12,49 @@
position="bottom"
:style="{ height: '100%' }"
>
<div class="flex flex-col h-full relative">
<div class="sticky top-0 flex justify-between items-center p-4 border-b border-gray-100 bg-white z-10">
<h3 class="font-medium text-lg">{{ title }}</h3>
<div class="relative flex h-full flex-col">
<div
class="sticky top-0 z-10 flex items-center justify-between border-b border-gray-100 bg-white p-4"
>
<h3 class="text-lg font-medium">{{ title }}</h3>
<van-icon name="cross" @click="$emit('update:show', false)" class="text-gray-500" />
</div>
<div class="flex-1 overflow-y-auto p-4 pt-0">
<div v-if="type === 'terms'" class="space-y-4 text-gray-600">
<p>欢迎使用美乐爱觉教育!在使用我们的服务之前,请仔细阅读以下用户协议。</p>
<p>欢迎使用生命力教育联盟教育!在使用我们的服务之前,请仔细阅读以下用户协议。</p>
<h5 class="font-medium text-gray-800">1. 服务内容</h5>
<p>美乐爱觉教育为用户提供在线教育、活动报名等服务。我们保留随时修改或中断服务的权利,而无需事先通知用户。</p>
<p>
生命力教育联盟教育为用户提供在线教育、活动报名等服务。我们保留随时修改或中断服务的权利,而无需事先通知用户。
</p>
<h5 class="font-medium text-gray-800">2. 用户责任</h5>
<p>用户在使用本服务时必须遵守所有适用的法律法规。用户承诺提供真实、准确、完整的个人信息。</p>
<p>
用户在使用本服务时必须遵守所有适用的法律法规。用户承诺提供真实、准确、完整的个人信息。
</p>
<h5 class="font-medium text-gray-800">3. 知识产权</h5>
<p>本平台的所有内容,包括但不限于文字、图片、音频、视频等,均受著作权法和其他知识产权法律法规的保护。</p>
<p>
本平台的所有内容,包括但不限于文字、图片、音频、视频等,均受著作权法和其他知识产权法律法规的保护。
</p>
<h5 class="font-medium text-gray-800">4. 免责声明</h5>
<p>对于因不可抗力或非本平台原因造成的服务中断或其他缺陷,本平台不承担任何责任。</p>
<h5 class="font-medium text-gray-800">1. 服务内容</h5>
<p>美乐爱觉教育为用户提供在线教育、活动报名等服务。我们保留随时修改或中断服务的权利,而无需事先通知用户。</p>
<p>
生命力教育联盟教育为用户提供在线教育、活动报名等服务。我们保留随时修改或中断服务的权利,而无需事先通知用户。
</p>
<h5 class="font-medium text-gray-800">2. 用户责任</h5>
<p>用户在使用本服务时必须遵守所有适用的法律法规。用户承诺提供真实、准确、完整的个人信息。</p>
<p>
用户在使用本服务时必须遵守所有适用的法律法规。用户承诺提供真实、准确、完整的个人信息。
</p>
<h5 class="font-medium text-gray-800">3. 知识产权</h5>
<p>本平台的所有内容,包括但不限于文字、图片、音频、视频等,均受著作权法和其他知识产权法律法规的保护。</p>
<p>
本平台的所有内容,包括但不限于文字、图片、音频、视频等,均受著作权法和其他知识产权法律法规的保护。
</p>
<h5 class="font-medium text-gray-800">4. 免责声明</h5>
<p>对于因不可抗力或非本平台原因造成的服务中断或其他缺陷,本平台不承担任何责任。</p>
......@@ -50,10 +64,14 @@
<p>我们重视您的隐私保护。本隐私政策说明我们如何收集、使用和保护您的个人信息。</p>
<h5 class="font-medium text-gray-800">1. 信息收集</h5>
<p>我们收集的信息包括但不限于:姓名、联系方式、位置信息等。这些信息用于提供更好的服务体验。</p>
<p>
我们收集的信息包括但不限于:姓名、联系方式、位置信息等。这些信息用于提供更好的服务体验。
</p>
<h5 class="font-medium text-gray-800">2. 信息使用</h5>
<p>我们承诺对您的个人信息进行严格保密,不会将其出售、出租或以其他方式泄露给任何第三方。</p>
<p>
我们承诺对您的个人信息进行严格保密,不会将其出售、出租或以其他方式泄露给任何第三方。
</p>
<h5 class="font-medium text-gray-800">3. 信息安全</h5>
<p>我们采用行业标准的安全措施保护您的个人信息,防止未经授权的访问、使用或泄露。</p>
......@@ -72,19 +90,19 @@ defineProps({
show: {
type: Boolean,
required: true,
default: false
default: false,
},
/** 协议类型: 'terms' | 'privacy' */
type: {
type: String,
required: true,
validator: (value) => ['terms', 'privacy'].includes(value)
validator: value => ['terms', 'privacy'].includes(value),
},
/** 弹窗标题 */
title: {
type: String,
required: true
}
required: true,
},
})
defineEmits(['update:show'])
......
......@@ -9,69 +9,81 @@
* @Description: 用户协议组件
-->
<template>
<van-popup
v-model:show="show"
round
position="bottom"
:style="{ height: '90%' }"
teleport="body"
>
<van-popup v-model:show="show" round position="bottom" :style="{ height: '90%' }" teleport="body">
<div class="p-4">
<div class="text-xl font-bold text-center mb-4">美乐爱觉宇宙用户协议</div>
<div class="agreement-content overflow-y-auto h-[calc(100vh*0.8-120px)] px-2">
<h2 class="text-lg font-semibold mb-3">1. 协议的范围</h2>
<p class="mb-4 text-gray-700">欢迎您使用美乐爱觉宇宙平台服务!为使用美乐爱觉宇宙平台服务,您应当阅读并遵守本《用户协议》。请您务必审慎阅读、充分理解各条款内容。</p>
<div class="mb-4 text-center text-xl font-bold">生命力教育联盟宇宙用户协议</div>
<div class="agreement-content h-[calc(100vh*0.8-120px)] overflow-y-auto px-2">
<h2 class="mb-3 text-lg font-semibold">1. 协议的范围</h2>
<p class="mb-4 text-gray-700">
欢迎您使用生命力教育联盟宇宙平台服务!为使用生命力教育联盟宇宙平台服务,您应当阅读并遵守本《用户协议》。请您务必审慎阅读、充分理解各条款内容。
</p>
<h2 class="text-lg font-semibold mb-3">2. 账号注册</h2>
<p class="mb-4 text-gray-700">您在使用本服务前需要注册一个美乐爱觉宇宙账号。美乐爱觉宇宙账号应当使用手机号码绑定注册,请您使用尚未与美乐爱觉宇宙账号绑定的手机号码,以及未被平台根据本协议封禁的手机号码注册。</p>
<h2 class="mb-3 text-lg font-semibold">2. 账号注册</h2>
<p class="mb-4 text-gray-700">
您在使用本服务前需要注册一个生命力教育联盟宇宙账号。生命力教育联盟宇宙账号应当使用手机号码绑定注册,请您使用尚未与生命力教育联盟宇宙账号绑定的手机号码,以及未被平台根据本协议封禁的手机号码注册。
</p>
<h2 class="text-lg font-semibold mb-3">3. 用户个人信息保护</h2>
<p class="mb-4 text-gray-700">我们非常重视用户个人信息的保护,保护用户个人信息是我们的基本原则之一。我们将会采取合理的措施保护用户的个人信息。除法律法规规定的情形外,未经用户许可我们不会向第三方公开、透露用户个人信息。</p>
<h2 class="mb-3 text-lg font-semibold">3. 用户个人信息保护</h2>
<p class="mb-4 text-gray-700">
我们非常重视用户个人信息的保护,保护用户个人信息是我们的基本原则之一。我们将会采取合理的措施保护用户的个人信息。除法律法规规定的情形外,未经用户许可我们不会向第三方公开、透露用户个人信息。
</p>
<h2 class="text-lg font-semibold mb-3">4. 内容规范</h2>
<p class="mb-4 text-gray-700">您在使用本服务时需要遵守法律法规、社会主义制度、国家利益、公民合法权益、公共秩序、社会道德风尚和信息真实性等七条底线。</p>
<h2 class="mb-3 text-lg font-semibold">4. 内容规范</h2>
<p class="mb-4 text-gray-700">
您在使用本服务时需要遵守法律法规、社会主义制度、国家利益、公民合法权益、公共秩序、社会道德风尚和信息真实性等七条底线。
</p>
<h2 class="text-lg font-semibold mb-3">5. 知识产权</h2>
<p class="mb-4 text-gray-700">美乐爱觉宇宙平台所包含的全部智力成果,包括但不限于平台内容、平台设计、源代码等,均属于平台所有。未经平台许可,任何人不得擅自使用。</p>
<h2 class="mb-3 text-lg font-semibold">5. 知识产权</h2>
<p class="mb-4 text-gray-700">
生命力教育联盟宇宙平台所包含的全部智力成果,包括但不限于平台内容、平台设计、源代码等,均属于平台所有。未经平台许可,任何人不得擅自使用。
</p>
<h2 class="text-lg font-semibold mb-3">6. 服务的变更、中断和终止</h2>
<p class="mb-4 text-gray-700">我们可能会对服务内容进行变更,也可能会中断、中止或终止服务。对于付费服务,我们会在变更前通知您,并向您提供退款等必要的补偿。</p>
<h2 class="mb-3 text-lg font-semibold">6. 服务的变更、中断和终止</h2>
<p class="mb-4 text-gray-700">
我们可能会对服务内容进行变更,也可能会中断、中止或终止服务。对于付费服务,我们会在变更前通知您,并向您提供退款等必要的补偿。
</p>
<h2 class="text-lg font-semibold mb-3">7. 违约处理</h2>
<p class="mb-4 text-gray-700">如果您违反本协议约定,我们有权视情况采取预先警示、限制或禁止使用全部或部分服务功能、封禁账号等措施。</p>
<h2 class="mb-3 text-lg font-semibold">7. 违约处理</h2>
<p class="mb-4 text-gray-700">
如果您违反本协议约定,我们有权视情况采取预先警示、限制或禁止使用全部或部分服务功能、封禁账号等措施。
</p>
<h2 class="text-lg font-semibold mb-3">8. 其他条款</h2>
<p class="mb-4 text-gray-700">本协议所有条款的标题仅为阅读方便,本身并无实际涵义,不能作为本协议涵义解释的依据。如果本协议中任何一条被视为废止、无效或不可执行,应视为可分的且并不影响任何其余条款的有效性和可执行性。</p>
<h2 class="mb-3 text-lg font-semibold">8. 其他条款</h2>
<p class="mb-4 text-gray-700">
本协议所有条款的标题仅为阅读方便,本身并无实际涵义,不能作为本协议涵义解释的依据。如果本协议中任何一条被视为废止、无效或不可执行,应视为可分的且并不影响任何其余条款的有效性和可执行性。
</p>
</div>
<div class="flex justify-center mt-4">
<van-button round type="primary" color="#4CAF50" block @click="handleClose">我已阅读并同意</van-button>
<div class="mt-4 flex justify-center">
<van-button round type="primary" color="#4CAF50" block @click="handleClose"
>我已阅读并同意</van-button
>
</div>
</div>
</van-popup>
</template>
<script setup>
import { ref, defineExpose } from 'vue';
import { ref, defineExpose } from 'vue'
const show = ref(false);
const show = ref(false)
/**
* @description 关闭协议弹窗
*/
const handleClose = () => {
show.value = false;
};
show.value = false
}
/**
* @description 打开协议弹窗
*/
const openAgreement = () => {
show.value = true;
};
show.value = true
}
defineExpose({
openAgreement
});
openAgreement,
})
</script>
<style lang="less" scoped>
......
<template>
<div class="recall-poster-container w-full max-w-[340px] relative select-none shrink-0 my-auto mx-auto">
<div
class="recall-poster-container relative mx-auto my-auto w-full max-w-[340px] shrink-0 select-none"
>
<!-- 最终生成的海报图片展示区域 -->
<div v-if="posterImgSrc" class="relative w-full fade-in">
<img :src="posterImgSrc" class="w-full h-auto rounded-2xl shadow-2xl block" alt="分享海报" />
<div class="text-white/80 text-center text-xs mt-4">长按图片保存</div>
<div v-if="posterImgSrc" class="fade-in relative w-full">
<img :src="posterImgSrc" class="block h-auto w-full rounded-2xl shadow-2xl" alt="分享海报" />
<div class="mt-4 text-center text-xs text-white/80">长按图片保存</div>
</div>
<!-- 生成中/加载中占位 -->
<div v-else
class="w-full h-[62vh] min-h-[400px] bg-white/10 backdrop-blur-sm rounded-2xl flex flex-col items-center justify-center text-white/80">
<div
v-else
class="flex h-[62vh] min-h-[400px] w-full flex-col items-center justify-center rounded-2xl bg-white/10 text-white/80 backdrop-blur-sm"
>
<van-loading type="spinner" color="#ffffff" size="32px" />
<div class="mt-4 text-sm font-medium">海报生成中...</div>
</div>
......@@ -27,31 +31,31 @@ const props = defineProps({
/** 海报背景图片 URL */
bgUrl: {
type: String,
required: true
required: true,
},
/** 海报标题 */
title: {
type: String,
default: ''
default: '',
},
/** Logo 图片 URL */
logoUrl: {
type: String,
default: 'https://cdn.ipadbiz.cn/mlaj/recall/poster/kai@2x.png'
default: 'https://cdn.ipadbiz.cn/mlaj/recall/poster/kai@2x.png',
},
/** 二维码图片 URL */
qrUrl: {
type: String,
default: 'https://cdn.ipadbiz.cn/mlaj/recall/poster/%E4%BA%8C%E7%BB%B4%E7%A0%81@2x.png'
}
default: 'https://cdn.ipadbiz.cn/mlaj/recall/poster/%E4%BA%8C%E7%BB%B4%E7%A0%81@2x.png',
},
})
const canvasRef = ref(null)
const posterImgSrc = ref('')
// 工具函数:加载图片
const loadImage = (src) => {
return new Promise((resolve, reject) => {
const loadImage = src =>
new Promise((resolve, reject) => {
if (!src) {
reject(new Error('Image source is empty'))
return
......@@ -62,7 +66,7 @@ const loadImage = (src) => {
img.crossOrigin = 'anonymous'
}
img.onload = () => resolve(img)
img.onerror = (e) => {
img.onerror = e => {
console.error('Failed to load image:', src)
// 图片加载失败不应该阻断流程,返回 null 或者透明图占位
// 这里resolve null,绘制时跳过
......@@ -70,7 +74,6 @@ const loadImage = (src) => {
}
img.src = src
})
}
// 工具函数:绘制圆角矩形
const drawRoundedRect = (ctx, x, y, width, height, radius) => {
......@@ -170,7 +173,10 @@ const generatePoster = async () => {
let qrUrlToLoad = props.qrUrl
let qrBlobUrl = null
// 兼容完整 URL 和相对路径,只要包含特定特征或是相对路径
if (qrUrlToLoad && (qrUrlToLoad.startsWith('/') || qrUrlToLoad.includes('m=srv') || qrUrlToLoad.includes('http'))) {
if (
qrUrlToLoad &&
(qrUrlToLoad.startsWith('/') || qrUrlToLoad.includes('m=srv') || qrUrlToLoad.includes('http'))
) {
try {
// 使用 fetch 替代 axios,避免拦截器自动添加 header 导致 CORS 预检失败
const res = await fetch(qrUrlToLoad)
......@@ -188,7 +194,7 @@ const generatePoster = async () => {
const [bgImg, logoImg, qrImg] = await Promise.all([
loadImage(props.bgUrl),
loadImage(props.logoUrl),
loadImage(qrUrlToLoad)
loadImage(qrUrlToLoad),
])
// 清理 Blob URL
......@@ -250,13 +256,13 @@ const generatePoster = async () => {
ctx.shadowOffsetY = 1 * scale
// Column 1: "每一段成长故事" (左边那列,离右边远一点)
// Column 2: "见证我在美乐爱觉宇宙的" (右边那列)
// Column 2: "见证我在生命力教育联盟宇宙的" (右边那列)
const colRightX = width - textRightMargin - (fontSize / 2)
const colRightX = width - textRightMargin - fontSize / 2
const colLeftX = colRightX - fontSize - 12 * scale // gap-3 = 12px
const textRight = "见证我在美乐爱觉宇宙的"
const textLeft = "每一段成长故事"
const textRight = '见证我在生命力教育联盟宇宙的'
const textLeft = '每一段成长故事'
// 计算文本对齐
// 目标:整个文本块的底部与 bottom-6 对齐
......@@ -294,7 +300,6 @@ const generatePoster = async () => {
ctx.shadowColor = 'transparent'
// 6. 绘制 Info Area 内容
// 坐标参考
const infoY = imgAreaHeight
......@@ -313,7 +318,7 @@ const generatePoster = async () => {
const titleX = padding
const titleY = infoY + padding
const qrSectionW = 85 * scale // approx
const titleMaxW = width - padding - qrSectionW - (10 * scale) // extra gap
const titleMaxW = width - padding - qrSectionW - 10 * scale // extra gap
ctx.fillStyle = '#0052D9'
ctx.font = `bold ${15 * scale}px sans-serif`
......@@ -331,7 +336,7 @@ const generatePoster = async () => {
// Section starts at width - padding - qrSectionW?
// Actually DOM is flex justify-between.
// QR is at the very right (minus padding).
const qrX = width - padding - qrSize + (4 * scale) // slight adjustment
const qrX = width - padding - qrSize + 4 * scale // slight adjustment
const qrY = infoY + padding
ctx.drawImage(qrImg, qrX, qrY, qrSize, qrSize)
......@@ -345,24 +350,26 @@ const generatePoster = async () => {
ctx.textAlign = 'center'
const textCenterX = qrX + qrSize / 2
const textStartY = qrY + qrSize + (8 * scale) // mb-2 is for img
const textStartY = qrY + qrSize + 8 * scale // mb-2 is for img
ctx.fillText("跟我一起加入", textCenterX, textStartY)
ctx.fillText("美乐爱觉宇宙吧", textCenterX, textStartY + smallTextSize * 1.4)
ctx.fillText('跟我一起加入', textCenterX, textStartY)
ctx.fillText('生命力教育联盟宇宙吧', textCenterX, textStartY + smallTextSize * 1.4)
}
// 7. 导出图片
posterImgSrc.value = canvas.toDataURL('image/png')
} catch (error) {
console.error('Canvas poster generation failed:', error)
showToast('生成失败,请重试')
}
}
watch(() => [props.bgUrl, props.qrUrl], () => {
watch(
() => [props.bgUrl, props.qrUrl],
() => {
generatePoster()
})
}
)
onMounted(() => {
// 稍微延时确保字体等资源就绪(虽然canvas不强依赖DOM渲染,但字体加载是全局的)
......@@ -370,7 +377,7 @@ onMounted(() => {
})
defineExpose({
generatePoster
generatePoster,
})
</script>
......
......@@ -8,12 +8,12 @@
<template>
<div class="welcome-content">
<!-- 标题区域 -->
<div class="mt-20 flex flex-col items-center z-10 w-full px-8">
<img :src="titleImg" class="w-full max-w-[300px] mb-4 object-contain" alt="美乐爱觉" />
<div class="z-10 mt-20 flex w-full flex-col items-center px-8">
<img :src="titleImg" class="mb-4 w-full max-w-[300px] object-contain" alt="生命力教育联盟" />
</div>
<!-- 功能入口区域 - 水平布局,自动推到底部 -->
<div class="mt-auto entry-orbit">
<div class="entry-orbit mt-auto">
<div class="orbit-entries">
<WelcomeEntryItem
v-for="entry in entries"
......@@ -39,7 +39,7 @@ const entries = ref(welcomeEntries)
// 导入标题图片
const titleImg = 'https://cdn.ipadbiz.cn/mlaj/recall/img/title007@2x.png'
const handleEntryClick = (entry) => {
const handleEntryClick = entry => {
if (entry.isExternal) {
// 外部链接:获取用户ID并拼接
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}')
......@@ -103,7 +103,8 @@ const handleEntryClick = (entry) => {
// 动画定义
@keyframes float {
0%, 100% {
0%,
100% {
transform: translateY(0);
}
50% {
......
......@@ -5,7 +5,7 @@
* @FilePath: /mlaj/src/composables/useShare.js
* @Description: 微信分享相关逻辑
*/
import wx from 'weixin-js-sdk';
import wx from 'weixin-js-sdk'
/**
* @function normalize_image_url
......@@ -14,15 +14,15 @@ import wx from 'weixin-js-sdk';
* @returns {string} 处理后的图片地址
*/
function normalize_image_url(src) {
if (!src) return '';
if (!src) return ''
if (src.includes('cdn.ipadbiz.cn')) {
const compress = 'imageMogr2/thumbnail/200x/strip/quality/70';
const compress = 'imageMogr2/thumbnail/200x/strip/quality/70'
if (src.includes('?')) {
return src.includes(compress) ? src : `${src}&${compress}`;
return src.includes(compress) ? src : `${src}&${compress}`
}
return `${src}?${compress}`;
return `${src}?${compress}`
}
return src;
return src
}
/**
......@@ -41,30 +41,30 @@ function normalize_image_url(src) {
* @param {string} params.imgUrl 分享图标地址
* @returns {void}
*/
export const sharePage = ({ title = '美乐爱觉', desc = '', imgUrl = '' }) => {
export const sharePage = ({ title = '生命力教育联盟', desc = '', imgUrl = '' }) => {
const shareData = {
title, // 分享标题
desc, // 分享描述
link: location.origin + location.pathname + location.hash, // 分享链接,需与公众号 JS 安全域名一致
imgUrl: normalize_image_url(imgUrl), // 分享图标,按规则追加压缩参数
success: function () {
success() {
// 设置成功回调
}
},
}
if (wx && typeof wx.ready === 'function') {
wx.ready(() => {
// 分享好友(微信好友或qq好友)
wx.updateAppMessageShareData(shareData);
wx.updateAppMessageShareData(shareData)
// 分享到朋友圈或qq空间
wx.updateTimelineShareData(shareData);
wx.updateTimelineShareData(shareData)
// 分享到腾讯微博
if (typeof wx.onMenuShareWeibo === 'function') {
wx.onMenuShareWeibo(shareData);
wx.onMenuShareWeibo(shareData)
}
});
})
} else {
// 微信 JSSDK 未初始化或未就绪,分享配置可能不会生效
console.warn('微信 JSSDK 未就绪:分享配置可能未生效');
console.warn('微信 JSSDK 未就绪:分享配置可能未生效')
}
}
......
This diff is collapsed. Click to expand it.
......@@ -23,7 +23,7 @@ export const routes = [
path: '/',
name: 'HomePage',
component: () => import('../views/HomePage.vue'),
meta: { title: '美乐爱觉' },
meta: { title: '生命力教育联盟' },
},
{
path: '/courses',
......@@ -158,7 +158,7 @@ export const routes = [
path: '/recall/choose',
name: 'ChoosePage',
component: () => import('../views/recall/ChoosePage.vue'),
meta: { title: '美乐爱觉AI星球' },
meta: { title: '生命力教育联盟AI星球' },
},
{
path: '/checkout',
......
This diff is collapsed. Click to expand it.
<template>
<div class="min-h-screen flex flex-col bg-gradient-to-br from-green-50 via-teal-50 to-blue-50 py-12 px-4 sm:px-6 lg:px-8">
<div
class="flex min-h-screen flex-col bg-gradient-to-br from-green-50 via-teal-50 to-blue-50 px-4 py-12 sm:px-6 lg:px-8"
>
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h1 class="text-center text-3xl font-bold text-gray-800 mb-2">美乐爱觉教育</h1>
<h1 class="mb-2 text-center text-3xl font-bold text-gray-800">生命力教育联盟教育</h1>
<h2 class="text-center text-xl font-medium text-gray-600">重置密码</h2>
</div>
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<FrostedGlass class="py-8 px-6 rounded-lg">
<div v-if="error" class="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-md">
<FrostedGlass class="rounded-lg px-6 py-8">
<div
v-if="error"
class="mb-4 rounded-md border border-red-400 bg-red-100 px-4 py-3 text-red-700"
>
{{ error }}
</div>
......@@ -25,7 +30,7 @@
maxlength="11"
@input="formData.phone = formData.phone.replace(/\D/g, '')"
@blur="validatePhone"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-green-500 focus:outline-none focus:ring-green-500"
/>
</div>
......@@ -40,13 +45,13 @@
type="text"
required
maxlength="6"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-green-500 focus:outline-none focus:ring-green-500"
/>
<button
type="button"
:disabled="countdown > 0 || !isPhoneValid"
@click="sendVerificationCode"
class="mt-1 px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
class="mt-1 whitespace-nowrap rounded-md border border-transparent bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
{{ countdown > 0 ? `${countdown}秒后重试` : '获取验证码' }}
</button>
......@@ -63,7 +68,7 @@
type="password"
required
minlength="6"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-green-500 focus:outline-none focus:ring-green-500"
/>
</div>
......@@ -77,7 +82,7 @@
type="password"
required
minlength="6"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-green-500 focus:outline-none focus:ring-green-500"
/>
</div>
......@@ -85,15 +90,15 @@
<button
type="submit"
:disabled="loading"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
:class="{ 'opacity-70 cursor-not-allowed': loading }"
class="flex w-full justify-center rounded-md border border-transparent bg-gradient-to-r from-green-500 to-green-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:from-green-600 hover:to-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
:class="{ 'cursor-not-allowed opacity-70': loading }"
>
{{ loading ? '提交中...' : '重置密码' }}
</button>
</div>
</form>
<div class="text-center mt-6">
<div class="mt-6 text-center">
<p class="text-sm text-gray-600">
记起密码了?
<router-link to="/login" class="font-medium text-green-600 hover:text-green-500">
......@@ -110,9 +115,9 @@
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import FrostedGlass from '@/components/effects/FrostedGlass.vue'
import { smsAPI } from '@/api/common';
import { resetPasswordAPI } from '@/api/users';
import { showToast } from 'vant';
import { smsAPI } from '@/api/common'
import { resetPasswordAPI } from '@/api/users'
import { showToast } from 'vant'
const router = useRouter()
const error = ref('')
......@@ -124,7 +129,7 @@ const formData = reactive({
phone: '',
verificationCode: '',
password: '',
confirmPassword: ''
confirmPassword: '',
})
const startCountdown = () => {
......@@ -187,7 +192,7 @@ const handleSubmit = async () => {
const { code } = await resetPasswordAPI({
mobile: formData.phone,
sms_code: formData.verificationCode,
password: formData.password
password: formData.password,
})
if (code === 1) {
......@@ -195,7 +200,6 @@ const handleSubmit = async () => {
// 重置成功后跳转到登录页
router.push('/login')
}
} catch (err) {
console.error('Reset password error:', err)
error.value = '重置密码失败,请稍后重试'
......
......@@ -8,7 +8,7 @@
size="10rem"
style="margin-bottom: 0.5rem"
/>
<h1 class="mb-2 text-center text-3xl font-bold text-gray-800">美乐爱觉教育</h1>
<h1 class="mb-2 text-center text-3xl font-bold text-gray-800">生命力教育联盟教育</h1>
<!-- <h2 class="text-center text-xl font-medium text-gray-600">欢迎回来</h2> -->
</div>
......@@ -168,7 +168,7 @@
class="cursor-pointer font-medium text-green-600 hover:text-green-500"
@click="userAgreementRef.openAgreement()"
>
美乐爱觉宇宙用户协议》
生命力教育联盟宇宙用户协议》
</span>
<UserAgreement ref="userAgreementRef" />
</p>
......
<template>
<div class="min-h-screen flex flex-col bg-gradient-to-br from-green-50 via-teal-50 to-blue-50 py-12 px-4 sm:px-6 lg:px-8">
<div
class="flex min-h-screen flex-col bg-gradient-to-br from-green-50 via-teal-50 to-blue-50 px-4 py-12 sm:px-6 lg:px-8"
>
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h1 class="text-center text-3xl font-bold text-gray-800 mb-2">美乐爱觉教育</h1>
<h1 class="mb-2 text-center text-3xl font-bold text-gray-800">生命力教育联盟教育</h1>
<h2 class="text-center text-xl font-medium text-gray-600">创建账号</h2>
</div>
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<FrostedGlass class="py-8 px-6 rounded-lg">
<div v-if="error" class="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-md">
<FrostedGlass class="rounded-lg px-6 py-8">
<div
v-if="error"
class="mb-4 rounded-md border border-red-400 bg-red-100 px-4 py-3 text-red-700"
>
{{ error }}
</div>
......@@ -21,7 +26,7 @@
v-model="formData.name"
type="text"
required
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-green-500 focus:outline-none focus:ring-green-500"
/>
</div>
......@@ -39,7 +44,7 @@
maxlength="11"
@input="formData.phone = formData.phone.replace(/\D/g, '')"
@blur="validatePhone"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-green-500 focus:outline-none focus:ring-green-500"
/>
</div>
......@@ -54,13 +59,13 @@
type="text"
required
maxlength="6"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-green-500 focus:outline-none focus:ring-green-500"
/>
<button
type="button"
:disabled="countdown > 0 || !isPhoneValid"
@click="sendVerificationCode"
class="mt-1 px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
class="mt-1 whitespace-nowrap rounded-md border border-transparent bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
{{ countdown > 0 ? `${countdown}秒后重试` : '获取验证码' }}
</button>
......@@ -78,7 +83,7 @@
autocomplete="new-password"
required
minlength="6"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-green-500 focus:outline-none focus:ring-green-500"
/>
</div>
......@@ -93,7 +98,7 @@
autocomplete="new-password"
required
minlength="6"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500"
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-green-500 focus:outline-none focus:ring-green-500"
/>
</div>
......@@ -105,15 +110,25 @@
checked-color="#4caf50"
class="scale-90"
icon-size="18px"
><span class="text-sm">我已阅读并同意 <a href="#" class="text-green-600 hover:text-green-500" @click.prevent="openTerms">用户协议</a> 和 <a href="#" class="text-green-600 hover:text-green-500" @click.prevent="openPrivacy">隐私政策</a></span></van-checkbox>
><span class="text-sm"
>我已阅读并同意
<a href="#" class="text-green-600 hover:text-green-500" @click.prevent="openTerms"
>用户协议</a
>
<a href="#" class="text-green-600 hover:text-green-500" @click.prevent="openPrivacy"
>隐私政策</a
></span
></van-checkbox
>
</div>
<div>
<button
type="submit"
:disabled="loading"
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
:class="{ 'opacity-70 cursor-not-allowed': loading }"
class="flex w-full justify-center rounded-md border border-transparent bg-gradient-to-r from-green-500 to-green-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:from-green-600 hover:to-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
:class="{ 'cursor-not-allowed opacity-70': loading }"
>
{{ loading ? '注册中...' : '立即注册' }}
</button>
......@@ -154,7 +169,7 @@
</div>
</div> -->
<div class="text-center mt-6">
<div class="mt-6 text-center">
<p class="text-sm text-gray-600">
已有账号?
<router-link to="/login" class="font-medium text-green-600 hover:text-green-500">
......@@ -165,16 +180,8 @@
</FrostedGlass>
</div>
<TermsPopup
v-model:show="showTerms"
:type="popupType"
:title="popupTitle"
/>
<TermsPopup
v-model:show="showPrivacy"
:type="popupType"
:title="popupTitle"
/>
<TermsPopup v-model:show="showTerms" :type="popupType" :title="popupTitle" />
<TermsPopup v-model:show="showPrivacy" :type="popupType" :title="popupTitle" />
</div>
</template>
......@@ -184,13 +191,13 @@ import { useRoute, useRouter } from 'vue-router'
import FrostedGlass from '@/components/effects/FrostedGlass.vue'
import TermsPopup from '@/components/common/TermsPopup.vue'
import { useAuth } from '@/contexts/auth'
import { useTitle } from '@vueuse/core';
import { smsAPI } from '@/api/common';
import { registerAPI } from '@/api/users';
import { showToast } from 'vant';
import { useTitle } from '@vueuse/core'
import { smsAPI } from '@/api/common'
import { registerAPI } from '@/api/users'
import { showToast } from 'vant'
const $route = useRoute();
useTitle($route.meta.title);
const $route = useRoute()
useTitle($route.meta.title)
const router = useRouter()
const { login } = useAuth()
......@@ -203,7 +210,7 @@ const formData = reactive({
verificationCode: '',
password: '',
confirmPassword: '',
agreeTerms: false
agreeTerms: false,
})
const startCountdown = () => {
......@@ -293,7 +300,7 @@ const handleSubmit = async () => {
const success = login({
name: formData.name,
mobile: formData.phone,
avatar: ''
avatar: '',
})
if (success) {
router.push('/')
......
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.