fix: move admin and web to correct locations
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>笑话大全</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "joke-web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.4.0",
|
||||||
|
"vue-router": "^4.2.5",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"axios": "^1.6.0",
|
||||||
|
"element-plus": "^2.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.0.0",
|
||||||
|
"vite": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<AppHeader />
|
||||||
|
<main class="main-content">
|
||||||
|
<router-view />
|
||||||
|
</main>
|
||||||
|
<AppFooter />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import AppHeader from '@/components/Header.vue'
|
||||||
|
import AppFooter from '@/components/Footer.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #fafafa; }
|
||||||
|
#app { min-height: 100vh; display: flex; flex-direction: column; }
|
||||||
|
.main-content { flex: 1; max-width: 1200px; margin: 0 auto; padding: 20px; width: 100%; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import request from './request'
|
||||||
|
|
||||||
|
export const getTypes = () => request.get('/types')
|
||||||
|
export const getCrowds = () => request.get('/crowds')
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import request from './request'
|
||||||
|
|
||||||
|
export const getJokes = params => request.get('/jokes', { params })
|
||||||
|
export const getJoke = id => request.get(`/jokes/${id}`)
|
||||||
|
export const getRandomJoke = () => request.get('/jokes/random')
|
||||||
|
export const searchJokes = params => request.get('/search', { params })
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export default axios.create({
|
||||||
|
baseURL: '/api',
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<footer class="footer">
|
||||||
|
<p>笑话大全 - 让快乐传递</p>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.footer {
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<header class="header">
|
||||||
|
<div class="header-content">
|
||||||
|
<router-link to="/" class="logo">笑话大全</router-link>
|
||||||
|
<nav class="nav">
|
||||||
|
<router-link to="/">首页</router-link>
|
||||||
|
<router-link to="/category">分类</router-link>
|
||||||
|
<router-link to="/search">搜索</router-link>
|
||||||
|
</nav>
|
||||||
|
<button class="random-btn" @click="handleRandom">随机笑话</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useJokeStore } from '@/stores/joke'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const jokeStore = useJokeStore()
|
||||||
|
|
||||||
|
const handleRandom = async () => {
|
||||||
|
const res = await jokeStore.fetchRandom()
|
||||||
|
router.push(`/detail/${res.id}`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #FF9500 0%, #FFB800 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 0 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.header-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
.nav a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
.nav a:hover, .nav a.router-link-active { opacity: 1; font-weight: 500; }
|
||||||
|
.random-btn {
|
||||||
|
background: white;
|
||||||
|
color: #FF9500;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.random-btn:hover { transform: scale(1.05); }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
app.use(ElementPlus)
|
||||||
|
app.mount('#app')
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{ path: '/', name: 'Home', component: () => import('@/views/home/index.vue') },
|
||||||
|
{ path: '/category', name: 'Category', component: () => import('@/views/category/index.vue') },
|
||||||
|
{ path: '/detail/:id', name: 'Detail', component: () => import('@/views/detail/index.vue') },
|
||||||
|
{ path: '/search', name: 'Search', component: () => import('@/views/search/index.vue') },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { getJokes, getRandomJoke } from '@/api/joke'
|
||||||
|
|
||||||
|
export const useJokeStore = defineStore('joke', {
|
||||||
|
state: () => ({
|
||||||
|
jokes: [],
|
||||||
|
total: 0,
|
||||||
|
loading: false
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
async fetchJokes(params) {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const res = await getJokes(params)
|
||||||
|
this.jokes = res.items
|
||||||
|
this.total = res.total
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async fetchRandom() {
|
||||||
|
return await getRandomJoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="category-page">
|
||||||
|
<h2>分类浏览</h2>
|
||||||
|
<div class="category-section">
|
||||||
|
<h3>按类型</h3>
|
||||||
|
<div class="category-list">
|
||||||
|
<div
|
||||||
|
v-for="type in types"
|
||||||
|
:key="type.id"
|
||||||
|
class="category-card"
|
||||||
|
@click="selectFilter('type_id', type.id)"
|
||||||
|
>
|
||||||
|
<span class="cat-name">{{ type.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="category-section">
|
||||||
|
<h3>按人群</h3>
|
||||||
|
<div class="category-list">
|
||||||
|
<div
|
||||||
|
v-for="crowd in crowds"
|
||||||
|
:key="crowd.id"
|
||||||
|
class="category-card"
|
||||||
|
@click="selectFilter('crowd_id', crowd.id)"
|
||||||
|
>
|
||||||
|
<span class="cat-name">{{ crowd.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { getTypes, getCrowds } from '@/api/category'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const types = ref([])
|
||||||
|
const crowds = ref([])
|
||||||
|
|
||||||
|
const selectFilter = (key, value) => {
|
||||||
|
router.push({ path: '/', query: { [key]: value } })
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
types.value = await getTypes()
|
||||||
|
crowds.value = await getCrowds()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.category-page h2 { margin-bottom: 20px; }
|
||||||
|
.category-section { margin-bottom: 30px; }
|
||||||
|
.category-section h3 { margin-bottom: 15px; color: #666; }
|
||||||
|
.category-list { display: flex; flex-wrap: wrap; gap: 12px; }
|
||||||
|
.category-card {
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.category-card:hover {
|
||||||
|
background: #FF9500;
|
||||||
|
color: white;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
.cat-name { font-size: 15px; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div class="detail-page" v-if="joke">
|
||||||
|
<div class="joke-detail">
|
||||||
|
<div class="tags">
|
||||||
|
<span class="tag type-tag" v-if="joke.type_name">{{ joke.type_name }}</span>
|
||||||
|
<span class="tag crowd-tag" v-if="joke.crowd_name">{{ joke.crowd_name }}</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="title">{{ joke.title }}</h1>
|
||||||
|
<div class="content">{{ joke.content }}</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="action-btn" @click="handleRandom">再看一条</button>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
浏览 {{ joke.view_count }} | 赞 {{ joke.like_count }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { getJoke } from '@/api/joke'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const joke = ref(null)
|
||||||
|
|
||||||
|
const loadJoke = async () => {
|
||||||
|
joke.value = await getJoke(route.params.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRandom = () => {
|
||||||
|
router.push('/')
|
||||||
|
setTimeout(() => window.location.reload(), 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => loadJoke())
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.joke-detail {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
.tags { display: flex; gap: 10px; margin-bottom: 15px; }
|
||||||
|
.tag { padding: 6px 14px; border-radius: 15px; font-size: 13px; }
|
||||||
|
.type-tag { background: #FFF3E0; color: #FF9500; }
|
||||||
|
.crowd-tag { background: #E3F2FD; color: #2196F3; }
|
||||||
|
.title { font-size: 24px; margin-bottom: 20px; color: #333; }
|
||||||
|
.content {
|
||||||
|
line-height: 1.8;
|
||||||
|
font-size: 17px;
|
||||||
|
color: #444;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.actions { margin-top: 30px; }
|
||||||
|
.action-btn {
|
||||||
|
padding: 12px 30px;
|
||||||
|
background: linear-gradient(135deg, #FF9500, #FFB800);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.action-btn:hover { opacity: 0.9; }
|
||||||
|
.meta { margin-top: 20px; color: #999; font-size: 14px; text-align: center; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<div class="joke-card" @click="$router.push(`/detail/${joke.id}`)">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="tag type-tag">{{ joke.type_name || '段子' }}</span>
|
||||||
|
<span class="tag crowd-tag" v-if="joke.crowd_name">{{ joke.crowd_name }}</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="title">{{ joke.title }}</h3>
|
||||||
|
<p class="content">{{ joke.content }}</p>
|
||||||
|
<div class="card-footer">
|
||||||
|
<span class="views">浏览 {{ joke.view_count }}</span>
|
||||||
|
<span class="likes">赞 {{ joke.like_count }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
joke: { type: Object, required: true }
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.joke-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.joke-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.card-header { display: flex; gap: 8px; margin-bottom: 12px; }
|
||||||
|
.tag {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.type-tag { background: #FFF3E0; color: #FF9500; }
|
||||||
|
.crowd-tag { background: #E3F2FD; color: #2196F3; }
|
||||||
|
.title { font-size: 18px; margin-bottom: 10px; color: #333; }
|
||||||
|
.content {
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
color: #999;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<div class="home">
|
||||||
|
<div class="filter-bar">
|
||||||
|
<div class="filter-group">
|
||||||
|
<span>类型:</span>
|
||||||
|
<el-select v-model="filter.type_id" placeholder="全部" clearable @change="loadJokes">
|
||||||
|
<el-option v-for="t in types" :key="t.id" :label="t.name" :value="t.id" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<span>人群:</span>
|
||||||
|
<el-select v-model="filter.crowd_id" placeholder="全部" clearable @change="loadJokes">
|
||||||
|
<el-option v-for="c in crowds" :key="c.id" :label="c.name" :value="c.id" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="joke-grid">
|
||||||
|
<JokeCard v-for="joke in jokeStore.jokes" :key="joke.id" :joke="joke" />
|
||||||
|
</div>
|
||||||
|
<el-pagination
|
||||||
|
v-if="jokeStore.total > 20"
|
||||||
|
:current-page="page"
|
||||||
|
:page-size="20"
|
||||||
|
:total="jokeStore.total"
|
||||||
|
layout="prev, pager, next"
|
||||||
|
@current-change="loadJokes"
|
||||||
|
style="margin-top: 20px; justify-content: center; display: flex"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useJokeStore } from '@/stores/joke'
|
||||||
|
import { getTypes, getCrowds } from '@/api/category'
|
||||||
|
import JokeCard from './components/JokeCard.vue'
|
||||||
|
|
||||||
|
const jokeStore = useJokeStore()
|
||||||
|
const types = ref([])
|
||||||
|
const crowds = ref([])
|
||||||
|
const page = ref(1)
|
||||||
|
const filter = ref({ type_id: null, crowd_id: null })
|
||||||
|
|
||||||
|
const loadJokes = async (p = 1) => {
|
||||||
|
page.value = p
|
||||||
|
await jokeStore.fetchJokes({ page: p, page_size: 20, ...filter.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
types.value = await getTypes()
|
||||||
|
crowds.value = await getCrowds()
|
||||||
|
await loadJokes()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.filter-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.filter-group { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.joke-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<div class="search-page">
|
||||||
|
<div class="search-box">
|
||||||
|
<el-input
|
||||||
|
v-model="keyword"
|
||||||
|
placeholder="搜索笑话..."
|
||||||
|
size="large"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="handleSearch">搜索</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
<div class="search-results" v-if="results.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="joke in results"
|
||||||
|
:key="joke.id"
|
||||||
|
class="result-item"
|
||||||
|
@click="$router.push(`/detail/${joke.id}`)"
|
||||||
|
>
|
||||||
|
<h3>{{ joke.title }}</h3>
|
||||||
|
<p>{{ joke.content }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-else-if="searched" description="未找到相关笑话" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { getJokes } from '@/api/joke'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const keyword = ref(route.query.q || '')
|
||||||
|
const results = ref([])
|
||||||
|
const searched = ref(false)
|
||||||
|
|
||||||
|
const handleSearch = async () => {
|
||||||
|
if (!keyword.value.trim()) return
|
||||||
|
searched.value = true
|
||||||
|
const res = await getJokes({ page: 1, page_size: 50 })
|
||||||
|
results.value = res.items.filter(j =>
|
||||||
|
j.title.includes(keyword.value) || j.content.includes(keyword.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyword.value) handleSearch()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-box { max-width: 600px; margin: 0 auto 30px; }
|
||||||
|
.search-results { display: flex; flex-direction: column; gap: 15px; }
|
||||||
|
.result-item {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.result-item:hover { transform: translateX(5px); }
|
||||||
|
.result-item h3 { margin-bottom: 8px; color: #333; }
|
||||||
|
.result-item p { color: #666; line-height: 1.5; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8000',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user