This commit is contained in:
zhutao
2025-10-27 15:42:57 +08:00
commit 7817c39606
44 changed files with 3881 additions and 0 deletions

13
src/App.vue Normal file
View File

@@ -0,0 +1,13 @@
<template>
<router-view></router-view>
</template>
<script setup lang="ts">
</script>
<style scoped lang="scss">
#app {
}
</style>

15
src/api/activity.ts Normal file
View File

@@ -0,0 +1,15 @@
import request from "@/utils/http/request";
/**
* 获取活动数据
*/
export function getActiveInfoApi() {
return request.get("/yg/active_info")
}
/**
* 创建订单
*/
export function createOrderApi() {
return request.post("/yg/order/create")
}

22
src/api/invite.ts Normal file
View File

@@ -0,0 +1,22 @@
import request from "@/utils/http/request";
/**
* 获取邀请信息
*/
export function getInviteInfoApi() {
return request.get("/yg/invite/info")
}
/**
* 获取邀请人详情
*/
export function getInviteDetailApi(code: string) {
return request.get(`/yg/invite/detail?invite_code=${code}`)
}
/**
* 绑定邀请关系
*/
export function bindInviteRelationApi(code: string) {
return request.post(`/yg/invite/bind?invite_code=${code}`)
}

15
src/api/wxchat.ts Normal file
View File

@@ -0,0 +1,15 @@
import request from "@/utils/http/request";
//微信授权
export function Login(data:any):Promise<any>{
return request.post('/login', data)
}
//微信分享
export function getJsSdk(params:any):Promise<any>{
return request.get('/get_jssdk', params)
}
export function upload(params:any){
return request.get("/getuploadtoken", params)
}

19
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}
declare global {
interface ImportMetaEnv {
readonly VITE_APPID: string;
readonly VITE_WEB_URL:string,
// 更多环境变量...
}
interface ImportMeta {
readonly envs: ImportMetaEnv
}
}

15
src/main.ts Normal file
View File

@@ -0,0 +1,15 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from "@/router";
import store from "@/stores";
import "@/styles/bg.css"
//引入vant
import Vant from "@/plugin/vant";
let app = createApp(App)
Vant(app)
app.use(router)
app.use(store)
app.mount('#app')

53
src/plugin/vant.ts Normal file
View File

@@ -0,0 +1,53 @@
import {
Button,
CountDown,
Dialog,
FloatingBubble,
Icon,
Image,
Progress,
Tag,
Cell,
CellGroup,
Form, Field,
RadioGroup,
Radio,
Skeleton,
SkeletonTitle,
SkeletonImage,
SkeletonAvatar,
SkeletonParagraph,
Empty, Loading, Calendar, TimePicker, Popup, Swipe, SwipeItem, Step, Steps
} from "vant";
import 'vant/lib/index.css';
export default function (app: any) {
app.use(Button)
app.use(Icon)
app.use(CountDown)
app.use(Image)
app.use(Progress)
app.use(Tag)
app.use(Dialog)
app.use(FloatingBubble)
app.use(Cell);
app.use(Form);
app.use(Field);
app.use(CellGroup)
app.use(RadioGroup)
app.use(Radio)
app.use(Skeleton)
app.use(SkeletonTitle)
app.use(SkeletonImage)
app.use(SkeletonAvatar)
app.use(SkeletonParagraph)
app.use(Empty)
app.use(Loading)
app.use(Calendar)
app.use(TimePicker)
app.use(Popup)
app.use(Swipe)
app.use(SwipeItem)
app.use(Step)
app.use(Steps)
}

29
src/router/index.ts Normal file
View File

@@ -0,0 +1,29 @@
import {createRouter, createWebHistory} from "vue-router";
import {dynamicRoutes} from "@/router/modules";
import wxShare from "@/wx/wxShare";
import wxLogin from "@/wx/wxLogin";
const routerHistory = createWebHistory()
const router = createRouter({
history: routerHistory,
routes: dynamicRoutes
})
router.beforeEach(async (to, from, next) => {
setTimeout(() => {
wxShare(to.meta).then()
}, 1000)
document.title = to.meta.title || "抱抱-心里有光成长不慌"
if (!to.meta.noAuth) {
wxLogin().then(() => {
next()
})
} else {
next()
}
})
export default router

View File

@@ -0,0 +1,25 @@
import type {RouteRecordRaw} from "vue-router";
const routes: RouteRecordRaw[] = [
{
path: "/activity",
component: () => import("@/views/activity/detail/index.vue"),
meta: {
title: "活动",
noAuth: true,
noShare: true,
}
},
{
path:"/pay_success",
component:() => import("@/views/activity/success/index.vue"),
meta:{
title: "支付成功",
noAuth: true,
noShare: true,
}
}
]
export default routes

View File

@@ -0,0 +1,35 @@
import type {RouteRecordRaw} from "vue-router";
const routes: RouteRecordRaw[] = [
{
// 404页面
path: "/:pathMatch(.*)*",
name: '404',
component: () => import("@/views/system/404.vue"),
meta: {
title: "404",
noAuth: true,
noShare: true,
},
},
{
path: "/invite",
component: () => import("@/views/invite/invite.vue"),
meta: {
title: "邀请好友得免费课时",
noAuth: true,
noShare: true,
},
},
{
path:"/accept",
component: () => import("@/views/invite/accept.vue"),
meta: {
title: "",
noAuth: true,
noShare: true,
},
}
]
export default routes

View File

@@ -0,0 +1,12 @@
import type {RouteRecordRaw} from "vue-router";
const dynamicRoutes: RouteRecordRaw[] = []
const dynamicRouteFiles = import.meta.glob("@/router/modules/*.ts", {eager: true})
Object.keys(dynamicRouteFiles).forEach((key) => {
const module = (dynamicRouteFiles[key] as any).default
dynamicRoutes.push(...module)
})
export {dynamicRoutes}

10
src/stores/index.ts Normal file
View File

@@ -0,0 +1,10 @@
import {createPinia} from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
export * from "./modules/user"
// 持久化存储pinia
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export default pinia

View File

@@ -0,0 +1,30 @@
import {defineStore} from "pinia";
interface AccessState {
token: string,
userInfo: UserState
}
interface UserState {
nickname: string,
tel: string,
avatar: string,
wx_openid: string
}
//@ts-ignore
export const useUserStore = defineStore("app-access", {
state: (): AccessState => ({
token: '',
userInfo: {
nickname: '', //姓名
avatar: '', //头像
tel: '', //手机号
wx_openid: '',
},
}),
persist: {
enabled: true,
}
})

1
src/styles/bg.css Normal file
View File

@@ -0,0 +1 @@
@import "tailwindcss";

9
src/types/index.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
import type {RouteMetaType} from '@/types/route-type'
declare module "weixin-js-sdk"
declare module 'vue-router' {
interface RouteMeta extends RouteMetaType {
} // 关键:合并声明
}

37
src/types/route-type.ts Normal file
View File

@@ -0,0 +1,37 @@
import type {RouteRecordRaw} from "vue-router";
/**
* 路由元信息
*/
export interface RouteMetaType {
/**
* 页面标题
*/
title: string;
/**
* 是否不需要授权
*/
noAuth?: boolean;
/**
* 是否不分享
*/
noShare?: boolean;
shareData?: ShareDataType
}
/**
* 分享数据
*/
export interface ShareDataType {
title: string;
desc?: string;
imgUrl?: string;
link?: string
}
export type RouteRawType = {
meta: RouteMetaType;
children?: RouteRawType[];
} & RouteRecordRaw

