hookehuyr

feat: 添加axios依赖并初始化数据管理

refactor: 重构页面组件以使用本地数据
style: 调整组件代码结构以提高可读性
docs: 更新组件注释和文档
......@@ -11,6 +11,7 @@
},
"dependencies": {
"@vitejs/plugin-vue-jsx": "4.1.2",
"axios": "^1.8.4",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
......
This diff is collapsed. Click to expand it.
......@@ -13,6 +13,7 @@
* @Description: 文件描述
*/
<template>
<AppProvider>
<div class="flex flex-col min-h-screen">
<Header />
<main class="flex-grow">
......@@ -20,12 +21,14 @@
</main>
<Footer />
</div>
</AppProvider>
</template>
<script setup>
import { onMounted } from 'vue'
import Header from './components/layout/Header.vue'
import Footer from './components/layout/Footer.vue'
import AppProvider from './providers/AppProvider.vue'
// 更新文档标题
onMounted(() => {
......
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<circle cx="64" cy="64" r="64" fill="#e0e0e0"/>
<circle cx="64" cy="56" r="24" fill="#bdbdbd"/>
<path d="M64 84c-16 0-48 8-48 24v20h96v-20c0-16-32-24-48-24z" fill="#bdbdbd"/>
</svg>
......@@ -42,7 +42,7 @@
<div class="relative">
<button @click="toggleProfileDropdown" class="flex items-center space-x-2">
<div class="h-8 w-8 rounded-full overflow-hidden border-2 border-green-500">
<img :src="currentUser?.avatar || '/assets/images/avatars/default_avatar.png'" alt="User Avatar" class="h-full w-full object-cover" />
<img :src="currentUser?.avatar || '/src/assets/images/avatars/default_avatar.svg'" alt="User Avatar" class="h-full w-full object-cover" />
</div>
<span class="hidden lg:block text-sm font-medium text-gray-700">{{ currentUser?.name || 'User' }}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
......
......@@ -19,7 +19,7 @@
</button>
</template>
<script setup>
<script>
import { computed } from 'vue'
const buttonVariants = {
......@@ -50,7 +50,10 @@ const buttonRounded = {
const buttonBlock = 'w-full flex justify-center'
const props = defineProps({
const baseClasses = 'flex items-center justify-center font-medium border shadow-sm transition duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed'
export default {
props: {
variant: {
type: String,
default: 'primary',
......@@ -90,13 +93,20 @@ const props = defineProps({
type: String,
default: 'button'
}
})
defineEmits(['click'])
const baseClasses = 'flex items-center justify-center font-medium border shadow-sm transition duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed'
},
emits: ['click'],
setup(props) {
const variantClass = computed(() => buttonVariants[props.variant] || buttonVariants.primary)
const sizeClass = computed(() => buttonSizes[props.size] || buttonSizes.md)
const roundedClass = computed(() => buttonRounded[props.rounded] || buttonRounded.md)
const variantClass = computed(() => buttonVariants[props.variant] || buttonVariants.primary)
const sizeClass = computed(() => buttonSizes[props.size] || buttonSizes.md)
const roundedClass = computed(() => buttonRounded[props.rounded] || buttonRounded.md)
return {
baseClasses,
variantClass,
sizeClass,
roundedClass,
buttonBlock
}
}
}
</script>
......
......@@ -51,7 +51,7 @@
</div>
</template>
<script setup>
<script>
import { computed } from 'vue'
const inputSizes = {
......@@ -60,7 +60,9 @@ const inputSizes = {
lg: 'px-4 py-3 text-lg'
}
const props = defineProps({
export default {
name: 'Input',
props: {
modelValue: {
type: [String, Number],
default: ''
......@@ -114,10 +116,16 @@ const props = defineProps({
type: [String, Object],
default: null
}
})
defineEmits(['update:modelValue', 'blur'])
},
emits: ['update:modelValue', 'blur'],
setup(props) {
const inputId = computed(() => `input-${props.name}`)
const sizeClass = computed(() => inputSizes[props.size] || inputSizes.md)
const inputId = computed(() => `input-${props.name}`)
const sizeClass = computed(() => inputSizes[props.size] || inputSizes.md)
return {
inputId,
sizeClass
}
}
}
</script>
......
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
// Set sizes based on the size prop
const sizeClasses = {
sm: 'max-w-md',
md: 'max-w-lg',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
full: 'max-w-full mx-4'
};
const props = defineProps({
isOpen: {
......@@ -33,15 +42,6 @@ const emit = defineEmits(['close']);
const modalRef = ref(null);
const isMounted = ref(false);
// Set sizes based on the size prop
const sizeClasses = {
sm: 'max-w-md',
md: 'max-w-lg',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
full: 'max-w-full mx-4'
};
const modalSize = computed(() => sizeClasses[props.size] || sizeClasses.md);
// Handle ESC key press
......
......@@ -21,6 +21,12 @@
</div>
</template>
<script>
// Define valid values for variant and size props
const validVariants = ['underline', 'pills', 'bordered'];
const validSizes = ['sm', 'md', 'lg'];
</script>
<script setup>
import { ref, computed } from 'vue';
......@@ -39,13 +45,13 @@ const props = defineProps({
},
variant: {
type: String,
default: 'underline', // 'underline', 'pills', 'bordered'
validator: (value) => ['underline', 'pills', 'bordered'].includes(value)
default: 'underline',
validator: (value) => validVariants.includes(value)
},
size: {
type: String,
default: 'md', // 'sm', 'md', 'lg'
validator: (value) => ['sm', 'md', 'lg'].includes(value)
default: 'md',
validator: (value) => validSizes.includes(value)
}
});
......
{
"activities": [
{
"id": "A0001",
"title": "Vue.js 3.0 读书会",
"description": "每周四晚上8点,我们一起学习Vue.js 3.0的新特性和最佳实践。",
"start_time": "2024-04-18 20:00:00",
"end_time": "2024-04-18 22:00:00",
"location": "线上会议室",
"max_participants": 20,
"current_participants": 5,
"organizer_id": "U0001",
"organizer_name": "张三",
"status": "upcoming",
"is_public": true,
"tags": [
"Vue.js",
"前端开发",
"读书会"
],
"requirements": "需要基本的JavaScript和Vue.js基础知识",
"materials": [
"Vue.js 3.0官方文档",
"示例代码仓库"
],
"created_at": "2024-04-10 10:00:00",
"updated_at": "2024-04-10 10:00:00"
}
]
}
{
"messages": [
{
"id": "M0001",
"sender_id": "system",
"recipient_id": "U0001",
"title": "活动提醒",
"content": "您报名的Vue.js 3.0读书会将在明天晚上8点开始,请准时参加。",
"type": "notification",
"priority": "normal",
"read_status": false,
"created_at": "2024-04-17 10:00:00",
"updated_at": "2024-04-17 10:00:00"
}
]
}
{
"registrations": [
{
"id": "R0001",
"activity_id": "A0001",
"user_id": "U0001",
"registration_time": "2024-04-15 14:30:00",
"status": "confirmed",
"custom_fields": [
{
"field_name": "experience_level",
"field_type": "select",
"field_label": "Vue.js经验水平",
"field_options": ["入门", "进阶", "专家"]
}
],
"custom_answers": {
"experience_level": "进阶"
},
"notes": "期待参加读书会!",
"created_at": "2024-04-15 14:30:00",
"updated_at": "2024-04-15 14:30:00"
}
]
}
{
"users": [
{
"id": "U0001",
"name": "张三",
"email": "zhangsan@example.com",
"avatar": "/assets/images/avatars/default.png",
"role": "member",
"created_at": "2024-01-01 00:00:00",
"updated_at": "2024-01-01 00:00:00",
"last_login": "2024-04-17 10:00:00",
"status": "active",
"preferences": {
"notification_email": true,
"notification_web": true
}
}
]
}
......@@ -379,17 +379,22 @@ const submitRegistration = async () => {
onMounted(async () => {
try {
const activityId = route.params.id
const response = await store.fetchActivity(activityId)
activity.value = response
loading.value = false
const foundActivity = activities.value.find(a => a.id === activityId)
if (foundActivity) {
activity.value = foundActivity
// Check registration status
const registration = await store.checkRegistration(activityId)
const registration = registrations.value.find(r => r.activity_id === activityId)
hasRegistered.value = registration !== null
registrationStatus.value = registration?.status
// Fetch similar activities
similarActivities.value = await store.fetchSimilarActivities(activityId)
// Find similar activities
similarActivities.value = activities.value
.filter(a => a.id !== activityId && a.category === foundActivity.category)
.slice(0, 3)
} else {
error.value = '活动不存在'
}
loading.value = false
} catch (err) {
error.value = err.message
loading.value = false
......
......@@ -258,12 +258,16 @@
<script setup>
import { ref, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAppStore } from '../stores/app'
import activitiesData from '../data/activities.json'
import registrationsData from '../data/registrations.json'
import usersData from '../data/users.json'
import Button from '../components/shared/Button.vue'
const route = useRoute()
const router = useRouter()
const appStore = useAppStore()
const activities = ref(activitiesData.activities)
const registrations = ref(registrationsData.registrations)
const currentUser = ref(usersData.users[0])
const activityId = route.params.activityId
const activity = ref(null)
......@@ -280,21 +284,19 @@ const isSubmitting = ref(false)
// Fetch activity details and user registration
const fetchData = async () => {
try {
if (!appStore.currentUser) {
if (!currentUser.value) {
error.value = '请先登录'
loading.value = false
return
}
if (appStore.activities.length > 0) {
const foundActivity = appStore.getActivityById(activityId)
const foundActivity = activities.value.find(a => a.id === activityId)
if (foundActivity) {
activity.value = foundActivity
// Find user registration for this activity
const registration = appStore.registrations.find(
reg => reg.activity_id === activityId && reg.user_id === appStore.currentUser.id
const registration = registrations.value.find(
reg => reg.activity_id === activityId && reg.user_id === currentUser.value.id
)
if (registration) {
......@@ -314,7 +316,7 @@ const fetchData = async () => {
} else {
error.value = '未找到活动信息'
}
}
loading.value = false
} catch (err) {
console.error('Failed to fetch data:', err)
......
......@@ -86,12 +86,12 @@
</template>
<script setup>
import { ref, computed, watchEffect } from 'vue'
import { useAppStore } from '../stores/app'
import { ref, watchEffect } from 'vue'
import activitiesData from '../data/activities.json'
import ActivityCard from '../components/shared/ActivityCard.vue'
const store = useAppStore()
const { activities, loading } = store
const activities = ref(activitiesData.activities)
const loading = ref(false)
const upcomingActivities = ref([])
const ongoingActivities = ref([])
......
......@@ -169,13 +169,15 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAppStore } from '../stores/app'
import activitiesData from '../data/activities.json'
import registrationsData from '../data/registrations.json'
import Button from '../components/shared/Button.vue'
import Input from '../components/shared/Input.vue'
const route = useRoute()
const router = useRouter()
const store = useAppStore()
const activities = ref(activitiesData.activities)
const registrations = ref(registrationsData.registrations)
const activityId = route.params.activityId
const activity = ref(null)
......@@ -196,9 +198,7 @@ const formErrors = ref({})
// Fetch activity details
onMounted(async () => {
try {
if (store.activities.length > 0) {
const foundActivity = store.getActivityById(activityId)
const foundActivity = activities.value.find(a => a.id === activityId)
if (foundActivity) {
activity.value = foundActivity
......@@ -214,7 +214,7 @@ onMounted(async () => {
} else {
error.value = '未找到活动信息'
}
}
loading.value = false
} catch (err) {
console.error('Failed to fetch activity details:', err)
......
......@@ -17,7 +17,7 @@
<div class="flex flex-col md:flex-row items-center">
<div
class="h-24 w-24 md:h-32 md:w-32 rounded-full overflow-hidden border-4 border-white mb-4 md:mb-0 md:mr-6">
<img :src="currentUser.avatar || '/assets/images/avatars/default_avatar.png'"
<img :src="currentUser.avatar || '/src/assets/images/avatars/default_avatar.svg'"
:alt="currentUser.name" class="h-full w-full object-cover" />
</div>
<div class="text-center md:text-left">
......
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import usersData from '../data/users.json'
import activitiesData from '../data/activities.json'
import registrationsData from '../data/registrations.json'
import messagesData from '../data/messages.json'
export const useAppStore = defineStore('app', () => {
const currentUser = ref(null)
......@@ -10,29 +14,21 @@ export const useAppStore = defineStore('app', () => {
const userMessages = ref([])
// 初始化应用数据
async function fetchInitialData() {
function fetchInitialData() {
try {
loading.value = true
// 获取用户数据
const usersResponse = await fetch('/data/users.json')
const usersData = await usersResponse.json()
currentUser.value = usersData[0]
currentUser.value = usersData.users[0]
// 获取活动数据
const activitiesResponse = await fetch('/data/activities.json')
const activitiesData = await activitiesResponse.json()
activities.value = activitiesData
activities.value = activitiesData.activities
// 获取报名数据
const registrationsResponse = await fetch('/data/registrations.json')
const registrationsData = await registrationsResponse.json()
registrations.value = registrationsData
registrations.value = registrationsData.registrations
// 获取消息数据
const messagesResponse = await fetch('/data/messages.json')
const messagesData = await messagesResponse.json()
userMessages.value = messagesData
userMessages.value = messagesData.messages
loading.value = false
} catch (err) {
......
import axios from 'axios'
// 创建axios实例
const instance = axios.create({
baseURL: '/src/data', // 基础URL,根据实际环境配置
timeout: 10000, // 请求超时时间
headers: {
'Content-Type': 'application/json',
},
})
// 请求拦截器
instance.interceptors.request.use(
(config) => {
// 在发送请求之前做些什么
// 例如:添加token
// const token = localStorage.getItem('token');
// if (token) {
// config.headers.Authorization = `Bearer ${token}`;
// }
return config
},
(error) => {
// 对请求错误做些什么
console.error('Request error:', error)
return Promise.reject(error)
},
)
// 响应拦截器
instance.interceptors.response.use(
(response) => {
// 对响应数据做点什么
return response.data
},
(error) => {
// 对响应错误做点什么
console.error('Response error:', error)
if (error.response) {
switch (error.response.status) {
case 401:
// 未授权处理
break
case 404:
// 资源不存在处理
break
case 500:
// 服务器错误处理
break
default:
break
}
}
return Promise.reject(error)
},
)
export default instance