1
This commit is contained in:
13
src/App.vue
Normal file
13
src/App.vue
Normal 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
15
src/api/activity.ts
Normal 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
22
src/api/invite.ts
Normal 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
15
src/api/wxchat.ts
Normal 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
19
src/env.d.ts
vendored
Normal 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
15
src/main.ts
Normal 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
53
src/plugin/vant.ts
Normal 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
29
src/router/index.ts
Normal 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
|
||||
25
src/router/modules/activity.ts
Normal file
25
src/router/modules/activity.ts
Normal 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
|
||||
35
src/router/modules/base.ts
Normal file
35
src/router/modules/base.ts
Normal 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
|
||||
12
src/router/modules/index.ts
Normal file
12
src/router/modules/index.ts
Normal 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
10
src/stores/index.ts
Normal 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
|
||||
30
src/stores/modules/user.ts
Normal file
30
src/stores/modules/user.ts
Normal 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
1
src/styles/bg.css
Normal file
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
9
src/types/index.d.ts
vendored
Normal file
9
src/types/index.d.ts
vendored
Normal 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
37
src/types/route-type.ts
Normal 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
24
src/utils/format.ts
Normal 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
13
src/utils/http/error.ts
Normal 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
55
src/utils/http/request.ts
Normal 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)
|
||||
})
|
||||
|
||||
|
||||
//封装post,get
|
||||
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
|
||||
57
src/views/activity/detail/component/course-highlights.vue
Normal file
57
src/views/activity/detail/component/course-highlights.vue
Normal 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>
|
||||
121
src/views/activity/detail/component/hero.vue
Normal file
121
src/views/activity/detail/component/hero.vue
Normal 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>
|
||||
117
src/views/activity/detail/component/price-section.vue
Normal file
117
src/views/activity/detail/component/price-section.vue
Normal 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>
|
||||
100
src/views/activity/detail/component/reviews.vue
Normal file
100
src/views/activity/detail/component/reviews.vue
Normal 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>
|
||||
34
src/views/activity/detail/component/time-unit.vue
Normal file
34
src/views/activity/detail/component/time-unit.vue
Normal 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>
|
||||
76
src/views/activity/detail/index.vue
Normal file
76
src/views/activity/detail/index.vue
Normal 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>
|
||||
|
||||
100
src/views/activity/success/index.vue
Normal file
100
src/views/activity/success/index.vue
Normal 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
238
src/views/invite/accept.vue
Normal 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
174
src/views/invite/invite.vue
Normal 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
73
src/views/system/404.vue
Normal 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
49
src/wx/wxLogin.ts
Normal 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
23
src/wx/wxPay.ts
Normal 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
85
src/wx/wxShare.ts
Normal 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: () => {
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user