24
src/utils/format.ts Normal file
View File

@@ -0,0 +1,24 @@
/**
* 日期格式化
* @param {Date} date
* @param {string} format 格式
*/
export function formatDate(date: any = new Date(), format = 'YYYY-MM-DD hh:mm:ss') {
if (!(date instanceof Date)) {
date = new Date(date);
}
const YYYY = date.getFullYear().toString();
const MM = ('0' + (date.getMonth() + 1)).slice(-2);
const DD = ('0' + date.getDate()).slice(-2);
const hh = ('0' + date.getHours()).slice(-2);
const mm = ('0' + date.getMinutes()).slice(-2);
const ss = ('0' + date.getSeconds()).slice(-2);
return format
.replace(/YYYY/, YYYY)
.replace(/MM/, MM)
.replace(/DD/, DD)
.replace(/hh/, hh)
.replace(/mm/, mm)
.replace(/ss/, ss);
}

13
src/utils/http/error.ts Normal file
View File

@@ -0,0 +1,13 @@
import qs from "qs";
/**
* 错误处理
*/
export function errorHand() {
window.localStorage.removeItem("app-access")
let location = window.location
let query = qs.parse(location.href.split('?')[1])
delete query.code
let query2 = qs.stringify(query)
window.location.href = location.origin + location.pathname + '?' + query2
}

55
src/utils/http/request.ts Normal file
View File

@@ -0,0 +1,55 @@
import axios, {type AxiosRequestHeaders, type AxiosResponse} from "axios"
import {closeToast, showToast} from 'vant';
import {useUserStore} from "@/stores";
import {errorHand} from "@/utils/http/error";
const instance = axios.create({
baseURL: import.meta.env.VITE_WEB_URL + "/api",
timeout: 60000,
})
//拦截器
instance.interceptors.request.use((config) => {
const store = useUserStore()
if (store.token) {
(config.headers as AxiosRequestHeaders).Authorization = `Bearer ${store.token}`
}
return config
})
instance.interceptors.response.use((response: AxiosResponse) => {
const {code, data, message} = response.data
closeToast()
if (code === 1) {
return data
} else if (code === 0) {
showToast(message)
return Promise.reject(new Error(message))
} else {
if (code === 401 || code === 403) {
errorHand()
} else {
showToast(message)
}
}
}, error => {
showToast(error.message)
})
//封装postget
function requestPost(url: string, data = {}) {
return instance.post(url, data)
}
function requestGet(url: string, params = {}) {
return instance.get(url, {params})
}
const request = {
get: requestGet,
post: requestPost
}
export default request

View File

@@ -0,0 +1,57 @@
<template>
<div class="py-12 px-4 bg-gradient-to-b from-white to-orange-50">
<!-- 标题 -->
<motion.div
:initial="{ opacity: 0, y: 30 }"
:while-in-view="{ opacity: 1, y: 0 }"
:viewport="{ once: true }"
:transition="{ duration: 0.6 }"
class="text-center mb-10"
>
<h2 class="text-3xl md:text-4xl mb-3 text-gray-800">课程核心优势</h2>
<p class="text-gray-600">四大亮点助力孩子高效学习</p>
</motion.div>
<!-- 卡片网格 -->
<div class="max-w-5xl mx-auto grid grid-cols-1 sm:grid-cols-2 gap-6">
<motion.div
v-for="(item, index) in highlights"
:key="index"
:initial="{ opacity: 0, y: 30 }"
:while-in-view="{ opacity: 1, y: 0 }"
:viewport="{ once: true }"
:transition="{ duration: 0.5, delay: index * 0.1 }"
:while-hover="{ scale: 1.05, y: -5 }"
class="bg-white rounded-2xl p-6 shadow-lg hover:shadow-xl transition-all border border-orange-100">
<div class="flex items-start gap-4">
<div
class="bg-gradient-to-br from-orange-400 to-orange-600 text-white p-3 rounded-xl flex-shrink-0">
<component :is="item.icon" :size="28"/>
</div>
<div class="flex-1">
<h3 class="text-xl mb-2 text-gray-800">{{ item.title }}</h3>
<p class="text-gray-600">{{ item.description }}</p>
</div>
</div>
</motion.div>
</div>
</div>
</template>
<script setup lang="ts">
import {motion} from 'motion-v'
import {User} from "@icon-park/vue-next"
interface Highlight {
icon: any
title: string
description: string
}
const highlights: Highlight[] = [
{icon: User, title: '专业讲师陪伴', description: '资深教师在线督导,实时答疑解惑'},
{icon: User, title: '高效学习氛围', description: '沉浸式学习环境,远离干扰提升效率'},
{icon: User, title: '专注力提升', description: '科学时间管理,培养良好学习习惯'},
{icon: User, title: '同伴互助学习', description: '与优秀学员共同进步,激发学习动力'}
]
</script>

View File

