1
This commit is contained in:
2
.env.development
Normal file
2
.env.development
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_APPID=wxbc438492e3efab70 #appid
|
||||
VITE_WEB_URL=https://baobao.cells.org.cn #后端接口
|
||||
2
.env.production
Normal file
2
.env.production
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_APPID=wxbc438492e3efab70 #appid
|
||||
VITE_WEB_URL=https://baobao.cells.org.cn #后端接口
|
||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
*dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
16
README.md
Normal file
16
README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar)
|
||||
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
|
||||
|
||||
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, Take Over mode will enable itself if the default TypeScript extension is disabled.
|
||||
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).
|
||||
17
components.d.ts
vendored
Normal file
17
components.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// biome-ignore lint: disable
|
||||
// oxlint-disable
|
||||
// ------
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
}
|
||||
14
index.html
Normal file
14
index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="icon" href="/favicon.ico"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Vite App</title>
|
||||
<link href="/src/styles/bg.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
35
package.json
Normal file
35
package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "vue3-wxchat",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build --mode development",
|
||||
"prod": "vute build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@icon-park/vue-next": "^1.4.2",
|
||||
"@tabler/icons-vue": "^3.35.0",
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"axios": "^1.12.2",
|
||||
"motion-v": "^1.7.4",
|
||||
"pinia": "^2.1.4",
|
||||
"pinia-plugin-persistedstate": "^4.2.0",
|
||||
"qs": "^6.14.0",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"vant": "^4.9.21",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"weixin-js-sdk": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/qs": "^6.14.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"sass": "^1.83.4",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.5",
|
||||
"vue-tsc": "^0.34.7"
|
||||
}
|
||||
}
|
||||
2030
pnpm-lock.yaml
generated
Normal file
2030
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
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: () => {
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
28
tsconfig.app.json
Normal file
28
tsconfig.app.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler", // ✅ 确保是这个
|
||||
"allowImportingTsExtensions": false,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"skipLibCheck": true, // ✅ 建议加上
|
||||
"types": ["vite/client"],
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "vite.config.ts"] // ✅ 把 vite.config.ts 也加进来
|
||||
}
|
||||
6
tsconfig.json
Normal file
6
tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
]
|
||||
}
|
||||
28
vite.config.ts
Normal file
28
vite.config.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from "path"
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
const pathResolve = (dir: string): any => {
|
||||
return resolve(__dirname, '.', dir)
|
||||
}
|
||||
const alias: Record<string, string> = {
|
||||
"@": pathResolve("src")
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
vue(),
|
||||
],
|
||||
resolve: {
|
||||
alias
|
||||
},
|
||||
build: {
|
||||
outDir: './dist'
|
||||
},
|
||||
// 开发服务配置
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user