Initial commit: 全友米项目完整代码
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# 高德地图API Key
|
||||
VITE_AMAP_KEY=your_amap_key_here
|
||||
|
||||
# 高德地图安全密钥(securityJsCode)
|
||||
# 获取方式:登录 https://console.amap.com/ -> 应用管理 -> 查看应用详情 -> 安全密钥
|
||||
VITE_AMAP_SECURITY_CODE=your_security_code_here
|
||||
|
||||
# API基础地址
|
||||
VITE_API_BASE_URL=/api
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>泉友米车友会</title>
|
||||
<script>
|
||||
// 运行时配置(可以通过环境变量注入)
|
||||
window.AMAP_CONFIG = {
|
||||
key: 'YOUR_AMAP_KEY_PLACEHOLDER',
|
||||
securityCode: 'YOUR_AMAP_SECURITY_CODE_PLACEHOLDER'
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,201 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>高德地图搜索测试</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
#container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
margin: 20px 0;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.search-box {
|
||||
margin: 20px 0;
|
||||
}
|
||||
input {
|
||||
padding: 10px;
|
||||
width: 300px;
|
||||
font-size: 14px;
|
||||
}
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.results {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.result-item {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
cursor: pointer;
|
||||
}
|
||||
.result-item:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.log {
|
||||
background: #f5f5f5;
|
||||
padding: 10px;
|
||||
margin-top: 20px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>高德地图搜索功能测试</h1>
|
||||
|
||||
<div class="search-box">
|
||||
<input type="text" id="keyword" placeholder="输入地点名称,如:泉州市政府" />
|
||||
<button onclick="searchPlace()">搜索</button>
|
||||
</div>
|
||||
|
||||
<div id="container"></div>
|
||||
|
||||
<div class="results" id="results"></div>
|
||||
|
||||
<div class="log" id="log"></div>
|
||||
|
||||
<script>
|
||||
// 替换为你的高德地图Key和安全密钥
|
||||
const AMAP_KEY = 'fd5b77136ba34fe6b4f0f86dca7782d9';
|
||||
const SECURITY_CODE = 'b2e67bb4d5eed8848f0b8aae6ca667fc';
|
||||
|
||||
let map = null;
|
||||
let placeSearch = null;
|
||||
let marker = null;
|
||||
|
||||
function log(message) {
|
||||
const logDiv = document.getElementById('log');
|
||||
const time = new Date().toLocaleTimeString();
|
||||
logDiv.innerHTML += `[${time}] ${message}<br>`;
|
||||
logDiv.scrollTop = logDiv.scrollHeight;
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
function initMap() {
|
||||
log('开始初始化地图...');
|
||||
|
||||
map = new AMap.Map('container', {
|
||||
zoom: 13,
|
||||
center: [118.6762, 24.8742] // 泉州市中心
|
||||
});
|
||||
|
||||
log('地图初始化成功');
|
||||
|
||||
// 初始化PlaceSearch
|
||||
AMap.plugin(['AMap.PlaceSearch'], function() {
|
||||
placeSearch = new AMap.PlaceSearch({
|
||||
pageSize: 10,
|
||||
pageIndex: 1,
|
||||
city: '泉州',
|
||||
citylimit: false,
|
||||
extensions: 'base'
|
||||
});
|
||||
log('PlaceSearch插件加载成功');
|
||||
log('配置信息:city=泉州, citylimit=false, extensions=base');
|
||||
});
|
||||
}
|
||||
|
||||
function searchPlace() {
|
||||
const keyword = document.getElementById('keyword').value.trim();
|
||||
|
||||
if (!keyword) {
|
||||
alert('请输入搜索关键词');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!placeSearch) {
|
||||
log('错误:PlaceSearch未初始化');
|
||||
alert('地图搜索功能未就绪,请刷新页面重试');
|
||||
return;
|
||||
}
|
||||
|
||||
log(`开始搜索:${keyword}`);
|
||||
|
||||
placeSearch.search(keyword, function(status, result) {
|
||||
log(`搜索状态:${status}`);
|
||||
|
||||
const resultsDiv = document.getElementById('results');
|
||||
resultsDiv.innerHTML = '';
|
||||
|
||||
if (status === 'complete' && result.info === 'OK') {
|
||||
if (result.poiList && result.poiList.pois && result.poiList.pois.length > 0) {
|
||||
log(`找到 ${result.poiList.pois.length} 个结果`);
|
||||
|
||||
result.poiList.pois.forEach((poi, index) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'result-item';
|
||||
div.innerHTML = `
|
||||
<strong>${index + 1}. ${poi.name}</strong><br>
|
||||
地址:${poi.address || '无'}<br>
|
||||
坐标:${poi.location.lng}, ${poi.location.lat}
|
||||
`;
|
||||
div.onclick = () => showOnMap(poi);
|
||||
resultsDiv.appendChild(div);
|
||||
});
|
||||
} else {
|
||||
log('搜索结果为空');
|
||||
resultsDiv.innerHTML = '<p>未找到相关地点</p>';
|
||||
}
|
||||
} else if (status === 'no_data') {
|
||||
log('无搜索结果');
|
||||
resultsDiv.innerHTML = '<p>未找到相关地点</p>';
|
||||
} else {
|
||||
log(`搜索失败:${status} - ${result.info || '未知错误'}`);
|
||||
log(`完整结果:${JSON.stringify(result)}`);
|
||||
resultsDiv.innerHTML = `<p style="color: red;">搜索失败:${result.info || status}</p>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showOnMap(poi) {
|
||||
const position = [poi.location.lng, poi.location.lat];
|
||||
|
||||
if (marker) {
|
||||
marker.setPosition(position);
|
||||
} else {
|
||||
marker = new AMap.Marker({
|
||||
position: position,
|
||||
map: map
|
||||
});
|
||||
}
|
||||
|
||||
map.setCenter(position);
|
||||
map.setZoom(16);
|
||||
|
||||
log(`在地图上显示:${poi.name}`);
|
||||
}
|
||||
|
||||
// 配置安全密钥(必须在加载地图前配置)
|
||||
window._AMapSecurityConfig = {
|
||||
securityJsCode: SECURITY_CODE
|
||||
};
|
||||
|
||||
log(`配置安全密钥:${SECURITY_CODE}`);
|
||||
|
||||
// 加载高德地图脚本
|
||||
const script = document.createElement('script');
|
||||
script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}`;
|
||||
script.onload = () => {
|
||||
log('高德地图脚本加载成功');
|
||||
initMap();
|
||||
};
|
||||
script.onerror = () => {
|
||||
log('错误:高德地图脚本加载失败');
|
||||
alert('地图加载失败,请检查网络连接或API Key配置');
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1135
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "quanyoumi-frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "泉友米车友会前端",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.5",
|
||||
"vant": "^4.7.1",
|
||||
"axios": "^1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.4.0",
|
||||
"vite": "^4.5.0"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
api.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
export default {
|
||||
// 活动相关
|
||||
getActivities: (params) => api.get('/activities', { params }),
|
||||
getActivity: (id) => api.get(`/activities/${id}`),
|
||||
|
||||
// 报名相关
|
||||
submitRegistration: (data) => api.post('/registrations', data),
|
||||
getRegistration: (id) => api.get(`/registrations/${id}`),
|
||||
getActivityRegistrations: (id) => api.get(`/activities/${id}/registrations`),
|
||||
|
||||
// 用户上传照片
|
||||
uploadUserPhotos: (id, data) => api.post(`/activities/${id}/photos`, data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: 300000 // 5分钟超时
|
||||
}),
|
||||
|
||||
// 用户验证登录
|
||||
verifyUser: (activityId, data) => api.post(`/auth/verify/${activityId}`, data),
|
||||
|
||||
// 管理员相关
|
||||
adminLogin: (data) => api.post('/admin/login', data),
|
||||
createActivity: (data) => api.post('/admin/activities', data),
|
||||
updateActivity: (id, data) => api.put(`/admin/activities/${id}`, data),
|
||||
approveRegistration: (id, status) => api.put(`/admin/registrations/${id}`, { status }),
|
||||
updateRegistration: (id, data) => api.put(`/admin/registrations/${id}`, data),
|
||||
checkInRegistration: (id) => api.put(`/admin/registrations/${id}/checkin`),
|
||||
getAllRegistrations: (params) => api.get('/admin/registrations', { params }),
|
||||
addExpense: (data) => api.post('/admin/expenses', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: 60000
|
||||
}),
|
||||
getExpenses: (params) => api.get('/admin/expenses', { params }),
|
||||
updateExpense: (id, data) => api.put(`/admin/expenses/${id}`, data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: 60000
|
||||
}),
|
||||
deleteExpense: (id) => api.delete(`/admin/expenses/${id}`),
|
||||
uploadPhotos: (data) => api.post('/admin/photos', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: 300000 // 5分钟超时,适配大文件上传
|
||||
}),
|
||||
getPendingPhotos: () => api.get('/admin/photos/pending'),
|
||||
approvePhoto: (id, status) => api.put(`/admin/photos/${id}`, { status }),
|
||||
deletePhoto: (id) => api.delete(`/admin/photos/${id}`),
|
||||
addSponsor: (data) => api.post('/admin/sponsors', data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: 60000 // 1分钟超时
|
||||
}),
|
||||
updateSponsor: (id, data) => api.put(`/admin/sponsors/${id}`, data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: 60000
|
||||
}),
|
||||
deleteSponsor: (id) => api.delete(`/admin/sponsors/${id}`)
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import Vant from 'vant';
|
||||
import 'vant/lib/index.css';
|
||||
|
||||
// 全局样式 - 防止移动端输入框自动缩放
|
||||
const globalStyle = document.createElement('style');
|
||||
globalStyle.textContent = `
|
||||
/* 防止iOS输入框自动缩放 */
|
||||
input, textarea, select {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
/* 防止页面缩放 */
|
||||
* {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* 优化输入框体验 */
|
||||
input:focus, textarea:focus, select:focus {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(globalStyle);
|
||||
|
||||
// 获取高德地图配置(优先使用运行时配置,其次使用构建时环境变量)
|
||||
const getAmapKey = () => {
|
||||
if (window.AMAP_CONFIG && window.AMAP_CONFIG.key && window.AMAP_CONFIG.key !== 'YOUR_AMAP_KEY_PLACEHOLDER') {
|
||||
return window.AMAP_CONFIG.key;
|
||||
}
|
||||
return import.meta.env.VITE_AMAP_KEY;
|
||||
};
|
||||
|
||||
const getAmapSecurityCode = () => {
|
||||
if (window.AMAP_CONFIG && window.AMAP_CONFIG.securityCode && window.AMAP_CONFIG.securityCode !== 'YOUR_AMAP_SECURITY_CODE_PLACEHOLDER') {
|
||||
return window.AMAP_CONFIG.securityCode;
|
||||
}
|
||||
return import.meta.env.VITE_AMAP_SECURITY_CODE || 'b2e67bb4d5eed8848f0b8aae6ca667fc';
|
||||
};
|
||||
|
||||
// 配置高德地图安全密钥(必须在加载地图前配置)
|
||||
window._AMapSecurityConfig = {
|
||||
securityJsCode: getAmapSecurityCode()
|
||||
};
|
||||
|
||||
console.log('高德地图配置:', {
|
||||
hasKey: !!getAmapKey(),
|
||||
keySource: window.AMAP_CONFIG && window.AMAP_CONFIG.key !== 'YOUR_AMAP_KEY_PLACEHOLDER' ? 'runtime' : 'build-time',
|
||||
hasSecurityCode: !!getAmapSecurityCode()
|
||||
});
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
app.use(Vant);
|
||||
app.mount('#app');
|
||||
|
||||
// 动态加载高德地图(含插件)
|
||||
const loadAMap = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const amapKey = getAmapKey();
|
||||
|
||||
if (!amapKey || amapKey === 'YOUR_AMAP_KEY_PLACEHOLDER' || amapKey === 'YOUR_AMAP_KEY') {
|
||||
console.error('高德地图Key未配置!');
|
||||
console.error('请在 docker-compose.yml 中配置 AMAP_KEY 环境变量');
|
||||
console.error('当前 Key 值:', amapKey);
|
||||
reject(new Error('高德地图Key未配置'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.AMap) {
|
||||
// 如果已加载,检查插件是否可用
|
||||
if (window.AMap.Geocoder && window.AMap.PlaceSearch) {
|
||||
console.log('高德地图已加载,插件可用');
|
||||
resolve(window.AMap);
|
||||
} else {
|
||||
// 插件未加载,异步加载
|
||||
console.log('加载高德地图插件...');
|
||||
window.AMap.plugin(['AMap.Geocoder', 'AMap.PlaceSearch', 'AMap.AutoComplete'], () => {
|
||||
console.log('高德地图插件加载完成');
|
||||
resolve(window.AMap);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('开始加载高德地图脚本...');
|
||||
const script = document.createElement('script');
|
||||
script.src = `https://webapi.amap.com/maps?v=2.0&key=${amapKey}&plugin=AMap.Geocoder,AMap.PlaceSearch,AMap.AutoComplete`;
|
||||
script.async = true;
|
||||
script.onload = () => {
|
||||
if (window.AMap) {
|
||||
console.log('高德地图脚本加载成功');
|
||||
// 脚本加载后再确保插件加载
|
||||
window.AMap.plugin(['AMap.Geocoder', 'AMap.PlaceSearch', 'AMap.AutoComplete'], () => {
|
||||
console.log('高德地图插件初始化完成');
|
||||
resolve(window.AMap);
|
||||
});
|
||||
} else {
|
||||
console.error('高德地图加载失败:window.AMap未定义');
|
||||
reject(new Error('高德地图加载失败'));
|
||||
}
|
||||
};
|
||||
script.onerror = (error) => {
|
||||
console.error('高德地图脚本加载失败:', error);
|
||||
reject(new Error('高德地图脚本加载失败'));
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
};
|
||||
|
||||
// 导出加载函数供其他组件使用
|
||||
window.loadAMap = loadAMap;
|
||||
|
||||
// 预加载地图
|
||||
loadAMap().catch(err => {
|
||||
console.error('地图预加载失败:', err);
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('../views/Home.vue')
|
||||
},
|
||||
{
|
||||
path: '/landing',
|
||||
name: 'LandingPage',
|
||||
component: () => import('../views/LandingPage.vue')
|
||||
},
|
||||
{
|
||||
path: '/activity/:id',
|
||||
name: 'ActivityDetail',
|
||||
component: () => import('../views/ActivityDetail.vue')
|
||||
},
|
||||
{
|
||||
path: '/register/:id',
|
||||
name: 'Register',
|
||||
component: () => import('../views/Register.vue')
|
||||
},
|
||||
{
|
||||
path: '/share/:id',
|
||||
name: 'SharePage',
|
||||
component: () => import('../views/SharePage.vue')
|
||||
},
|
||||
{
|
||||
path: '/admin/login',
|
||||
name: 'AdminLogin',
|
||||
component: () => import('../views/admin/Login.vue')
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
name: 'Admin',
|
||||
component: () => import('../views/admin/Dashboard.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/activity/:id',
|
||||
name: 'ActivityManage',
|
||||
component: () => import('../views/admin/ActivityManage.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.meta.requiresAuth && !localStorage.getItem('token')) {
|
||||
next('/admin/login');
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<div class="hero">
|
||||
<van-swipe class="hero-swipe" :autoplay="3000" indicator-color="white">
|
||||
<van-swipe-item v-for="(image, index) in bannerImages" :key="index">
|
||||
<div class="swipe-item" :style="{ backgroundImage: `url(${image})` }"></div>
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
<div class="hero-content">
|
||||
<h1 class="club-name">泉友米车友会</h1>
|
||||
<p class="club-slogan">驾驭激情 · 共享旅程</p>
|
||||
<van-button
|
||||
type="primary"
|
||||
round
|
||||
size="small"
|
||||
@click="$router.push('/landing')"
|
||||
class="landing-btn"
|
||||
>
|
||||
查看宣传页
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<van-tabs v-model:active="activeTab" @change="loadActivities" class="custom-tabs">
|
||||
<van-tab title="精彩活动" name="upcoming"></van-tab>
|
||||
<van-tab title="往期回顾" name="completed"></van-tab>
|
||||
</van-tabs>
|
||||
|
||||
<div class="activity-list">
|
||||
<div v-if="loading" class="loading-state">
|
||||
<van-loading size="24px">加载中...</van-loading>
|
||||
</div>
|
||||
<div v-else-if="activities.length === 0" class="empty-state">
|
||||
<van-empty description="暂无活动" />
|
||||
</div>
|
||||
<div v-else class="activity-cards">
|
||||
<div
|
||||
v-for="activity in activities"
|
||||
:key="activity.id"
|
||||
class="activity-card"
|
||||
@click="$router.push(`/activity/${activity.id}`)"
|
||||
>
|
||||
<div class="card-image" :style="{ backgroundImage: `url(${activity.coverImage || 'https://s1.xiaomiev.com/activity-outer-assets/0328/images/su7_ultra_20250227/pc/5.jpg'})` }">
|
||||
<div class="card-badge">{{ activity.status === 'upcoming' ? '报名中' : '已结束' }}</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">{{ activity.title }}</h3>
|
||||
<p class="card-desc">{{ activity.description }}</p>
|
||||
<div class="card-meta">
|
||||
<div class="meta-item">
|
||||
<van-icon name="clock-o" />
|
||||
<span>{{ formatDate(activity.startTime) }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<van-icon name="location-o" />
|
||||
<span>{{ activity.location }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer" class="beian-link">
|
||||
闽ICP备2021011414号-1
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import api from '../api';
|
||||
|
||||
const router = useRouter();
|
||||
const activeTab = ref('upcoming');
|
||||
const activities = ref([]);
|
||||
const loading = ref(false);
|
||||
const finished = ref(false);
|
||||
|
||||
// 轮播图图片 - 小米汽车官方高清图片
|
||||
const bannerImages = ref([
|
||||
'https://s1.xiaomiev.com/activity-outer-assets/0328/images/su7_20260319/pc/2-1.png',
|
||||
'https://s1.xiaomiev.com/activity-outer-assets/0328/images/su7_ultra_20250227/pc/1.jpg',
|
||||
'https://s1.xiaomiev.com/activity-outer-assets/0328/images/yu7_20250626/pc/1.png',
|
||||
'https://s1.xiaomiev.com/activity-outer-assets/0328/images/su7_20260319/pc/4-1.png'
|
||||
]);
|
||||
|
||||
const loadActivities = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await api.getActivities({ status: activeTab.value });
|
||||
activities.value = data;
|
||||
finished.value = true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const formatDate = (date) => {
|
||||
return new Date(date).toLocaleDateString('zh-CN');
|
||||
};
|
||||
|
||||
loadActivities();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #f8f9fa 0%, #fff 100%);
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
height: 240px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-swipe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.swipe-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.swipe-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.5) 100%);
|
||||
}
|
||||
|
||||
.swipe-item::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 60px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 50% 50% 0 0 / 100% 100% 0 0;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.club-name {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
text-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.club-slogan {
|
||||
font-size: 14px;
|
||||
margin: 12px 0 0;
|
||||
opacity: 0.9;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
.landing-btn {
|
||||
margin-top: 20px;
|
||||
background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%);
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
padding: 0 24px;
|
||||
box-shadow: 0 4px 12px rgba(255, 103, 0, 0.4);
|
||||
}
|
||||
|
||||
.custom-tabs {
|
||||
margin-top: -30px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.empty-state {
|
||||
padding: 60px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.activity-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.activity-card {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||||
transition: transform 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.activity-card:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.card-image {
|
||||
height: 180px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-color: #e8e8e8;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-badge {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%);
|
||||
color: #fff;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2px 8px rgba(255, 103, 0, 0.3);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin: 0 0 8px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0 0 12px;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.meta-item .van-icon {
|
||||
font-size: 14px;
|
||||
color: #FF6700;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.beian-link {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.beian-link:hover {
|
||||
color: #FF6700;
|
||||
}
|
||||
|
||||
.beian-link:active {
|
||||
color: #FF8533;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,957 @@
|
||||
<template>
|
||||
<div class="landing-page">
|
||||
<!-- 背景音乐控制 -->
|
||||
<div class="music-control" @click="toggleMusic">
|
||||
<van-icon :name="isPlaying ? 'volume-o' : 'volume'" :class="{ playing: isPlaying }" />
|
||||
</div>
|
||||
<audio ref="audioPlayer" loop>
|
||||
<source src="/are-you-ok.mp3" type="audio/mpeg">
|
||||
</audio>
|
||||
|
||||
<!-- 顶部导航栏 -->
|
||||
<div class="top-nav">
|
||||
<van-icon name="arrow-left" class="nav-back" @click="$router.push('/')" />
|
||||
<span class="nav-title">小米汽车车友会</span>
|
||||
<div class="nav-placeholder"></div>
|
||||
</div>
|
||||
|
||||
<!-- Hero轮播区 -->
|
||||
<div class="hero-section">
|
||||
<van-swipe class="hero-swipe" :autoplay="4000" indicator-color="#FF6700">
|
||||
<van-swipe-item v-for="(image, index) in heroImages" :key="index">
|
||||
<div class="swipe-item" :style="{ backgroundImage: `url(${image})` }">
|
||||
<div class="swipe-overlay"></div>
|
||||
</div>
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
|
||||
<div class="hero-content">
|
||||
<div class="hero-badge">小米汽车车友会</div>
|
||||
<h1 class="hero-title">驾驭未来 · 智享生活</h1>
|
||||
<p class="hero-subtitle">Are You OK? 一起探索小米汽车的无限可能</p>
|
||||
<div class="hero-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">1000+</div>
|
||||
<div class="stat-label">车友</div>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">50+</div>
|
||||
<div class="stat-label">活动</div>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">20+</div>
|
||||
<div class="stat-label">城市</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 特色卡片区 -->
|
||||
<div class="features-section">
|
||||
<h2 class="section-title">
|
||||
<span class="title-icon">✨</span>
|
||||
车友会特色
|
||||
</h2>
|
||||
|
||||
<div class="feature-cards">
|
||||
<div
|
||||
v-for="(feature, index) in features"
|
||||
:key="index"
|
||||
class="feature-card"
|
||||
:style="{ backgroundImage: `url(${feature.image})` }"
|
||||
>
|
||||
<div class="card-overlay"></div>
|
||||
<div class="card-content">
|
||||
<div class="card-icon">{{ feature.icon }}</div>
|
||||
<h3 class="card-title">{{ feature.title }}</h3>
|
||||
<p class="card-desc">{{ feature.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 活动预览区 -->
|
||||
<div class="activities-section">
|
||||
<h2 class="section-title">
|
||||
<span class="title-icon">🎯</span>
|
||||
精彩活动
|
||||
</h2>
|
||||
|
||||
<div v-if="loading" class="loading-state">
|
||||
<van-loading size="24px">加载中...</van-loading>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activities.length === 0" class="empty-state">
|
||||
<van-empty description="敬请期待更多精彩活动" />
|
||||
</div>
|
||||
|
||||
<div v-else class="activity-preview">
|
||||
<div
|
||||
v-for="activity in activities.slice(0, 3)"
|
||||
:key="activity.id"
|
||||
class="activity-card"
|
||||
@click="$router.push(`/activity/${activity.id}`)"
|
||||
>
|
||||
<div class="activity-image" :style="{ backgroundImage: `url(${activity.coverImage || heroImages[0]})` }">
|
||||
<div class="activity-badge">报名中</div>
|
||||
</div>
|
||||
<div class="activity-info">
|
||||
<h3 class="activity-title">{{ activity.title }}</h3>
|
||||
<div class="activity-meta">
|
||||
<van-icon name="clock-o" />
|
||||
<span>{{ formatDate(activity.startTime) }}</span>
|
||||
</div>
|
||||
<div class="activity-meta">
|
||||
<van-icon name="location-o" />
|
||||
<span>{{ activity.location }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<van-button
|
||||
type="primary"
|
||||
block
|
||||
round
|
||||
size="large"
|
||||
@click="$router.push('/')"
|
||||
class="view-more-btn"
|
||||
>
|
||||
查看全部活动
|
||||
<van-icon name="arrow" />
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
<!-- 赞助商区域 -->
|
||||
<div class="sponsors-section">
|
||||
<h2 class="section-title">
|
||||
<span class="title-icon">🤝</span>
|
||||
合作伙伴
|
||||
</h2>
|
||||
|
||||
<div class="sponsors-grid">
|
||||
<div class="sponsor-card">
|
||||
<div class="sponsor-logo xiaomi-logo">
|
||||
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="10" y="10" width="80" height="80" rx="20" fill="#FF6700"/>
|
||||
<text x="50" y="65" font-family="Arial, sans-serif" font-size="40" font-weight="bold" fill="white" text-anchor="middle">MI</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="sponsor-info">
|
||||
<h3 class="sponsor-name">小米汽车泉州信兴汽车城授权服务中心</h3>
|
||||
<p class="sponsor-desc">泉州德劲新能源科技有限公司</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sponsor-card">
|
||||
<div class="sponsor-logo europark-logo">
|
||||
<svg viewBox="0 0 200 80" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="europark-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#4A90E2;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#7B68EE;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="200" height="80" rx="12" fill="url(#europark-gradient)"/>
|
||||
<text x="100" y="50" font-family="Microsoft YaHei, Arial, sans-serif" font-size="32" font-weight="bold" fill="white" text-anchor="middle">欧乐堡</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="sponsor-info">
|
||||
<h3 class="sponsor-name">泉州欧乐堡</h3>
|
||||
<p class="sponsor-desc">童话世界 · 海洋王国</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA区域 -->
|
||||
<div class="cta-section">
|
||||
<div class="cta-content">
|
||||
<h2 class="cta-title">加入我们</h2>
|
||||
<p class="cta-desc">与千位小米汽车车主一起,探索智能出行新体验</p>
|
||||
<van-button
|
||||
type="primary"
|
||||
round
|
||||
size="large"
|
||||
@click="$router.push('/')"
|
||||
class="cta-btn"
|
||||
>
|
||||
立即加入车友会
|
||||
</van-button>
|
||||
</div>
|
||||
<div class="cta-bg"></div>
|
||||
</div>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="footer">
|
||||
<div class="footer-content">
|
||||
<div class="footer-logo">
|
||||
<div class="logo-icon">🚗</div>
|
||||
<div class="logo-text">
|
||||
<div class="logo-name">小米汽车车友会</div>
|
||||
<div class="logo-slogan">驾驭未来 · 智享生活</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer" class="beian-link">
|
||||
闽ICP备2021011414号-1
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import api from '../api';
|
||||
|
||||
const router = useRouter();
|
||||
const audioPlayer = ref(null);
|
||||
const isPlaying = ref(false);
|
||||
const activities = ref([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// Hero轮播图 - 小米汽车官方高清图片
|
||||
const heroImages = ref([
|
||||
'https://s1.xiaomiev.com/activity-outer-assets/0328/images/su7_20260319/pc/2-1.png',
|
||||
'https://s1.xiaomiev.com/activity-outer-assets/0328/images/su7_ultra_20250227/pc/1.jpg',
|
||||
'https://s1.xiaomiev.com/activity-outer-assets/0328/images/yu7_20250626/pc/1.png',
|
||||
'https://s1.xiaomiev.com/activity-outer-assets/0328/images/su7_20260319/pc/4-1.png',
|
||||
'https://s1.xiaomiev.com/activity-outer-assets/0328/images/su7_ultra_20250227/pc/4.jpg'
|
||||
]);
|
||||
|
||||
// 特色功能 - 使用小米汽车真实图片
|
||||
const features = ref([
|
||||
{
|
||||
icon: '🚀',
|
||||
title: '极致性能',
|
||||
desc: '零百加速2.78秒,800V超级快充,续航800km+',
|
||||
image: 'https://s1.xiaomiev.com/activity-outer-assets/0328/images/su7_ultra_20250227/pc/5.jpg'
|
||||
},
|
||||
{
|
||||
icon: '🤝',
|
||||
title: '车友互动',
|
||||
desc: '定期组织自驾游、车友聚会、技术交流等活动',
|
||||
image: 'https://s1.xiaomiev.com/activity-outer-assets/0328/images/yu7_20250522/pc/3.jpg'
|
||||
},
|
||||
{
|
||||
icon: '🎁',
|
||||
title: '专属福利',
|
||||
desc: '车友专属优惠、保养折扣、配件优惠等福利',
|
||||
image: 'https://s1.xiaomiev.com/activity-outer-assets/0328/images/su7_20260319/pc/8-1.png'
|
||||
},
|
||||
{
|
||||
icon: '🌟',
|
||||
title: '智能生态',
|
||||
desc: '小米全生态互联,打造智能出行新体验',
|
||||
image: 'https://s1.xiaomiev.com/activity-outer-assets/0328/images/yu7_20250626/pc/15.png'
|
||||
}
|
||||
]);
|
||||
|
||||
// 音乐控制
|
||||
const toggleMusic = () => {
|
||||
if (!audioPlayer.value) return;
|
||||
|
||||
if (isPlaying.value) {
|
||||
audioPlayer.value.pause();
|
||||
isPlaying.value = false;
|
||||
} else {
|
||||
audioPlayer.value.play().catch(err => {
|
||||
console.log('音乐播放需要用户交互:', err);
|
||||
});
|
||||
isPlaying.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 尝试自动播放音乐(需要用户交互)
|
||||
const tryAutoPlay = () => {
|
||||
if (!audioPlayer.value || isPlaying.value) return;
|
||||
|
||||
audioPlayer.value.play().then(() => {
|
||||
isPlaying.value = true;
|
||||
// 移除事件监听
|
||||
document.removeEventListener('click', tryAutoPlay);
|
||||
document.removeEventListener('touchstart', tryAutoPlay);
|
||||
}).catch(() => {
|
||||
// 自动播放失败,等待用户点击
|
||||
});
|
||||
};
|
||||
|
||||
// 加载活动
|
||||
const loadActivities = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await api.getActivities({ status: 'upcoming' });
|
||||
activities.value = data;
|
||||
} catch (error) {
|
||||
console.error('加载活动失败:', error);
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const formatDate = (date) => {
|
||||
return new Date(date).toLocaleDateString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadActivities();
|
||||
|
||||
// 监听用户交互,尝试自动播放音乐
|
||||
document.addEventListener('click', tryAutoPlay, { once: true });
|
||||
document.addEventListener('touchstart', tryAutoPlay, { once: true });
|
||||
|
||||
// 3秒后如果还没播放,尝试一次
|
||||
setTimeout(() => {
|
||||
if (!isPlaying.value) {
|
||||
tryAutoPlay();
|
||||
}
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (audioPlayer.value && isPlaying.value) {
|
||||
audioPlayer.value.pause();
|
||||
}
|
||||
// 清理事件监听
|
||||
document.removeEventListener('click', tryAutoPlay);
|
||||
document.removeEventListener('touchstart', tryAutoPlay);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.landing-page {
|
||||
min-height: 100vh;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.top-nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 46px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
z-index: 999;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.nav-placeholder {
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
/* 音乐控制 */
|
||||
.music-control {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
color: #FF6700;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.music-control:hover {
|
||||
background: rgba(255, 255, 255, 1);
|
||||
box-shadow: 0 4px 16px rgba(255, 103, 0, 0.2);
|
||||
}
|
||||
|
||||
.music-control:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.music-control .van-icon {
|
||||
font-size: 22px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.music-control .van-icon.playing {
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
}
|
||||
|
||||
.music-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Hero区域 */
|
||||
.hero-section {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
min-height: 600px;
|
||||
overflow: hidden;
|
||||
margin-top: 46px;
|
||||
}
|
||||
|
||||
.hero-swipe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.swipe-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.swipe-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.6) 100%);
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
display: inline-block;
|
||||
background: rgba(255, 103, 0, 0.9);
|
||||
padding: 8px 24px;
|
||||
border-radius: 30px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24px;
|
||||
letter-spacing: 2px;
|
||||
box-shadow: 0 4px 16px rgba(255, 103, 0, 0.4);
|
||||
animation: fadeInDown 1s;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
margin: 0 0 16px;
|
||||
text-shadow: 0 4px 16px rgba(0,0,0,0.3);
|
||||
letter-spacing: 4px;
|
||||
animation: fadeInUp 1s 0.2s both;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 18px;
|
||||
margin: 0 0 48px;
|
||||
opacity: 0.95;
|
||||
letter-spacing: 2px;
|
||||
animation: fadeInUp 1s 0.4s both;
|
||||
}
|
||||
|
||||
.hero-stats {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
align-items: center;
|
||||
animation: fadeInUp 1s 0.6s both;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: #FF6700;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
width: 1px;
|
||||
height: 40px;
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 特色区域 */
|
||||
.features-section,
|
||||
.activities-section {
|
||||
padding: 80px 16px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 0 0 48px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.feature-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 24px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
position: relative;
|
||||
height: 320px;
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 12px 32px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.card-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.7) 100%);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
padding: 24px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
opacity: 0.95;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 活动预览 */
|
||||
.activity-preview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 24px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.activity-card {
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.activity-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
.activity-image {
|
||||
height: 200px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.activity-badge {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
background: #FF6700;
|
||||
color: #fff;
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 8px rgba(255, 103, 0, 0.3);
|
||||
}
|
||||
|
||||
.activity-info {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.activity-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.activity-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.activity-meta .van-icon {
|
||||
color: #FF6700;
|
||||
}
|
||||
|
||||
.view-more-btn {
|
||||
background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%);
|
||||
border: none;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
height: 52px;
|
||||
box-shadow: 0 4px 16px rgba(255, 103, 0, 0.3);
|
||||
}
|
||||
|
||||
.view-more-btn .van-icon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* 赞助商区域 */
|
||||
.sponsors-section {
|
||||
padding: 60px 16px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.sponsors-grid {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
gap: 16px;
|
||||
margin-top: 32px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sponsor-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||
transition: transform 0.3s, box-shadow 0.3s;
|
||||
flex: 0 1 140px;
|
||||
max-width: 150px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.sponsor-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.sponsor-logo {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sponsor-logo svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.xiaomi-logo svg {
|
||||
filter: drop-shadow(0 2px 8px rgba(255, 103, 0, 0.2));
|
||||
}
|
||||
|
||||
.europark-logo svg {
|
||||
filter: drop-shadow(0 2px 8px rgba(74, 144, 226, 0.2));
|
||||
}
|
||||
|
||||
.sponsor-info {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sponsor-name {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0 0 4px;
|
||||
line-height: 1.3;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.sponsor-desc {
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* CTA区域 */
|
||||
.cta-section {
|
||||
position: relative;
|
||||
padding: 100px 20px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%);
|
||||
}
|
||||
|
||||
.cta-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
opacity: 0.1;
|
||||
background-image: url('https://s1.xiaomiev.com/activity-outer-assets/0328/images/su7_ultra_20250227/pc/7-1-1.jpeg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.cta-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.cta-title {
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.cta-desc {
|
||||
font-size: 18px;
|
||||
color: rgba(255,255,255,0.95);
|
||||
margin: 0 0 32px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.cta-btn {
|
||||
background: #fff;
|
||||
color: #FF6700;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
height: 52px;
|
||||
padding: 0 48px;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* 页脚 */
|
||||
.footer {
|
||||
background: #1a1a1a;
|
||||
padding: 48px 20px;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.logo-name {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.logo-slogan {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.beian-link {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.beian-link:hover {
|
||||
color: #FF6700;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.empty-state {
|
||||
padding: 60px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.hero-title {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.hero-stats {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.feature-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.activity-preview {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.cta-title {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.sponsors-grid {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sponsor-card {
|
||||
flex: 0 1 calc(20% - 10px);
|
||||
min-width: 100px;
|
||||
max-width: 130px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.sponsor-logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.sponsor-name {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.sponsor-desc {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<div class="register">
|
||||
<div class="register-header">
|
||||
<div class="header-back" @click="$router.back()">
|
||||
<van-icon name="arrow-left" />
|
||||
</div>
|
||||
<h1 class="header-title">活动报名</h1>
|
||||
</div>
|
||||
|
||||
<div class="register-content">
|
||||
<div class="form-card">
|
||||
<van-form @submit="onSubmit">
|
||||
<div class="form-section">
|
||||
<div class="section-label">基本信息</div>
|
||||
<van-field
|
||||
v-model="form.name"
|
||||
label="姓名"
|
||||
placeholder="请输入您的姓名"
|
||||
required
|
||||
:rules="[{ required: true, message: '请输入姓名' }]"
|
||||
/>
|
||||
<van-field
|
||||
v-model="form.phone"
|
||||
label="手机号"
|
||||
type="tel"
|
||||
placeholder="请输入手机号"
|
||||
maxlength="11"
|
||||
required
|
||||
:rules="[{ required: true, message: '请输入手机号' }, { pattern: /^1\d{10}$/, message: '请输入正确的手机号' }]"
|
||||
/>
|
||||
<van-field
|
||||
v-model.number="form.peopleCount"
|
||||
label="人数"
|
||||
type="number"
|
||||
placeholder="请输入参加人数"
|
||||
required
|
||||
:rules="[{ required: true, message: '请输入人数' }]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="section-label">车辆信息</div>
|
||||
<van-field
|
||||
v-model="form.carModel"
|
||||
label="车型"
|
||||
placeholder="请选择车型"
|
||||
readonly
|
||||
is-link
|
||||
@click="showCarModelPicker = true"
|
||||
/>
|
||||
<van-field
|
||||
label="车牌号"
|
||||
:rules="[{ required: true, message: '请输入车牌号' }]"
|
||||
class="car-number-field"
|
||||
>
|
||||
<template #input>
|
||||
<div class="license-plate-input">
|
||||
<span class="plate-prefix">闽</span>
|
||||
<input
|
||||
ref="letterInput"
|
||||
v-model="carLetter"
|
||||
@input="handleLetterInput"
|
||||
placeholder="_"
|
||||
maxlength="1"
|
||||
class="plate-letter"
|
||||
/>
|
||||
<span class="plate-dot">·</span>
|
||||
<input
|
||||
ref="digitsInput"
|
||||
v-model="carDigits"
|
||||
@input="handleDigitsInput"
|
||||
placeholder="______"
|
||||
maxlength="6"
|
||||
class="plate-digits"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="section-label">其他信息</div>
|
||||
<van-field
|
||||
v-model="form.note"
|
||||
label="备注"
|
||||
type="textarea"
|
||||
placeholder="有什么想说的吗?"
|
||||
rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="submit-btn">
|
||||
<van-button round block type="primary" native-type="submit" size="large" color="#FF6700">
|
||||
提交报名
|
||||
</van-button>
|
||||
</div>
|
||||
</van-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 车型选择器 -->
|
||||
<van-popup v-model:show="showCarModelPicker" position="bottom" round>
|
||||
<van-picker
|
||||
:columns="carModels"
|
||||
@confirm="onCarModelConfirm"
|
||||
@cancel="showCarModelPicker = false"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { showToast, showDialog } from 'vant';
|
||||
import api from '../api';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const showCarModelPicker = ref(false);
|
||||
const carModels = ref([
|
||||
{ text: '小米SU7', value: '小米SU7' },
|
||||
{ text: '小米YU7', value: '小米YU7' },
|
||||
{ text: '小米SU7 Ultra', value: '小米SU7 Ultra' }
|
||||
]);
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
phone: '',
|
||||
peopleCount: 1,
|
||||
carModel: '',
|
||||
carNumber: '',
|
||||
note: ''
|
||||
});
|
||||
|
||||
const carLetter = ref('');
|
||||
const carDigits = ref('');
|
||||
const letterInput = ref(null);
|
||||
const digitsInput = ref(null);
|
||||
|
||||
const handleLetterInput = (e) => {
|
||||
const value = e.target.value.toUpperCase().replace(/[^A-Z]/g, '');
|
||||
carLetter.value = value;
|
||||
form.value.carNumber = value + carDigits.value;
|
||||
|
||||
// 输入完1位字母后自动跳到数字输入框
|
||||
if (value.length === 1) {
|
||||
setTimeout(() => {
|
||||
digitsInput.value?.focus();
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDigitsInput = (e) => {
|
||||
const value = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||
carDigits.value = value;
|
||||
form.value.carNumber = carLetter.value + value;
|
||||
};
|
||||
|
||||
// 车型选择确认
|
||||
const onCarModelConfirm = ({ selectedOptions }) => {
|
||||
form.value.carModel = selectedOptions[0].value;
|
||||
showCarModelPicker.value = false;
|
||||
};
|
||||
|
||||
// 车牌号格式化已被handleLetterInput和handleDigitsInput替代
|
||||
|
||||
// 从缓存加载数据
|
||||
onMounted(() => {
|
||||
const cached = localStorage.getItem('userRegistrationInfo');
|
||||
if (cached) {
|
||||
try {
|
||||
const data = JSON.parse(cached);
|
||||
form.value = { ...form.value, ...data };
|
||||
} catch (e) {
|
||||
console.error('加载缓存失败', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
// 添加"闽"前缀到车牌号
|
||||
const fullCarNumber = form.value.carNumber ? `闽${form.value.carNumber}` : '';
|
||||
|
||||
// 保存到缓存
|
||||
localStorage.setItem('userRegistrationInfo', JSON.stringify({
|
||||
name: form.value.name,
|
||||
phone: form.value.phone,
|
||||
carModel: form.value.carModel,
|
||||
carNumber: fullCarNumber
|
||||
}));
|
||||
|
||||
const { data } = await api.submitRegistration({
|
||||
activityId: route.params.id,
|
||||
...form.value,
|
||||
carNumber: fullCarNumber
|
||||
});
|
||||
|
||||
// 保存报名ID和登录信息
|
||||
const registrations = JSON.parse(localStorage.getItem('myRegistrations') || '[]');
|
||||
registrations.push({
|
||||
activityId: route.params.id,
|
||||
registrationId: data.id,
|
||||
date: new Date().toISOString()
|
||||
});
|
||||
localStorage.setItem('myRegistrations', JSON.stringify(registrations));
|
||||
|
||||
// 自动保存登录状态
|
||||
localStorage.setItem(`user_reg_${route.params.id}`, JSON.stringify({
|
||||
id: data.id,
|
||||
name: form.value.name,
|
||||
phone: form.value.phone,
|
||||
carNumber: fullCarNumber,
|
||||
carModel: form.value.carModel,
|
||||
status: 'pending',
|
||||
activityId: route.params.id
|
||||
}));
|
||||
|
||||
await showDialog({
|
||||
title: '报名成功',
|
||||
message: `您的报名ID: ${data.id}\n已自动登录,请等待管理员审核`
|
||||
});
|
||||
|
||||
router.back();
|
||||
} catch (error) {
|
||||
showToast('报名失败');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.register {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #FF6700 0%, #FF8533 100%);
|
||||
}
|
||||
|
||||
.register-header {
|
||||
position: relative;
|
||||
padding: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header-back {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255,255,255,0.2);
|
||||
border-radius: 50%;
|
||||
font-size: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.register-content {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-section:last-of-type {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 8px;
|
||||
border-left: 3px solid #FF6700;
|
||||
}
|
||||
|
||||
/* 车牌号字段特殊样式 - 垂直居中 */
|
||||
.car-number-field :deep(.van-cell) {
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.car-number-field :deep(.van-field__label) {
|
||||
padding-top: 5px !important;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
/* 车牌输入样式 - 橙色主题 */
|
||||
.license-plate-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(255, 103, 0, 0.3);
|
||||
width: fit-content;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.plate-prefix {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.plate-letter {
|
||||
width: 22px;
|
||||
height: 28px;
|
||||
border: 2px solid rgba(255,255,255,0.9);
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
outline: none;
|
||||
text-transform: uppercase;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.plate-dot {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin: 0 1px;
|
||||
}
|
||||
|
||||
.plate-digits {
|
||||
width: 110px;
|
||||
height: 28px;
|
||||
border: 2px solid rgba(255,255,255,0.9);
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
outline: none;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.plate-letter::placeholder,
|
||||
.plate-digits::placeholder {
|
||||
color: #ddd;
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,450 @@
|
||||
<template>
|
||||
<div class="share-page">
|
||||
<div v-if="loading" class="loading-container">
|
||||
<van-loading size="32px">加载中...</van-loading>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activity.id" class="share-content">
|
||||
<div class="share-header" :style="{ backgroundImage: `url(${activity.coverImage || 'https://s1.xiaomiev.com/activity-outer-assets/0328/images/su7_ultra_20250227/pc/5.jpg'})` }">
|
||||
<div class="header-overlay"></div>
|
||||
<div class="header-back" @click="$router.push('/')">
|
||||
<van-icon name="arrow-left" />
|
||||
</div>
|
||||
<div class="header-info">
|
||||
<div class="club-badge">泉友米车友会</div>
|
||||
<h1 class="activity-title">{{ activity.title }}</h1>
|
||||
<div class="activity-status">
|
||||
<span class="status-badge" :class="activity.status">
|
||||
{{ activity.status === 'upcoming' ? '🔥 报名中' : '✅ 已结束' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="share-body">
|
||||
<div class="info-cards">
|
||||
<div class="info-card">
|
||||
<van-icon name="clock-o" class="card-icon" />
|
||||
<div class="card-content">
|
||||
<div class="card-label">活动时间</div>
|
||||
<div class="card-value">{{ formatDate(activity.startTime) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-card">
|
||||
<van-icon name="location-o" class="card-icon" />
|
||||
<div class="card-content">
|
||||
<div class="card-label">活动地点</div>
|
||||
<div class="card-value">{{ activity.location }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-card">
|
||||
<van-icon name="calendar-o" class="card-icon" />
|
||||
<div class="card-content">
|
||||
<div class="card-label">报名截止</div>
|
||||
<div class="card-value">{{ formatDate(activity.registrationDeadline) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="description-section">
|
||||
<h3 class="section-title">
|
||||
<van-icon name="notes-o" />
|
||||
活动介绍
|
||||
</h3>
|
||||
<p class="description-text">{{ activity.description }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="registrations.length" class="members-section">
|
||||
<h3 class="section-title">
|
||||
<van-icon name="friends-o" />
|
||||
已报名 <span class="member-count">{{ registrations.length }}人</span>
|
||||
</h3>
|
||||
<div class="member-avatars">
|
||||
<div v-for="(reg, index) in registrations.slice(0, 10)" :key="reg.id" class="member-avatar" :style="{ zIndex: 10 - index }">
|
||||
{{ reg.name.charAt(0) }}
|
||||
</div>
|
||||
<div v-if="registrations.length > 10" class="member-more">
|
||||
+{{ registrations.length - 10 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-section">
|
||||
<van-button
|
||||
type="primary"
|
||||
block
|
||||
round
|
||||
size="large"
|
||||
@click="goToDetail"
|
||||
class="action-btn"
|
||||
>
|
||||
<van-icon name="arrow" />
|
||||
查看详情并报名
|
||||
</van-button>
|
||||
|
||||
<div class="share-tip">
|
||||
<van-icon name="info-o" />
|
||||
点击按钮查看完整活动信息
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="share-footer">
|
||||
<div class="footer-logo">
|
||||
<div class="logo-icon">🚗</div>
|
||||
<div class="logo-text">
|
||||
<div class="logo-name">泉友米车友会</div>
|
||||
<div class="logo-slogan">驾驭激情 · 共享旅程</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="error-state">
|
||||
<van-empty description="活动不存在或已删除" />
|
||||
<van-button type="primary" round @click="$router.push('/')">返回首页</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import api from '../api';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const activity = ref({});
|
||||
const registrations = ref([]);
|
||||
const loading = ref(true);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const activityId = route.params.id || route.query.id;
|
||||
if (!activityId) {
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await api.getActivity(activityId);
|
||||
activity.value = data.activity;
|
||||
|
||||
const { data: regs } = await api.getActivityRegistrations(activityId);
|
||||
registrations.value = regs;
|
||||
} catch (error) {
|
||||
console.error('加载失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (date) => {
|
||||
return new Date(date).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
const goToDetail = () => {
|
||||
router.push(`/activity/${activity.value.id}`);
|
||||
};
|
||||
|
||||
onMounted(loadData);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.share-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #f8f9fa 0%, #fff 100%);
|
||||
}
|
||||
|
||||
.loading-container,
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.share-header {
|
||||
position: relative;
|
||||
height: 320px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.header-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.7) 100%);
|
||||
}
|
||||
|
||||
.header-back {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
z-index: 2;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255,255,255,0.3);
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.club-badge {
|
||||
display: inline-block;
|
||||
background: rgba(255,255,255,0.25);
|
||||
backdrop-filter: blur(10px);
|
||||
color: #fff;
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.activity-title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin: 0 0 12px;
|
||||
text-shadow: 0 2px 12px rgba(0,0,0,0.4);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.activity-status {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.status-badge.upcoming {
|
||||
background: rgba(255,107,107,0.9);
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 12px rgba(255,107,107,0.4);
|
||||
}
|
||||
|
||||
.status-badge.completed {
|
||||
background: rgba(76,175,80,0.9);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.share-body {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.info-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 28px;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.description-section,
|
||||
.members-section {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 17px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.section-title .van-icon {
|
||||
font-size: 20px;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.member-count {
|
||||
color: #667eea;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.member-avatars {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.member-avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border: 3px solid #fff;
|
||||
margin-left: -12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.member-avatar:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.member-more {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
background: #f0f0f0;
|
||||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
border: 3px solid #fff;
|
||||
margin-left: -12px;
|
||||
}
|
||||
|
||||
.action-section {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
height: 52px;
|
||||
box-shadow: 0 4px 16px rgba(102,126,234,0.4);
|
||||
}
|
||||
|
||||
.action-btn .van-icon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.share-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.share-tip .van-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.share-footer {
|
||||
padding: 32px 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.logo-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.logo-slogan {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,920 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<div class="dashboard-header">
|
||||
<div class="header-content">
|
||||
<h1>管理后台</h1>
|
||||
<p>泉友米车友会</p>
|
||||
</div>
|
||||
<van-button class="logout-btn" size="small" round @click="logout">
|
||||
<van-icon name="logout" /> 退出
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-content">
|
||||
<div class="content-header">
|
||||
<h2>活动管理</h2>
|
||||
<van-button type="primary" round icon="plus" @click="createNewActivity">
|
||||
创建活动
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
<div class="activity-cards">
|
||||
<div v-for="activity in activities" :key="activity.id" class="activity-card" @click="viewActivityDetail(activity)">
|
||||
<div class="card-header">
|
||||
<h3>{{ activity.title }}</h3>
|
||||
<van-tag :type="activity.status === 'upcoming' ? 'primary' : 'default'">
|
||||
{{ activity.status === 'upcoming' ? '进行中' : '已结束' }}
|
||||
</van-tag>
|
||||
</div>
|
||||
<p class="card-desc">{{ activity.description }}</p>
|
||||
<div class="card-info">
|
||||
<div class="info-row">
|
||||
<van-icon name="location-o" />
|
||||
<span>{{ activity.location }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<van-icon name="clock-o" />
|
||||
<span>{{ formatDate(activity.startTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<van-icon name="arrow" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<van-empty v-if="!activities.length" description="暂无活动" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 活动表单弹窗 -->
|
||||
<van-popup v-model:show="showActivityForm" position="bottom" round>
|
||||
<div class="form-popup">
|
||||
<div class="popup-header">
|
||||
<h3>创建活动</h3>
|
||||
<van-icon name="cross" @click="closeActivityForm" />
|
||||
</div>
|
||||
<van-form @submit="submitActivity" class="popup-form">
|
||||
<van-cell-group>
|
||||
<van-field
|
||||
v-model="activityForm.title"
|
||||
label="活动标题"
|
||||
placeholder="请输入活动标题"
|
||||
required
|
||||
:rules="[{ required: true, message: '请输入活动标题' }]"
|
||||
/>
|
||||
<van-field
|
||||
v-model="activityForm.description"
|
||||
label="活动描述"
|
||||
type="textarea"
|
||||
placeholder="请输入活动描述,包括活动内容、注意事项等"
|
||||
rows="3"
|
||||
autosize
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
<van-field
|
||||
v-model="activityForm.price"
|
||||
label="人均费用"
|
||||
type="number"
|
||||
placeholder="请输入人均费用(选填)"
|
||||
>
|
||||
<template #left-icon>
|
||||
<van-icon name="gold-coin-o" style="margin-right: 8px;" />
|
||||
</template>
|
||||
<template #button>
|
||||
<span style="color: #999;">元</span>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field
|
||||
v-model="activityForm.budget"
|
||||
label="活动预算"
|
||||
type="number"
|
||||
placeholder="请输入预算金额(选填)"
|
||||
>
|
||||
<template #left-icon>
|
||||
<van-icon name="balance-o" style="margin-right: 8px;" />
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field
|
||||
v-model="activityForm.location"
|
||||
label="活动地点"
|
||||
placeholder="点击选择地点"
|
||||
readonly
|
||||
is-link
|
||||
required
|
||||
:rules="[{ required: true, message: '请选择活动地点' }]"
|
||||
@click="openMapPicker"
|
||||
>
|
||||
<template #left-icon>
|
||||
<van-icon name="location-o" style="margin-right: 8px;" />
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field
|
||||
v-model="startTimeDisplay"
|
||||
label="开始时间"
|
||||
placeholder="请选择开始时间"
|
||||
readonly
|
||||
is-link
|
||||
required
|
||||
:rules="[{ required: true, message: '请选择开始时间' }]"
|
||||
@click="showStartTimePicker = true"
|
||||
>
|
||||
<template #left-icon>
|
||||
<van-icon name="clock-o" style="margin-right: 8px;" />
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field
|
||||
v-model="deadlineDisplay"
|
||||
label="报名截止"
|
||||
placeholder="请选择报名截止时间"
|
||||
readonly
|
||||
is-link
|
||||
required
|
||||
:rules="[{ required: true, message: '请选择报名截止时间' }]"
|
||||
@click="showDeadlinePicker = true"
|
||||
>
|
||||
<template #left-icon>
|
||||
<van-icon name="calendar-o" style="margin-right: 8px;" />
|
||||
</template>
|
||||
</van-field>
|
||||
</van-cell-group>
|
||||
<div class="form-actions">
|
||||
<van-button round block type="primary" native-type="submit" size="large">
|
||||
创建活动
|
||||
</van-button>
|
||||
</div>
|
||||
</van-form>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<!-- 开始时间选择器 -->
|
||||
<van-popup v-model:show="showStartTimePicker" position="bottom" round>
|
||||
<div class="datetime-picker-popup">
|
||||
<div class="picker-header">
|
||||
<van-button size="small" @click="showStartTimePicker = false">取消</van-button>
|
||||
<h3>选择开始时间</h3>
|
||||
<van-button size="small" type="primary" @click="confirmStartTime">确定</van-button>
|
||||
</div>
|
||||
<div class="picker-body">
|
||||
<input
|
||||
type="datetime-local"
|
||||
v-model="startTimeInput"
|
||||
class="datetime-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<!-- 报名截止时间选择器 -->
|
||||
<van-popup v-model:show="showDeadlinePicker" position="bottom" round>
|
||||
<div class="datetime-picker-popup">
|
||||
<div class="picker-header">
|
||||
<van-button size="small" @click="showDeadlinePicker = false">取消</van-button>
|
||||
<h3>选择报名截止时间</h3>
|
||||
<van-button size="small" type="primary" @click="confirmDeadline">确定</van-button>
|
||||
</div>
|
||||
<div class="picker-body">
|
||||
<input
|
||||
type="datetime-local"
|
||||
v-model="deadlineInput"
|
||||
class="datetime-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<!-- 地图选点弹窗 -->
|
||||
<van-popup v-model:show="showMapPicker" position="bottom" round :style="{ height: '80%' }">
|
||||
<div class="map-picker">
|
||||
<div class="map-picker-header">
|
||||
<van-icon name="arrow-left" @click="showMapPicker = false" />
|
||||
<h3>选择地点</h3>
|
||||
<van-button type="primary" size="small" @click="confirmLocation">确定</van-button>
|
||||
</div>
|
||||
<div class="map-search">
|
||||
<van-search
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索地点"
|
||||
@search="searchLocation"
|
||||
@clear="clearSearch"
|
||||
show-action
|
||||
action-text="搜索"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="searchResults.length" class="search-results">
|
||||
<div
|
||||
v-for="(result, index) in searchResults"
|
||||
:key="index"
|
||||
class="search-result-item"
|
||||
@click="selectSearchResult(result)"
|
||||
>
|
||||
<div class="result-name">{{ result.name }}</div>
|
||||
<div class="result-address">{{ result.address }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="map-picker-container" class="map-container"></div>
|
||||
<div class="selected-location">
|
||||
<van-icon name="location-o" />
|
||||
<span>{{ selectedLocation.name || '请在地图上选择位置' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { showToast } from 'vant';
|
||||
import api from '../../api';
|
||||
|
||||
const router = useRouter();
|
||||
const activities = ref([]);
|
||||
|
||||
const showActivityForm = ref(false);
|
||||
const showMapPicker = ref(false);
|
||||
const showStartTimePicker = ref(false);
|
||||
const showDeadlinePicker = ref(false);
|
||||
const searchKeyword = ref('');
|
||||
const searchResults = ref([]);
|
||||
const selectedLocation = ref({});
|
||||
let pickerMap = null;
|
||||
let pickerMarker = null;
|
||||
let geocoder = null;
|
||||
|
||||
const activityForm = ref({
|
||||
title: '',
|
||||
description: '',
|
||||
budget: '',
|
||||
location: '',
|
||||
locationLat: null,
|
||||
locationLng: null,
|
||||
startTime: '',
|
||||
registrationDeadline: ''
|
||||
});
|
||||
|
||||
// 日期时间选择器的值
|
||||
const startTimeInput = ref('');
|
||||
const deadlineInput = ref('');
|
||||
|
||||
// 显示格式化的日期时间
|
||||
const startTimeDisplay = computed(() => {
|
||||
if (!activityForm.value.startTime) return '';
|
||||
return formatDateTime(activityForm.value.startTime);
|
||||
});
|
||||
|
||||
const deadlineDisplay = computed(() => {
|
||||
if (!activityForm.value.registrationDeadline) return '';
|
||||
return formatDateTime(activityForm.value.registrationDeadline);
|
||||
});
|
||||
|
||||
const formatDate = (date) => {
|
||||
return new Date(date).toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
// 格式化日期时间显示
|
||||
const formatDateTime = (dateTime) => {
|
||||
if (!dateTime) return '';
|
||||
const date = new Date(dateTime);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
// 开始时间确认
|
||||
const confirmStartTime = () => {
|
||||
if (!startTimeInput.value) {
|
||||
showToast('请选择开始时间');
|
||||
return;
|
||||
}
|
||||
const selectedDate = new Date(startTimeInput.value);
|
||||
activityForm.value.startTime = selectedDate.toISOString();
|
||||
showStartTimePicker.value = false;
|
||||
};
|
||||
|
||||
// 报名截止时间确认
|
||||
const confirmDeadline = () => {
|
||||
if (!deadlineInput.value) {
|
||||
showToast('请选择报名截止时间');
|
||||
return;
|
||||
}
|
||||
const selectedDate = new Date(deadlineInput.value);
|
||||
if (activityForm.value.startTime && selectedDate > new Date(activityForm.value.startTime)) {
|
||||
showToast('报名截止时间不能晚于活动开始时间');
|
||||
return;
|
||||
}
|
||||
activityForm.value.registrationDeadline = selectedDate.toISOString();
|
||||
showDeadlinePicker.value = false;
|
||||
};
|
||||
|
||||
// 打开地图选择器
|
||||
const openMapPicker = () => {
|
||||
showMapPicker.value = true;
|
||||
};
|
||||
|
||||
// 关闭活动表单
|
||||
const closeActivityForm = () => {
|
||||
showActivityForm.value = false;
|
||||
activityForm.value = {
|
||||
title: '',
|
||||
description: '',
|
||||
budget: '',
|
||||
location: '',
|
||||
locationLat: null,
|
||||
locationLng: null,
|
||||
startTime: '',
|
||||
registrationDeadline: ''
|
||||
};
|
||||
startTimeInput.value = '';
|
||||
deadlineInput.value = '';
|
||||
};
|
||||
|
||||
const loadActivities = async () => {
|
||||
try {
|
||||
const { data } = await api.getActivities();
|
||||
activities.value = data;
|
||||
} catch (error) {
|
||||
showToast('加载失败');
|
||||
}
|
||||
};
|
||||
|
||||
const createNewActivity = () => {
|
||||
activityForm.value = {
|
||||
title: '',
|
||||
description: '',
|
||||
budget: '',
|
||||
location: '',
|
||||
locationLat: null,
|
||||
locationLng: null,
|
||||
startTime: '',
|
||||
registrationDeadline: ''
|
||||
};
|
||||
startTimeInput.value = '';
|
||||
deadlineInput.value = '';
|
||||
showActivityForm.value = true;
|
||||
};
|
||||
|
||||
const viewActivityDetail = (activity) => {
|
||||
router.push(`/admin/activity/${activity.id}`);
|
||||
};
|
||||
|
||||
const initMapPicker = async () => {
|
||||
try {
|
||||
console.log('开始初始化地图选择器...');
|
||||
const AMap = await window.loadAMap();
|
||||
console.log('地图API加载完成');
|
||||
|
||||
if (pickerMap) {
|
||||
console.log('销毁旧地图实例');
|
||||
pickerMap.destroy();
|
||||
}
|
||||
|
||||
const center = activityForm.value.locationLat && activityForm.value.locationLng
|
||||
? [activityForm.value.locationLng, activityForm.value.locationLat]
|
||||
: [118.6762, 24.8742];
|
||||
|
||||
console.log('地图中心点:', center);
|
||||
|
||||
pickerMap = new AMap.Map('map-picker-container', {
|
||||
zoom: 15,
|
||||
center: center
|
||||
});
|
||||
|
||||
console.log('地图实例创建完成');
|
||||
|
||||
geocoder = new AMap.Geocoder();
|
||||
console.log('地理编码器创建完成');
|
||||
|
||||
pickerMarker = new AMap.Marker({
|
||||
position: center,
|
||||
map: pickerMap,
|
||||
draggable: true
|
||||
});
|
||||
|
||||
console.log('地图标记创建完成');
|
||||
|
||||
if (activityForm.value.location) {
|
||||
selectedLocation.value = {
|
||||
name: activityForm.value.location,
|
||||
lat: activityForm.value.locationLat,
|
||||
lng: activityForm.value.locationLng
|
||||
};
|
||||
}
|
||||
|
||||
pickerMap.on('click', (e) => {
|
||||
const { lng, lat } = e.lnglat;
|
||||
pickerMarker.setPosition([lng, lat]);
|
||||
|
||||
selectedLocation.value = {
|
||||
name: '获取地址中...',
|
||||
lat: lat,
|
||||
lng: lng
|
||||
};
|
||||
|
||||
if (geocoder) {
|
||||
geocoder.getAddress([lng, lat], (status, result) => {
|
||||
if (status === 'complete' && result.info === 'OK') {
|
||||
selectedLocation.value.name = result.regeocode.formattedAddress;
|
||||
} else {
|
||||
console.warn('逆地理编码失败:', status, result);
|
||||
selectedLocation.value.name = `泉州市区 (${lat.toFixed(4)}, ${lng.toFixed(4)})`;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
selectedLocation.value.name = `泉州市区 (${lat.toFixed(4)}, ${lng.toFixed(4)})`;
|
||||
}
|
||||
});
|
||||
|
||||
pickerMarker.on('dragend', (e) => {
|
||||
const { lng, lat } = e.lnglat;
|
||||
|
||||
selectedLocation.value = {
|
||||
name: '获取地址中...',
|
||||
lat: lat,
|
||||
lng: lng
|
||||
};
|
||||
|
||||
if (geocoder) {
|
||||
geocoder.getAddress([lng, lat], (status, result) => {
|
||||
if (status === 'complete' && result.info === 'OK') {
|
||||
selectedLocation.value.name = result.regeocode.formattedAddress;
|
||||
} else {
|
||||
console.warn('逆地理编码失败:', status, result);
|
||||
selectedLocation.value.name = `泉州市区 (${lat.toFixed(4)}, ${lng.toFixed(4)})`;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
selectedLocation.value.name = `泉州市区 (${lat.toFixed(4)}, ${lng.toFixed(4)})`;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('地图初始化完成');
|
||||
} catch (error) {
|
||||
console.error('地图初始化失败:', error);
|
||||
showToast('地图加载失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const searchLocation = async () => {
|
||||
if (!searchKeyword.value.trim()) {
|
||||
showToast('请输入搜索关键词');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('开始搜索地点:', searchKeyword.value);
|
||||
const AMap = await window.loadAMap();
|
||||
|
||||
if (!AMap.PlaceSearch) {
|
||||
console.error('PlaceSearch插件未加载');
|
||||
showToast('地图搜索功能未就绪');
|
||||
return;
|
||||
}
|
||||
|
||||
const placeSearch = new AMap.PlaceSearch({
|
||||
pageSize: 10,
|
||||
pageIndex: 1,
|
||||
city: '泉州',
|
||||
citylimit: false,
|
||||
extensions: 'base'
|
||||
});
|
||||
|
||||
placeSearch.search(searchKeyword.value, (status, result) => {
|
||||
console.log('搜索状态:', status);
|
||||
console.log('搜索结果:', result);
|
||||
|
||||
if (status === 'complete' && result.info === 'OK') {
|
||||
if (result.poiList && result.poiList.pois && result.poiList.pois.length > 0) {
|
||||
searchResults.value = result.poiList.pois.map(poi => ({
|
||||
name: poi.name,
|
||||
address: poi.address || poi.district || '',
|
||||
lat: poi.location.lat,
|
||||
lng: poi.location.lng
|
||||
}));
|
||||
console.log('搜索成功,找到', searchResults.value.length, '个结果');
|
||||
} else {
|
||||
searchResults.value = [];
|
||||
showToast('未找到相关地点');
|
||||
}
|
||||
} else if (status === 'no_data') {
|
||||
searchResults.value = [];
|
||||
showToast('未找到相关地点');
|
||||
} else {
|
||||
console.error('搜索失败:', status, result);
|
||||
searchResults.value = [];
|
||||
showToast('搜索失败: ' + (result.info || status));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('搜索异常:', error);
|
||||
showToast('搜索失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const selectSearchResult = (result) => {
|
||||
selectedLocation.value = result;
|
||||
pickerMarker.setPosition([result.lng, result.lat]);
|
||||
pickerMap.setCenter([result.lng, result.lat]);
|
||||
searchResults.value = [];
|
||||
searchKeyword.value = '';
|
||||
};
|
||||
|
||||
const clearSearch = () => {
|
||||
searchResults.value = [];
|
||||
};
|
||||
|
||||
const confirmLocation = () => {
|
||||
if (selectedLocation.value.lat && selectedLocation.value.lng) {
|
||||
activityForm.value.location = selectedLocation.value.name;
|
||||
activityForm.value.locationLat = selectedLocation.value.lat;
|
||||
activityForm.value.locationLng = selectedLocation.value.lng;
|
||||
showMapPicker.value = false;
|
||||
} else {
|
||||
showToast('请在地图上选择位置');
|
||||
}
|
||||
};
|
||||
|
||||
watch(showMapPicker, (val) => {
|
||||
if (val) {
|
||||
setTimeout(() => {
|
||||
initMapPicker();
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
|
||||
const submitActivity = async () => {
|
||||
try {
|
||||
const formData = { ...activityForm.value };
|
||||
// 清理空字符串,转换为null
|
||||
if (formData.budget === '' || formData.budget === undefined) {
|
||||
formData.budget = null;
|
||||
}
|
||||
if (formData.price === '' || formData.price === undefined) {
|
||||
formData.price = null;
|
||||
}
|
||||
|
||||
await api.createActivity(formData);
|
||||
showToast('创建成功');
|
||||
closeActivityForm();
|
||||
loadActivities();
|
||||
} catch (error) {
|
||||
showToast('创建失败');
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token');
|
||||
router.push('/admin/login');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadActivities();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard {
|
||||
min-height: 100vh;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
background: #fff;
|
||||
padding: 24px 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
font-size: 24px;
|
||||
margin: 0 0 4px;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.header-content p {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
background: #f5f5f5;
|
||||
border: none;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.logout-btn:active {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.dashboard-content {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.content-header h2 {
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
color: #1a1a1a;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.activity-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.activity-card {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
border: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.activity-card:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
font-size: 17px;
|
||||
margin: 0;
|
||||
color: #1a1a1a;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0 0 12px;
|
||||
line-height: 1.6;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.info-row .van-icon {
|
||||
font-size: 16px;
|
||||
color: #FF6700;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
color: #ddd;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 弹窗 */
|
||||
.form-popup {
|
||||
max-height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border-radius: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.popup-header h3 {
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
color: #1a1a1a;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.popup-header .van-icon {
|
||||
font-size: 20px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.popup-form {
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.popup-form .van-cell-group {
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.popup-form .van-cell {
|
||||
padding: 14px 20px;
|
||||
}
|
||||
|
||||
.popup-form .van-field__label {
|
||||
width: 90px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.popup-form .van-field__control {
|
||||
color: #1a1a1a;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.popup-form .van-field__control::placeholder {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin: 0;
|
||||
padding: 16px 20px 20px;
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.form-actions .van-button {
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 地图选点 */
|
||||
.map-picker {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.map-picker-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.map-picker-header h3 {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.map-picker-header .van-icon {
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.map-search {
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-result-item:active {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.result-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.result-address {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selected-location {
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.selected-location .van-icon {
|
||||
color: #FF6700;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.selected-location span {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.datetime-picker-popup {
|
||||
background: #fff;
|
||||
border-radius: 16px 16px 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.picker-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.picker-header h3 {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.picker-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.datetime-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.datetime-input:focus {
|
||||
border-color: #FF6700;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<div class="login-bg">
|
||||
<div class="bg-circle circle-1"></div>
|
||||
<div class="bg-circle circle-2"></div>
|
||||
<div class="bg-circle circle-3"></div>
|
||||
</div>
|
||||
|
||||
<div class="login-content">
|
||||
<div class="logo">
|
||||
<div class="logo-icon">
|
||||
<van-icon name="manager-o" />
|
||||
</div>
|
||||
<h1>泉友米管理后台</h1>
|
||||
<p>欢迎回来</p>
|
||||
</div>
|
||||
|
||||
<van-form @submit="onSubmit" class="login-form">
|
||||
<van-cell-group inset>
|
||||
<van-field
|
||||
v-model="form.username"
|
||||
label="用户名"
|
||||
placeholder="请输入用户名"
|
||||
required
|
||||
left-icon="user-o"
|
||||
clearable
|
||||
/>
|
||||
<van-field
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
label="密码"
|
||||
placeholder="请输入密码"
|
||||
required
|
||||
left-icon="lock"
|
||||
clearable
|
||||
/>
|
||||
</van-cell-group>
|
||||
|
||||
<div class="form-actions">
|
||||
<van-button round block type="primary" native-type="submit" size="large">
|
||||
登录
|
||||
</van-button>
|
||||
</div>
|
||||
</van-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { showToast } from 'vant';
|
||||
import api from '../../api';
|
||||
|
||||
const router = useRouter();
|
||||
const form = ref({
|
||||
username: '',
|
||||
password: ''
|
||||
});
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
const { data } = await api.adminLogin(form.value);
|
||||
localStorage.setItem('token', data.token);
|
||||
showToast('登录成功');
|
||||
router.push('/admin');
|
||||
} catch (error) {
|
||||
showToast('登录失败');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bg-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.1);
|
||||
animation: float 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.circle-1 {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
top: -100px;
|
||||
right: -100px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.circle-2 {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
bottom: -50px;
|
||||
left: -50px;
|
||||
animation-delay: 5s;
|
||||
}
|
||||
|
||||
.circle-3 {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
top: 50%;
|
||||
left: 10%;
|
||||
animation-delay: 10s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
opacity: 0.3;
|
||||
}
|
||||
50% {
|
||||
transform: translate(30px, -30px) scale(1.1);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.login-content {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-align: center;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 20px;
|
||||
background: rgba(255,255,255,0.25);
|
||||
border-radius: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.logo-icon .van-icon {
|
||||
font-size: 42px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.logo h1 {
|
||||
color: #fff;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
margin: 0 0 8px;
|
||||
text-shadow: 0 2px 12px rgba(0,0,0,0.2);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.logo p {
|
||||
color: rgba(255,255,255,0.9);
|
||||
font-size: 15px;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.login-form .van-cell-group {
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(255,255,255,0.95);
|
||||
}
|
||||
|
||||
.login-form .van-field {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.login-form .van-field__label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login-form .van-field__control {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.form-actions .van-button {
|
||||
height: 52px;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.2);
|
||||
background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%);
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-actions .van-button:active {
|
||||
transform: translateY(2px);
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.25);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user