@@ -0,0 +1,121 @@
<script setup lang="ts">
import {ref, onUnmounted, inject} from 'vue'
import {Motion} from 'motion-v'
import TimeUnit from './time-unit.vue'
const activeInfo = inject("activeInfo") as any;
const timeLeft = ref({hours: 0, minutes: 0, seconds: 0})
let timer: any = null
const props = defineProps({
end: {
type: Boolean,
default: false
}
})
// 🕐 初始化倒计时
function initCountdown() {
if (!activeInfo?.value) return;
const endTime = new Date(activeInfo.value.active_end_at).getTime();
if (props.end) {
return;
}
timer = setInterval(() => {
const now = Date.now();
const diff = Math.max(0, endTime - now);
if (diff === 0 && timer) {
clearInterval(timer);
timer = null;
}
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
timeLeft.value = {hours, minutes, seconds};
}, 1000)
}
initCountdown()
onUnmounted(() => {
if (timer) clearInterval(timer)
})
</script>
<template>
<div class="relative h-[70vh] min-h-[500px] overflow-hidden">
<!-- 背景图 + 遮罩 -->
<div class="absolute inset-0 bg-cover bg-center"
:style="{backgroundImage: `url('https://images.unsplash.com/photo-1753613648191-4771cf76f034')`}">
<div class="absolute inset-0 bg-gradient-to-b from-black/50 via-black/40 to-black/60"/>
</div>
<!-- 顶部导航 -->
<div class="relative z-10 flex justify-between items-center p-4">
<Motion
:initial="{ opacity: 0, x: -20 }"
:animate="{ opacity: 1, x: 0 }"
:transition="{ duration: 0.5 }"
class="text-white text-xl">
<span class="bg-gradient-to-r from-orange-400 to-orange-500 px-3 py-1 rounded-lg">
有光自习室
</span>
</Motion>
</div>
<!-- 主体内容 -->
<div class="relative z-10 flex flex-col items-center justify-center h-full px-4 text-center text-white pb-16">
<Motion
:initial="{ opacity: 0, y: 30 }"
:animate="{ opacity: 1, y: 0 }"
:transition="{ duration: 0.6, delay: 0.2 }"
>
<h1 class="text-4xl md:text-5xl mb-4 text-white">专业老师在线陪读</h1>
<p class="text-xl md:text-2xl mb-8 text-white/90">让孩子高效学习专注成长</p>
</Motion>
<!-- 倒计时 -->
<Motion
:initial="{ opacity: 0, scale: 0.8 }"
:animate="{ opacity: 1, scale: 1 }"
:transition="{ duration: 0.6, delay: 0.4 }"
class="bg-white/10 backdrop-blur-md rounded-2xl p-6 border border-white/20 shadow-2xl"
>
<p class="text-orange-400 mb-4">
{{ end ? "活动已结束" : "限时优惠倒计时" }}
</p>
<div class="flex gap-4">
<TimeUnit :value="timeLeft.hours" label="时"/>
<span class="text-3xl">:</span>
<TimeUnit :value="timeLeft.minutes" label="分"/>
<span class="text-3xl">:</span>
<TimeUnit :value="timeLeft.seconds" label="秒"/>
</div>
</Motion>
<Motion
:initial="{ opacity: 0, y: 20 }"
:animate="{ opacity: 1, y: 0 }"
:transition="{ duration: 0.6, delay: 0.6 }"
class="mt-8"
>
<div
class="bg-gradient-to-r from-orange-500 to-orange-600 text-white px-8 py-3 rounded-full inline-block shadow-lg">
<span class="text-2xl">课时翻倍 限时抢购</span>
</div>
</Motion>
</div>
</div>
</template>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,117 @@
<script setup lang="ts">
import {motion} from 'motion-v'
import {Check,Time} from "@icon-park/vue-next"
import {inject} from "vue";
const activeInfo = inject("activeInfo") as any;
</script>
<template>
<div class="py-16 px-4 bg-gradient-to-br from-orange-500 to-orange-600 text-white relative overflow-hidden">
<!-- 背景装饰 -->
<div class="absolute inset-0 opacity-10">
<div class="absolute top-10 left-10 w-32 h-32 bg-white rounded-full blur-3xl"/>
<div class="absolute bottom-10 right-10 w-40 h-40 bg-white rounded-full blur-3xl"/>
</div>
<div class="max-w-4xl mx-auto relative z-10">
<!-- 标题 -->
<motion.div
:initial="{ opacity: 0, y: 30 }"
:while-in-view="{ opacity: 1, y: 0 }"
:viewport="{ once: true }"
:transition="{ duration: 0.6 }"
class="text-center mb-12"
>
<div class="inline-flex items-center gap-2 bg-white/20 backdrop-blur-sm px-4 py-2 rounded-full mb-4">
<Time :size="20"/>
<span>限时特惠</span>
</div>
<h2 class="text-3xl md:text-4xl mb-3">超值优惠方案</h2>
<p class="text-white/90 text-lg">现在报名立享课时翻倍</p>
</motion.div>
<!-- 价格卡片 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 items-center">
<!-- 原价 -->
<motion.div
:initial="{ opacity: 0, x: -30 }"
:while-in-view="{ opacity: 1, x: 0 }"
:viewport="{ once: true }"
:transition="{ duration: 0.6, delay: 0.2 }"
class="bg-white/10 backdrop-blur-md rounded-2xl p-8 border border-white/20"
>
<div class="text-center">
<p class="text-white/70 mb-2">原价套餐</p>
<div class="text-4xl mb-4">
<span class="line-through opacity-60">¥200</span>
</div>
<div class="space-y-2">
<div class="flex items-center justify-center gap-2 text-white/80">
<check :size="18"/>
<span>10节课时</span>
</div>
<div class="flex items-center justify-center gap-2 text-white/80">
<check :size="18"/>
<span>在线自习陪伴</span>
</div>
</div>
</div>
</motion.div>
<!-- 特惠 -->
<motion.div
:initial="{ opacity: 0, x: 30 }"
:while-in-view="{ opacity: 1, x: 0 }"
:viewport="{ once: true }"
:transition="{ duration: 0.6, delay: 0.4 }"
class="bg-white text-gray-800 rounded-2xl p-8 shadow-2xl relative overflow-hidden"
>
<!-- 角标 -->
<div class="absolute -top-1 -right-1">
<div
class="bg-gradient-to-br from-yellow-400 to-orange-500 text-white px-6 py-2 rounded-bl-2xl rounded-tr-2xl shadow-lg">
<span class="text-sm">限时抢购</span>
</div>
</div>
<div class="text-center">
<p class="text-orange-600 mb-2">现在购买</p>
<div class="text-5xl mb-1">
<span class="text-orange-600">¥{{activeInfo.order_amount}}</span>
</div>
<p class="text-gray-500 mb-6">享双倍课时</p>
<div class="space-y-3">
<div class="flex items-center gap-3 bg-orange-50 rounded-lg p-3">
<div class="bg-orange-500 text-white rounded-full p-1">
<check :size="18"/>
</div>
<span class="text-lg">{{ activeInfo.course_num}}节课时</span>
</div>
<div class="flex items-center gap-3 bg-orange-50 rounded-lg p-3">
<div class="bg-orange-500 text-white rounded-full p-1">
<check :size="18"/>
</div>
<span class="text-lg">专业老师陪读</span>
</div>
<div class="flex items-center gap-3 bg-orange-50 rounded-lg p-3">
<div class="bg-orange-500 text-white rounded-full p-1">
<check :size="18"/>
</div>
<span class="text-lg">实时答疑辅导</span>
</div>
</div>
<!-- <div-->
<!-- class="mt-6 bg-gradient-to-r from-orange-400 to-red-500 text-white py-2 px-4 rounded-full inline-block">-->
<!-- <span>¥ · 仅限今日</span>-->
<!-- </div>-->
</div>
</motion.div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,100 @@
<script setup lang="ts">
import {ref} from 'vue'
import {motion} from 'motion-v'
import {Star} from "@icon-park/vue-next"
interface Review {
name: string
avatar: string
rating: number
comment: string
}
const reviews = ref<Review[]>([
{
name: '李妈妈',
avatar: 'https://images.unsplash.com/photo-1561065533-316e3142d586?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx5b3VuZyUyMHN0dWRlbnQlMjBwb3J0cmFpdHxlbnwxfHx8fDE3NjExOTU1NzZ8MA&ixlib=rb-4.1.0&q=80&w=1080',
rating: 5,
comment: '孩子在有光自习室学习后,专注力明显提升,作业效率也提高了很多!'
},
{
name: '王同学',
avatar: 'https://images.unsplash.com/photo-1514355315815-2b64b0216b14?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhc2lhbiUyMHN0dWRlbnQlMjBoYXBweXxlbnwxfHx8fDE3NjEyMjc3MzR8MA&ixlib=rb-4.1.0&q=80&w=1080',
rating: 5,
comment: '老师很负责,遇到问题可以随时提问,学习氛围特别好,推荐!'
},
{
name: '张爸爸',
avatar: 'https://images.unsplash.com/photo-1585432959389-67f059cf1e41?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0ZWVuYWdlciUyMHN0dWR5aW5nfGVufDF8fHx8MTc2MTI3MzgwMnww&ixlib=rb-4.1.0&q=80&w=1080',
rating: 5,
comment: '性价比超高20节课才200元老师专业又耐心孩子很喜欢'
},
{
name: '陈同学',
avatar: 'https://images.unsplash.com/photo-1561065533-316e3142d586?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx5b3VuZyUyMHN0dWRlbnQlMjBwb3J0cmFpdHxlbnwxfHx8fDE3NjExOTU1NzZ8MA&ixlib=rb-4.1.0&q=80&w=1080',
rating: 5,
comment: '和优秀的同学一起学习,感觉自己也变得更加努力了,很有动力!'
}
])
</script>
<template>
<div class="py-16 px-4 bg-white">
<div class="max-w-6xl mx-auto">
<!-- 标题区 -->
<motion.div
:initial="{ opacity: 0, y: 30 }"
:while-in-view="{ opacity: 1, y: 0 }"
:viewport="{ once: true }"
:transition="{ duration: 0.6 }"
class="text-center mb-10"
>
<h2 class="text-3xl md:text-4xl mb-3 text-gray-800">学员真实反馈</h2>
<p class="text-gray-600">数千家长和学员的共同选择</p>
</motion.div>
<!-- 移动端横向滚动 -->
<div class="md:hidden overflow-x-auto pb-4 -mx-4 px-4">
<div class="flex gap-4" style="width: max-content;">
<motion.div
v-for="(review, index) in reviews"
:key="index"
:initial="{ opacity: 0, y: 30 }"
:while-in-view="{ opacity: 1, y: 0 }"
:viewport="{ once: true }"
:transition="{ duration: 0.5, delay: index * 0.1 }"
class="w-[300px] flex-shrink-0 bg-gradient-to-br from-orange-50 to-white rounded-2xl p-6 shadow-lg border border-orange-100 hover:shadow-xl transition-shadow"
>
<div class="flex items-center gap-4 mb-4">
<div class="w-12 h-12 rounded-full overflow-hidden border-2 border-orange-300">
<img
:src="review.avatar"
:alt="review.name"
class="w-full h-full object-cover"
/>
</div>
<div class="flex-1">
<h4 class="text-gray-800">{{ review.name }}</h4>
<div class="flex gap-1">
<Star
theme="filled"
v-for="i in review.rating"
:key="i"
size="14"
class="fill-orange-400 text-orange-400"
/>
</div>
</div>
</div>
<p class="text-gray-700 leading-relaxed">{{ review.comment }}</p>
</motion.div>
</div>
</div>
<div class="md:hidden text-center mt-4 text-gray-400 text-sm">
滑动查看更多评价
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,34 @@
<template>
<div class="flex flex-col items-center">
<!-- motion-v <motion.div> 实现数字翻转动画 -->
<motion.div
:key="value"
:initial="{ scale: 1.2, opacity: 0.5 }"
:animate="{ scale: 1, opacity: 1 }"
:transition="{ duration: 0.3 }"
class="bg-gradient-to-br from-orange-500 to-orange-600
text-white text-3xl md:text-4xl
w-16 h-16 md:w-20 md:h-20
rounded-xl flex items-center justify-center shadow-lg"
>
{{ formatted }}
</motion.div>
<span class="text-sm mt-2 text-white/80">{{ label }}</span>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { motion } from 'motion-v'
/* -------- 类型 & props -------- */
interface Props {
value: number
label: string
}
const props = defineProps<Props>()
/* -------- 补零格式化 -------- */
const formatted = computed(() => String(props.value).padStart(2, '0'))
</script>

View File

@@ -0,0 +1,76 @@
<script setup lang="ts">
import Hero from "./component/hero.vue";
import CourseHighlights from "./component/course-highlights.vue";
import PriceSection from "@/views/activity/detail/component/price-section.vue";
import Reviews from "@/views/activity/detail/component/reviews.vue";
import {createOrderApi, getActiveInfoApi} from "@/api/activity";
import {provide, ref} from "vue";
import {showLoadingToast, showToast} from "vant";
import {wxPay} from "@/wx/wxPay";
import {useRouter} from "vue-router";
const router = useRouter()
//活动信息
const activeInfo = ref<any>({})
const loading = ref(false)
const isEnded = ref<boolean>(false)
const initData = async () => {
let res = await getActiveInfoApi()
activeInfo.value = res
loading.value = true
//判断活动结束
const endTime = new Date(activeInfo.value.active_end_at).getTime();
const status = activeInfo.value.active_status;
// 如果状态为已结束2或结束时间早于当前时间直接结束
if (status != 1 || endTime <= Date.now()) {
isEnded.value = true;
}
}
initData()
provide("activeInfo", activeInfo)
//点击支付
const handPay = async () => {
showLoadingToast({
duration: 0,
forbidClick: true,
message: '支付中'
})
let res = await createOrderApi()
wxPay(res).then((state) => {
if (state) {
showToast("支付成功")
router.push({
path: "/pay_success",
query: activeInfo.value
})
}
})
}
</script>
<template>
<div class="min-h-screen bg-white" v-if="loading">
<Hero :end="isEnded"/>
<CourseHighlights/>
<PriceSection/>
<Reviews/>
<div class="h-24"/>
<div
v-if="!isEnded"
@click="handPay"
class="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 shadow-2xl z-50 px-4 py-3 safe-area-bottom">
<div
class="flex-1 flex items-center justify-center gap-2 bg-gradient-to-r from-orange-500 to-orange-600 text-white px-8 py-4 rounded-full hover:from-orange-600 hover:to-orange-700 transition-all shadow-lg">
<span class="text-lg">立即参与</span>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,100 @@
<script setup lang="ts">
import {CheckOne, Time} from "@icon-park/vue-next"
import {useRoute} from "vue-router";
const route = useRoute()
const activeInfo = route.query as any
</script>
<template>
<div class="min-h-screen bg-gradient-to-b from-amber-50 to-orange-50 flex items-center justify-center p-4">
<div class="w-full max-w-md">
<!-- Success Icon-->
<div class="flex flex-col items-center mb-8">
<div class="relative">
<div class="absolute inset-0 bg-green-400 rounded-full blur-xl opacity-30 animate-pulse"></div>
<check-one theme="outline" size="80" class="w-20 h-20 text-green-500 relative z-10"/>
</div>
<h1 class="mt-6 text-center text-green-600">支付成功</h1>
<p class="text-center text-muted-foreground mt-2">
恭喜您成功开启有光自习室学习之旅
</p>
</div>
<!-- Order Details Card-->
<div
class="bg-card text-card-foreground flex flex-col gap-6 rounded-xl border mb-6 border-amber-200 bg-white shadow-lg">
<div class="px-6 [&:last-child]:pb-6 pt-6">
<div class="flex items-center justify-between mb-4 pb-4 border-b border-gray-200 ">
<span class="text-gray-500">订单详情</span>
<span class="text-green-600">已支付</span>
</div>
<div class="space-y-4">
<div class="flex items-start gap-3">
<div class="p-2 bg-amber-100 rounded-lg">
<Time class="w-5 h-5 text-amber-600"/>
</div>
<div class="flex-1">
<div class="flex items-center gap-2">
<span>有光自习室课程</span>
<span class="px-2 py-0.5 bg-red-500 text-white text-sm rounded">限时优惠</span>
</div>
<div class="mt-1">
<span class="text-amber-600">{{ activeInfo.course_num}}课时</span>
</div>
</div>
</div>
<div class="flex items-center justify-between pt-4 border-t border-gray-200 ">
<span class="text-gray-500">实付金额</span>
<div class="flex items-baseline gap-1">
<span class="text-sm"></span>
<span class="text-2xl text-red-500">{{activeInfo.order_amount}}</span>
</div>
</div>
</div>
</div>
</div>
<!-- WeChat QR Code Section-->
<div
class="bg-card text-card-foreground flex flex-col gap-6 rounded-xl border mb-6 border-amber-200 bg-white shadow-lg">
<div class="px-6 [&:last-child]:pb-6 pt-6">
<div class="flex items-center justify-center gap-2 mb-4">
<h3 class="text-center text-amber-900">添加客服微信立即预约上课</h3>
</div>
<div class="flex flex-col items-center">
<div class="bg-white p-4 rounded-xl shadow-md border-2 border-amber-100">
<img src="https://images.unsplash.com/photo-1604357209793-fca5dca89f97?w=300&h=300&fit=crop"
class="w-48 h-48 object-cover"/>
</div>
</div>
<p class="text-center text-muted-foreground mt-4 text-sm">
长按识别二维码
</p>
<p class="text-center mt-1">
添加专属客服微信
</p>
<div class="mt-4 p-3 bg-amber-100 rounded-lg w-full">
<p class="text-sm text-amber-900 text-center">
💡 客服将为您安排课程时间匹配专业老师
</p>
</div>
</div>
</div>
<div className="mt-6 text-center">
<p className="text-sm text-muted-foreground">
如有任何问题请及时联系客服
</p>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
</style>

238
src/views/invite/accept.vue Normal file
View File

@@ -0,0 +1,238 @@
<script setup lang="ts">
import {Star, Avatar, Tips, ChartLine, Peoples, Calendar, Time} from "@icon-park/vue-next"
import {useRoute} from "vue-router";
import {ref} from "vue";
import {bindInviteRelationApi, getInviteDetailApi} from "@/api/invite";
const route = useRoute()
const isError = ref<boolean>(false)
//邀请人信息
let inviteInfo = ref<any>({})
//显示弹窗
let show = ref<boolean>(false)
const initDta = () => {
let code = (route.query.code || "") as string
getInviteDetailApi(code).then((res) => {
inviteInfo.value = res
// 自动绑定
bindInviteRelationApi(code).then()
}).catch(() => {
isError.value = true
})
}
initDta()
</script>
<template>
<div class="relative bg-white" v-if="!isError">
<!-- 主要内容区域 -->
<main class="pt-16 pb-24 px-4">
<!-- 标题与邀请信息 -->
<div class="text-center mb-6">
<h1 class="text-2xl font-bold text-gray-900 mb-2">有光自习室 <span
class="text-primary">免费体验课</span></h1>
<p class="text-gray-600 text-sm">好友 <span
class="font-medium text-gray-900">{{ inviteInfo.user_name }}</span> 邀请您免费体验
1 </p>
</div>
<!-- 课程亮点 -->
<div class="mb-6">
<h2 class="text-lg font-semibold text-gray-900 mb-3">课程亮点</h2>
<div class="grid grid-cols-2 gap-3">
<div class="feature-icon p-3 rounded-xl flex flex-col items-center">
<avatar theme="filled" class=" text-xl mb-2 text-blue-600"/>
<span class="text-xs text-gray-700 text-center">专业讲师陪伴</span>
</div>
<div class="feature-icon p-3 rounded-xl flex flex-col items-center">
<tips theme="filled" class=" text-xl mb-2 text-blue-600"/>
<span class="text-xs text-gray-700 text-center">高效学习氛围</span>
</div>
<div class="feature-icon p-3 rounded-xl flex flex-col items-center">
<chart-line theme="filled" class=" text-xl mb-2 text-blue-600"/>
<span class="text-xs text-gray-700 text-center">专注力提升</span>
</div>
<div class="feature-icon p-3 rounded-xl flex flex-col items-center">
<peoples theme="filled" class=" text-xl mb-2 text-blue-600"/>
<span class="text-xs text-gray-700 text-center">同伴互助学习</span>
</div>
</div>
</div>
<!-- 课程时间安排 -->
<div class="course-card rounded-2xl p-4 mb-6">
<h2 class="text-lg font-semibold text-gray-900 mb-3">课程时间安排</h2>
<div class="flex items-start mb-3">
<calendar theme="filled" class="mt-1 mr-2 text-blue-600"/>
<div>
<p class="text-gray-900 font-medium">周一 周五</p>
<p class="text-gray-600 text-sm">晚上 18:30 - 20:30</p>
</div>
</div>
<div class="flex items-start">
<Time theme="filled" class="mt-1 mr-2 text-blue-600"/>
<p class="text-gray-600 text-sm">课程时长60 分钟</p>
</div>
</div>
<!-- 讲师介绍 -->
<div class="instructor-card rounded-2xl p-4 mb-6">
<h2 class="text-lg font-semibold text-gray-900 mb-3">讲师介绍</h2>
<div class="flex items-center">
<img src="https://ai-public.mastergo.com/ai/img_res/4cd9b75536d72a09e7781386096f3207.jpg"
alt="讲师头像"
class="w-16 h-16 rounded-full object-cover mr-3">
<div>
<h3 class="font-medium text-gray-900">小邱老师</h3>
<p class="text-sm text-gray-600">5 年青少年学习指导经验</p>
<p class="text-xs text-gray-500 mt-1">擅长专注力训练与学习习惯培养</p>
</div>
</div>
</div>
<!-- 用户反馈 -->
<div class="mb-6">
<h2 class="text-lg font-semibold text-gray-900 mb-3">家长评价</h2>
<div class="grid grid-cols-1 gap-4">
<div class="feedback-card bg-white rounded-2xl p-4">
<div class="flex items-center mb-2">
<img src="https://ai-public.mastergo.com/ai/img_res/ae00aa53ed5b0b1ea3615a216625bd8d.jpg"
alt="用户头像" class="w-8 h-8 rounded-full object-cover mr-2">
<div>
<h4 class="text-sm font-medium text-gray-900">张妈妈</h4>
<div class="flex text-yellow-400">
<Star
theme="filled"
v-for="i in 5"
:key="i"
size="14"
/>
</div>
</div>
</div>
<p class="text-xs text-gray-600">孩子参与后专注力明显提升老师很有耐心推荐</p>
</div>
<div class="feedback-card bg-white rounded-2xl p-4">
<div class="flex items-center mb-2">
<img src="https://ai-public.mastergo.com/ai/img_res/d29ee3e4da89879b6e075b5518fff479.jpg"
alt="用户头像" class="w-8 h-8 rounded-full object-cover mr-2">
<div>
<h4 class="text-sm font-medium text-gray-900">刘爸爸</h4>
<div class="flex text-yellow-400">
<Star
theme="filled"
v-for="i in 5"
:key="i"
size="14"
/>
</div>
</div>
</div>
<p class="text-xs text-gray-600">环境很好孩子很喜欢希望能长期参加</p>
</div>
<div class="feedback-card bg-white rounded-2xl p-4">
<div class="flex items-center mb-2">
<img src="https://ai-public.mastergo.com/ai/img_res/6c561f1786ee16855d4e72c6772e22bb.jpg"
alt="用户头像" class="w-8 h-8 rounded-full object-cover mr-2">
<div>
<h4 class="text-sm font-medium text-gray-900">陈妈妈</h4>
<div class="flex text-yellow-400">
<Star
theme="filled"
v-for="i in 5"
:key="i"
size="14"
/>
</div>
</div>
</div>
<p class="text-xs text-gray-600">自习室氛围很棒孩子在这里学习效率很高</p>
</div>
<div class="feedback-card bg-white rounded-2xl p-4">
<div class="flex items-center mb-2">
<img src="https://ai-public.mastergo.com/ai/img_res/490f9d9225d4c3afa0e0d7e20bfb27ab.jpg"
alt="用户头像" class="w-8 h-8 rounded-full object-cover mr-2">
<div>
<h4 class="text-sm font-medium text-gray-900">赵爸爸</h4>
<div class="flex text-yellow-400">
<Star
theme="filled"
v-for="i in 5"
:key="i"
size="14"
/>
</div>
</div>
</div>
<p class="text-xs text-gray-600">老师很专业孩子在这里养成了良好的学习习惯</p>
</div>
<div class="feedback-card bg-white rounded-2xl p-4">
<div class="flex items-center mb-2">
<img src="https://ai-public.mastergo.com/ai/img_res/63f58a356804b5e28a92adad9fb9c316.jpg"
alt="用户头像" class="w-8 h-8 rounded-full object-cover mr-2">
<div>
<h4 class="text-sm font-medium text-gray-900">周妈妈</h4>
<div class="flex text-yellow-400">
<Star
theme="filled"
v-for="i in 5"
:key="i"
size="14"
/>
</div>
</div>
</div>
<p class="text-xs text-gray-600">非常感谢老师的用心陪伴孩子的学习积极性提高了很多</p>
</div>
</div>
</div>
</main>
<div
class="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 shadow-2xl z-50 px-4 py-3 safe-area-bottom"
@click="show = true">
<div
class="flex-1 flex items-center justify-center gap-2 bg-gradient-to-r from-orange-500 to-orange-600 text-white px-8 py-4 rounded-full hover:from-orange-600 hover:to-orange-700 transition-all shadow-lg">
<span class="text-lg">免费体检 立即预约</span>
</div>
</div>
</div>
<van-popup v-model:show="show">
<div class="bg-white rounded-2xl w-full max-w-sm p-6 relative">
<h3 class="text-lg font-semibold text-gray-900 mb-2 text-center">预约免费体验课</h3>
<p class="text-gray-600 text-sm mb-4 text-center">长按识别二维码添加客服老师<br>预约专业老师免费体验</p>
<div class="flex justify-center mb-4">
<img src="https://ai-public.mastergo.com/ai/img_res/03641bf9e91f19c3b2f822bedd141581.jpg"
alt="客服微信二维码"
class="w-40 h-40 object-cover rounded-lg">
</div>
<p class="text-xs text-gray-500 text-center">添加时请备注免费体验课预约</p>
</div>
</van-popup>
</template>
<style scoped lang="scss">
.feedback-card {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.course-card {
background: linear-gradient(135deg, #F0F9FF 0%, #E0F2FE 100%);
}
.instructor-card {
background: linear-gradient(135deg, #F5F3FF 0%, #EDE9FE 100%);
}
.feature-icon {
background-color: #E0F2FE;
border-radius: 12px;
}
.scroll-hidden {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scroll-hidden::-webkit-scrollbar {
display: none;
}
</style>

174
src/views/invite/invite.vue Normal file
View File

@@ -0,0 +1,174 @@
<script setup lang="ts">
import {getInviteInfoApi} from "@/api/invite";
import {computed, ref} from "vue";
import {showToast} from "vant";
import {Down} from "@icon-park/vue-next"
//邀请信息
const inviteInfo = ref<any>({
invite_list: []
})
//邀请链接
const inviteLink = computed(() => {
return `${import.meta.env.VITE_WEB_URL}/accept?code=${inviteInfo.value?.invite_code}`
})
const initData = async () => {
let res = await getInviteInfoApi()
console.log(res)
inviteInfo.value = res
}
///点击复制
const handCopy = () => {
const t = document.createElement('textarea');
document.body.appendChild(t)
t.value = inviteLink.value
t.select();
document.execCommand('Copy');
document.body.removeChild(t)
showToast("复制成功")
}
initData()
</script>
<template>
<div class="relative w-full min-h-screen mx-auto overflow-x-hidden bg-white pt-4 px-4">
<section
class="mb-6 overflow-hidden whitespace-nowrap bg-yellow-50 border-l-4 border-yellow-400 p-3 rounded-r-lg shadow-sm">
<div class="scrolling-text text-sm text-yellow-700">
🎉 恭喜 用户小明成功邀请好友获得 1 节免费课时🎉 恭喜 用户小红成功邀请好友获得 1 节免费课时🎉
</div>
</section>
<!-- Title Section -->
<section class="mt-6 mb-8 text-center">
<h1 class="text-2xl font-bold text-gray-800">邀请好友得免费课时</h1>
<p class="mt-2 text-sm text-gray-600">每成功邀请一位好友即可获得 1 节免费课程</p>
</section>
<!-- Invite Link Card -->
<section class="mb-6 p-4 bg-blue-50 rounded-xl shadow-sm">
<div class="flex items-center justify-between mb-2">
<span class="text-sm font-medium text-gray-700">我的专属邀请链接</span>
<button
class="px-3 rounded bg-white border border-blue-500" @click="handCopy">
复制
</button>
</div>
<div class="p-3 bg-white rounded-lg border border-gray-200 break-all text-sm text-gray-800">
{{ inviteLink }}
</div>
</section>
<!-- Stats Section -->
<section class="flex justify-between mb-6">
<div class="text-center p-4 bg-white rounded-xl shadow-sm flex-1 mr-2">
<p class="text-sm text-gray-600">已邀请人数</p>
<p class="text-2xl font-bold text-amber-600">{{ inviteInfo?.invite_num || 0 }} </p>
</div>
<div class="text-center p-4 bg-white rounded-xl shadow-sm flex-1 ml-2">
<p class="text-sm text-gray-600">累计获得课时</p>
<p class="text-2xl font-bold text-amber-300">{{ inviteInfo?.invite_reward_num || 0 }} </p>
</div>
</section>
<!-- Invite History -->
<section class="mb-6">
<details class="group">
<summary
class="cursor-pointer p-3 bg-white rounded-t-xl shadow-sm flex justify-between items-center">
<span class="font-medium text-gray-700">查看我的邀请记录</span>
<down theme="outline"
class="fas fa-chevron-down group-open:rotate-180 transition-transform duration-200 text-gray-500"/>
</summary>
<div class="p-3 bg-white rounded-b-xl shadow-sm border-t border-gray-100">
<table class="w-full text-xs text-left text-gray-600">
<thead>
<tr class="border-b border-gray-200">
<th class="pb-2">昵称</th>
<th class="pb-2">状态</th>
<th class="pb-2">奖励</th>
</tr>
</thead>
<tbody>
<tr class="border-b border-gray-200"
v-for="(item,index) in inviteInfo.invite_list"
:key="index">
<td class="py-2">{{ item.invited_user_name }}</td>
<td >
<span class="text-gray-400" v-if="item.invite_status == 0">未预约</span>
<span class="text-green-900" v-else-if="item.invite_status == 1">已预约</span>
<span class="text-green-400" v-else-if="item.invite_status == 2">已体验</span>
<span class="text-orange-400" v-else-if="item.invite_status == 3">已被别人邀请</span>
</td>
<td class="py-2">{{ item.reward_text}}</td>
</tr>
</tbody>
</table>
</div>
</details>
</section>
<!-- Reward Claim Guide -->
<section class="mb-6 p-4 bg-white rounded-xl shadow-sm">
<h3 class="font-medium text-gray-700 mb-2">如何领取奖励</h3>
<p class="text-sm text-gray-600 leading-relaxed">
邀请好友来完成体验课后系统会自动放发奖励课时
</p>
</section>
<!-- FAQ Section -->
<section>
<h3 class="font-medium text-gray-700 mb-3">常见问题</h3>
<div class="space-y-3">
<details class="group bg-white rounded-lg shadow-sm">
<summary class="list-none cursor-pointer p-3 flex justify-between items-center">
<span class="text-sm font-medium text-gray-700">Q: 如何才算成功邀请好友</span>
<down theme="outline"
class="fas fa-chevron-down group-open:rotate-180 transition-transform duration-200 text-gray-500"/>
</summary>
<div class="px-3 pb-3 text-sm text-gray-600">
A: 好友通过您的邀请并完成首次课程学习即视为成功邀请
</div>
</details>
<details class="group bg-white rounded-lg shadow-sm">
<summary class="list-none cursor-pointer p-3 flex justify-between items-center">
<span class="text-sm font-medium text-gray-700">Q: 奖励何时到账</span>
<down theme="outline"
class="fas fa-chevron-down group-open:rotate-180 transition-transform duration-200 text-gray-500"/>
</summary>
<div class="px-3 pb-3 text-sm text-gray-600">
A: 好友满足条件后的 24 小时内奖励将自动发放到您的账户中
</div>
</details>
<details class="group bg-white rounded-lg shadow-sm">
<summary class="list-none cursor-pointer p-3 flex justify-between items-center">
<span class="text-sm font-medium text-gray-700">Q: 是否有限制次数</span>
<down theme="outline"
class="fas fa-chevron-down group-open:rotate-180 transition-transform duration-200 text-gray-500"/>
</summary>
<div class="px-3 pb-3 text-sm text-gray-600">
A: 本次活动不限制邀请人数邀请越多奖励越多
</div>
</details>
</div>
</section>
</div>
</template>
<style scoped lang="scss">
.scrolling-text {
display: inline-block;
white-space: nowrap;
animation: scroll-left 15s linear infinite;
}
@keyframes scroll-left {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(-100%);
}
}
</style>

73
src/views/system/404.vue Normal file
View File

@@ -0,0 +1,73 @@
<template>
<div class="container">
<div class="block-404">
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="960px" height="560px" viewBox="0 0 960 560" enable-background="new 0 0 960 560" xml:space="preserve" p-id="3"><path fill="#EEEEEE" d="M233.483,208.48c0,0,0.422-0.047,0.656,0.234c0.234,0.281,0.516,4.218,1.547,5.296
c1.031,1.078,5.203,0.469,5.203,1.875c0,1.125-3.877,1.246-5.189,2.324c-1.312,1.078-0.562,5.624-2.17,5.457
c-1.259-0.131-0.375-3.75-1.922-5.203s-5.296-1.172-5.296-2.484s3.843-0.516,5.156-2.015
C232.78,212.464,232.218,208.48,233.483,208.48z" p-id="4"></path><path fill="#B1AFAE" d="M376.804,222.306c0,0,0.255-0.028,0.396,0.141s0.311,2.546,0.934,3.197c0.622,0.651,3.14,0.283,3.14,1.132
c0,0.679-2.34,0.752-3.132,1.403s-0.339,3.395-1.309,3.293c-0.76-0.079-0.226-2.263-1.16-3.14c-0.934-0.877-3.197-0.707-3.197-1.499
c0-0.792,2.32-0.311,3.112-1.216C376.379,224.711,376.04,222.306,376.804,222.306z" p-id="5"></path><path fill="#EEEEEE" d="M727.965,219.204c0,0,0.289-0.032,0.45,0.161s0.353,2.891,1.06,3.63c0.707,0.739,3.566,0.321,3.566,1.285
c0,0.771-2.657,0.854-3.557,1.593c-0.9,0.739-0.385,3.855-1.487,3.74c-0.863-0.09-0.257-2.57-1.317-3.566
c-1.06-0.996-3.63-0.803-3.63-1.703s2.634-0.353,3.534-1.381C727.483,221.935,727.098,219.204,727.965,219.204z" p-id="6"></path><path fill="#B1AFAE" d="M594.571,211.794c0,0,0.289-0.032,0.45,0.161s0.353,2.891,1.06,3.63c0.707,0.739,3.566,0.321,3.566,1.285
c0,0.771-2.657,0.854-3.557,1.593c-0.9,0.739-0.386,3.855-1.487,3.74c-0.863-0.09-0.257-2.57-1.317-3.566
c-1.06-0.996-3.63-0.803-3.63-1.703s2.634-0.353,3.534-1.381C594.089,214.525,593.704,211.794,594.571,211.794z" p-id="7"></path><circle fill="#EEEEEE" cx="401.1" cy="183.965" r="4.203" p-id="8"></circle><circle fill="#EEEEEE" cx="732.584" cy="299.094" r="2.634" p-id="9"></circle><circle fill="#FFFFFF" cx="469.913" cy="293.441" r="86.028" p-id="10"></circle><path fill="#EEEEEE" d="M469.913,379.469c28.044,0,52.877-13.696,68.215-34.751l-39.807-94.67h-27.307h-2.204h-27.307l-39.807,94.67
C417.036,365.774,441.869,379.469,469.913,379.469z" p-id="11"></path><path fill="#FDFDFD" d="M469.913,379.469c20.08,0,38.512-7.028,52.997-18.742l-30.408-109.613h-21.713h-1.753h-21.713
l-30.408,109.613C431.401,372.441,449.832,379.469,469.913,379.469z" p-id="12"></path><path fill="#EEEEEE" d="M470.315,251.114h-0.804h-9.959l-14.518,123.922c0.051,0.244,0.105,0.481,0.156,0.723
c8.203,2.964,15.897,3.71,24.724,3.71c8.827,0,16.479-0.079,24.724-3.71c0.052-0.242,0.105-0.48,0.156-0.723l-14.518-123.922
H470.315z" p-id="13"></path><path fill="#D8D8D8" d="M513.965,229.357c-0.322-23.854-19.748-43.094-43.678-43.094c-23.93,0-43.356,19.24-43.677,43.094H513.965z" p-id="14"></path><rect x="426.595" y="229.133" fill="#FFFFFF" width="87.385" height="7.182" p-id="15"></rect><path fill="#D8D8D8" d="M518.769,236.296h-96.962c-5.454,0-9.876,4.422-9.876,9.876s4.421,9.876,9.876,9.876h8.875
c-0.001,0.032-0.009,0.061-0.009,0.094v3.367c0,2.107,1.708,3.816,3.816,3.816s3.816-1.708,3.816-3.816v-3.367
c0-0.032-0.009-0.062-0.009-0.094h63.987c-0.001,0.032-0.009,0.061-0.009,0.094v3.367c0,2.107,1.708,3.816,3.816,3.816
s3.816-1.708,3.816-3.816v-3.367c0-0.032-0.009-0.062-0.009-0.094h8.875c5.454,0,9.876-4.422,9.876-9.876
S524.223,236.296,518.769,236.296z" p-id="16"></path><g p-id="17"><circle fill="#FDFDFD" cx="450.404" cy="246.415" r="3.964" p-id="18"></circle><circle fill="#FDFDFD" cx="470.288" cy="246.415" r="3.964" p-id="19"></circle><circle fill="#FDFDFD" cx="490.172" cy="246.415" r="3.964" p-id="20"></circle></g><g p-id="21"><g p-id="22"><path fill="#D8D8D8" d="M352.619,327.549h-17.822v38.158h-17.103v-38.158h-64.989v-12.055l61.659-96.818h20.433v94.161h17.822
V327.549z M317.694,312.837v-61.604c0-4.366,0.12-9.396,0.36-15.092h-0.36c-0.841,2.405-2.61,6.17-5.311,11.295l-41.496,65.4
H317.694z" p-id="23"></path></g><g p-id="24"><path fill="#D8D8D8" d="M687.12,327.549h-17.822v38.158h-17.103v-38.158h-64.989v-12.055l61.659-96.818h20.433v94.161h17.822
V327.549z M652.195,312.837v-61.604c0-4.366,0.12-9.396,0.36-15.092h-0.36c-0.841,2.405-2.61,6.17-5.311,11.295l-41.496,65.4
H652.195z" p-id="25"></path></g></g></svg>
<div class="msg">404抱歉您查看的页面不存在<router-link to="/">去首页</router-link></div>
</div>
</div>
</template>
<script setup>
console.log(import.meta.env)
</script>
<style scoped lang="scss">
.container {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
justify-content: center;
background-color: #f4f5f7;
.block-404 {
padding-top: 20%;
color: #666;
font-size: 14px;
text-align: center;
svg {
width: 70vw;
height: 70vw;
}
.msg {
margin-top: -25%;
white-space: nowrap;
a {
color: #1989fa;
&:active {
color: #0456a9;
}
}
}
}
}
</style>

49
src/wx/wxLogin.ts Normal file
View File

@@ -0,0 +1,49 @@
import qs from "qs"
import {Login} from "@/api/wxchat";
import {useUserStore} from "@/stores";
/**
* snsapi_base 不弹出授权页面直接跳转只能获取用户openid
* snsapi_userinfo 弹出授权页面可通过openid拿到昵称、性别、所在地。并且 即使在未关注的情况下,只要用户授权,也能获取其信息
*/
const scope: Array<string> = ['snsapi_base', 'snsapi_userinfo']
const appid = import.meta.env.VITE_APPID
let locationUrl: string = window.location.href
let webUrl: string = locationUrl.split("?")[0]
export default function (): Promise<boolean> {
const store = useUserStore()
return new Promise((resolve) => {
if (store.token) {
resolve(true)
return
}
let query = qs.parse(locationUrl.split('?')[1])
if (query.code) {
Login({
wx_code: query.code,
...query
}).then(res => {
if (!res.accessToken) {
delete query.code
delete query.state
webUrl = webUrl + qs.stringify(query)
hrefLogin(webUrl)
return
}
store.token = res.accessToken
store.userInfo = res.userInfo
resolve(true)
})
} else {
hrefLogin(locationUrl)
}
})
}
//防止登录时多次在url沙上添加code
function hrefLogin(redirectUrl: string) {
redirectUrl = encodeURIComponent(redirectUrl)
window.location.href = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirectUrl}&response_type=code&scope=${scope[1]}#wechat_redirect`
}

23
src/wx/wxPay.ts Normal file
View File

@@ -0,0 +1,23 @@
import wx from "weixin-js-sdk"
//打开支付,会返回回调1为支付成功2为取消支付
export function wxPay(payInfoConfig: any): Promise<boolean> {
return new Promise((resolve) => {
wx.chooseWXPay({
timestamp: payInfoConfig['timestamp'], // 支付签名时间戳注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
nonceStr: payInfoConfig['nonceStr'], // 支付签名随机串,不长于 32 位
package: payInfoConfig['package'], // 统一支付接口返回的prepay_id参数值提交格式如prepay_id=\*\*\*
signType: payInfoConfig['signType'], // 微信支付V3的传入RSA,微信支付V2的传入格式与V2统一下单的签名格式保持一致
paySign: payInfoConfig['paySign'], // 支付签名
success: function () {
resolve(true)
},
cancel: function (err: any) {
console.log(err)
resolve(false)
}
})
})
}

85
src/wx/wxShare.ts Normal file
View File

@@ -0,0 +1,85 @@
import wx from "weixin-js-sdk"
import {getJsSdk} from "@/api/wxchat";
import qs from "qs"
import {type RouteMetaType} from "@/types/route-type";
export default async function (meta: RouteMetaType) {
let isWeXin = navigator.userAgent.toLowerCase().indexOf("micromessenger") !== -1
let query = qs.parse(window.location.search.split('?')[1])
let isShare = !meta.noShare
delete query.code
//默认分享路径
let defaultHref = window.location.origin + window.location.pathname + '?' + qs.stringify(query)
if (isShare && isWeXin) {
let data = await getJsSdk({
url: window.location.href
})
const shareDataObj = {
title: meta?.shareData?.title || '备考不孤单抱抱APP陪伴冲刺',
desc: meta?.shareData?.desc || '中高考家庭心理轻测评平台亲子双端AI测评+专业心理师,早一步减压,多一份把握',
link: meta?.shareData?.link || defaultHref,
imgUrl: meta?.shareData?.imgUrl || 'https://keyang2.tuzuu.com/lingting/logo.jpg'
}
wx.config({
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来若要查看传入的参数可以在pc端打开参数信息会通过log打出仅在pc端时才会打印。
appId: data.jssdk.appId, // 必填,公众号的唯一标识
timestamp: data.jssdk.timestamp, // 必填,生成签名的时间戳
nonceStr: data.jssdk.nonceStr, // 必填,生成签名的随机串
signature: data.jssdk.signature,// 必填,签名
jsApiList: data.jssdk.jsApiList, // 必填需要使用的JS接口列表
openTagList: ['wx-open-launch-weapp']
})
wx.ready(function () {
if (wx.updateAppMessageShareData) {
wx.updateAppMessageShareData({
title: shareDataObj.title,
desc: shareDataObj.desc,
link: shareDataObj.link,
imgUrl: shareDataObj.imgUrl,
success: () => {
console.log('分享朋友成功')
},
})
}
if (wx.updateTimelineShareData) {
wx.updateTimelineShareData({
title: shareDataObj.title,
link: shareDataObj.link,
imgUrl: shareDataObj.imgUrl,
success: () => {
console.log('分享朋友圈')
},
})
}
if (wx.onMenuShareAppMessage) {
wx.onMenuShareAppMessage({
title: shareDataObj.title,
desc: shareDataObj.desc,
link: shareDataObj.link,
imgUrl: shareDataObj.imgUrl,
success: () => {
console.log('旧版本分享朋友成功')
},
cancel: () => {
}
})
}
if (wx.onMenuShareTimeline) {
wx.onMenuShareTimeline({
title: shareDataObj.title,
link: shareDataObj.link,
imgUrl: shareDataObj.imgUrl,
success: () => {
console.log('旧版本分享朋友圈')
},
cancel: () => {
}
})
}
})
}
}