code_&_ReadMe

cbd
Heng Chen 8 months ago
parent 1aaa178473
commit 9b57924580

@ -0,0 +1,45 @@
## YouCo - 旅行交友新方式
YouCo是一个创新的旅行社交平台致力于为用户提供独特的旅行交友体验。通过我们的平台用户可以找到志同道合的旅伴分享旅行故事规划旅程预算探索世界各地的美景。
## 主要功能
- 盲盒交友:随机匹配旅行伴侣
- 预算规划:智能旅行费用规划工具
- 社区互动:分享旅行故事和照片
- 个人信息:查看和编辑个人信息
## 技术栈
- Vue.js 2.6.14
- Vue Router 3.6.5
- Material Design Icons
- Core.js 3.8.3
## 开始使用
(1) 克隆项目
git clone https://github.com/Marcus4477/Final_Edition.git
cd youco
(2) 安装依赖
npm install
(3) 运行开发服务器
npm run serve
## 项目结构
- /src
- /assets - 静态资源文件
- /components - Vue组件
- /data - 数据文件
- /router - 路由配置
- App.vue - 根组件
- main.js - 入口文件
## 浏览器支持
- Chrome (最新版本)
- Firefox (最新版本)
- Safari (最新版本)
- Edge (最新版本)
## 贡献指南
1. Fork 项目
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 开启 Pull Request

@ -0,0 +1,13 @@
{
"name": "YouCo",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/@mdi/font": {
"version": "7.4.47",
"resolved": "https://registry.npmmirror.com/@mdi/font/-/font-7.4.47.tgz",
"integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==",
"license": "Apache-2.0"
}
}
}

@ -0,0 +1,3 @@
Disclaimer:
Hi there, thanks for contributing! Before anything else, please ensure you didn't mean to create an issue on the main MaterialDesign repo instead.
If this is intentional, just erase this message. Thanks!

@ -0,0 +1,20 @@
Pictogrammers Free License
--------------------------
This icon collection is released as free, open source, and GPL friendly by
the [Pictogrammers](http://pictogrammers.com/) icon group. You may use it
for commercial projects, open source projects, or anything really.
# Icons: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
Some of the icons are redistributed under the Apache 2.0 license. All other
icons are either redistributed under their respective licenses or are
distributed under the Apache 2.0 license.
# Fonts: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
All web and desktop fonts are distributed under the Apache 2.0 license. Web
and desktop fonts contain some icons that are redistributed under the Apache
2.0 license. All other icons are either redistributed under their respective
licenses or are distributed under the Apache 2.0 license.
# Code: MIT (https://opensource.org/licenses/MIT)
The MIT license applies to all non-font and non-icon files.

@ -0,0 +1,25 @@
> *Note:* Please use the main [MaterialDesign](https://github.com/Templarian/MaterialDesign/issues) repo to report issues. This repo is for distribution of the Webfont files only.
# Webfont - Material Design Icons
Webfont distribution for the [Material Design Icons](https://materialdesignicons.com).
```
npm install @mdi/font
```
> Package built with [@mdi/font-build](https://github.com/Templarian/MaterialDesign-Font-Build).
## Related Packages
[NPM @MDI Organization](https://npmjs.com/org/mdi)
- JavaScript/Typescript: [MaterialDesign-JS](https://github.com/Templarian/MaterialDesign-JS)
- SVG: [MaterialDesign-SVG](https://github.com/Templarian/MaterialDesign-SVG)
- Font-Build [MaterialDesign-Font-Build](https://github.com/Templarian/MaterialDesign-Font-Build)
- Desktop Font: [MaterialDesign-Font](https://github.com/Templarian/MaterialDesign-Font)
## Learn More
- [MaterialDesignIcons.com](https://materialdesignicons.com)
- https://github.com/Templarian/MaterialDesign

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,30 @@
{
"name": "@mdi/font",
"version": "7.4.47",
"description": "Dist for Material Design Webfont. This includes the Stock and Community icons in a single webfont collection.",
"style": "css/materialdesignicons.css",
"scripts": {
"verify": "node scripts/verify.js",
"prepublish": "node scripts/verify.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://github.com/Templarian/MaterialDesign-Webfont.git"
},
"keywords": [
"material",
"design",
"icons",
"webfont"
],
"author": {
"name": "Austin Andrews",
"web": "http://twitter.com/templarian"
},
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/Templarian/MaterialDesign/issues"
},
"homepage": "https://materialdesignicons.com"
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,41 @@
const fs = require('fs');
// Parse package.json
const packageFile = './package.json';
const packageText = fs.readFileSync(packageFile, 'utf8');
const packageJson = JSON.parse(packageText);
const packageVersion = packageJson.version;
// Check for preview.html
const previewFile = './preview.html';
if (!fs.existsSync(previewFile)) {
throw new Error('Error: preview.html must exist!');
}
const previewText = fs.readFileSync(previewFile, 'utf8');
const parts = previewText.match(/<span class="version">([^<]+)<\/span>/);
if (parts === null) {
// Did you modify preview.html file ???
throw new Error('Error: preview.html version string not found!');
}
// Never include a index.html file!
const indexFile = './index.html';
if (fs.existsSync(indexFile)) {
throw new Error('Error: index.html should not exist, only preview.html');
}
const previewVersion = parts[1];
if (packageVersion != previewVersion) {
// Not good, almost published the wrong version
throw new Error(`Error: package "${packageVersion}" != preview.html "${previewVersion}"`);
}
// Verify SCSS Version
const scssVariablesFile = './scss/_variables.scss';
const scssVariablesText = fs.readFileSync(scssVariablesFile, 'utf8');
const vParts = scssVariablesText.match(/"(\d+).(\d+).(\d+)" !default;/);
if (vParts === null) {
throw new Error('Error: Could not parse SCSS version!');
}
const scssVersion = `${vParts[1]}.${vParts[2]}.${vParts[3]}`;
if (packageVersion != scssVersion) {
// Not good, almost published the wrong version
throw new Error(`Error: package "${packageVersion}" != scss/variables.scss "${previewVersion}"`);
}
console.log(`Success: ${packageVersion} looks good!`);

@ -0,0 +1,27 @@
// From Font Awesome
.#{$mdi-css-prefix}-spin:before {
-webkit-animation: #{$mdi-css-prefix}-spin 2s infinite linear;
animation: #{$mdi-css-prefix}-spin 2s infinite linear;
}
@-webkit-keyframes #{$mdi-css-prefix}-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes #{$mdi-css-prefix}-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}

@ -0,0 +1,10 @@
.#{$mdi-css-prefix}:before,
.#{$mdi-css-prefix}-set {
display: inline-block;
font: normal normal normal #{$mdi-font-size-base}/1 '#{$mdi-font-name}'; // shortening font declaration
font-size: inherit; // can't have font-size inherit on line above, so need to override
text-rendering: auto; // optimizelegibility throws things off #1094
line-height: inherit;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

@ -0,0 +1,65 @@
$mdi-sizes: 18 24 36 48 !default;
@each $mdi-size in $mdi-sizes {
.#{$mdi-css-prefix}-#{$mdi-size}px {
&.#{$mdi-css-prefix}-set,
&.#{$mdi-css-prefix}:before {
font-size: $mdi-size * 1px;
}
}
}
.#{$mdi-css-prefix}-dark {
&:before {
color: rgba(0, 0, 0, 0.54);
}
&.#{$mdi-css-prefix}-inactive:before {
color: rgba(0, 0, 0, 0.26);
}
}
.#{$mdi-css-prefix}-light {
&:before {
color: rgba(255, 255, 255, 1);
}
&.#{$mdi-css-prefix}-inactive:before {
color: rgba(255, 255, 255, 0.3);
}
}
$mdi-degrees: 45 90 135 180 225 270 315 !default;
@each $mdi-degree in $mdi-degrees {
.#{$mdi-css-prefix}-rotate-#{$mdi-degree}{
&:before {
-webkit-transform: rotate(#{$mdi-degree}deg);
-ms-transform: rotate(#{$mdi-degree}deg);
transform: rotate(#{$mdi-degree}deg);
}
/*
// Not included in production
&.#{$mdi-css-prefix}-flip-h:before {
-webkit-transform: scaleX(-1) rotate(#{$mdi-degree}deg);
transform: scaleX(-1) rotate(#{$mdi-degree}deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.#{$mdi-css-prefix}-flip-v:before {
-webkit-transform: scaleY(-1) rotate(#{$mdi-degree}deg);
-ms-transform: rotate(#{$mdi-degree}deg);
transform: scaleY(-1) rotate(#{$mdi-degree}deg);
filter: FlipV;
-ms-filter: "FlipV";
}
*/
}
}
.#{$mdi-css-prefix}-flip-h:before {
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
filter: FlipH;
-ms-filter: "FlipH";
}
.#{$mdi-css-prefix}-flip-v:before {
-webkit-transform: scaleY(-1);
transform: scaleY(-1);
filter: FlipV;
-ms-filter: "FlipV";
}

@ -0,0 +1,20 @@
@function char($character-code) {
@if function-exists("selector-append") {
@return unquote("\"\\#{$character-code}\"");
}
@if "\\#{'x'}" == "\\x" {
@return str-slice("\x", 1, 1) + $character-code;
}
@else {
@return #{"\"\\"}#{$character-code + "\""};
}
}
@function mdi($name) {
@if map-has-key($mdi-icons, $name) == false {
@warn "Icon #{$name} not found.";
@return "";
}
@return char(map-get($mdi-icons, $name));
}

@ -0,0 +1,10 @@
@each $key, $value in $mdi-icons {
.#{$mdi-css-prefix}-#{$key}::before {
content: char($value);
}
}
.#{$mdi-css-prefix}-blank::before {
content: "\F68C";
visibility: hidden;
}

@ -0,0 +1,10 @@
@font-face {
font-family: '#{$mdi-font-name}';
src: url('#{$mdi-font-path}/#{$mdi-filename}-webfont.eot?v=#{$mdi-version}');
src: url('#{$mdi-font-path}/#{$mdi-filename}-webfont.eot?#iefix&v=#{$mdi-version}') format('embedded-opentype'),
url('#{$mdi-font-path}/#{$mdi-filename}-webfont.woff2?v=#{$mdi-version}') format('woff2'),
url('#{$mdi-font-path}/#{$mdi-filename}-webfont.woff?v=#{$mdi-version}') format('woff'),
url('#{$mdi-font-path}/#{$mdi-filename}-webfont.ttf?v=#{$mdi-version}') format('truetype');
font-weight: normal;
font-style: normal;
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,8 @@
/* MaterialDesignIcons.com */
@import "variables";
@import "functions";
@import "path";
@import "core";
@import "icons";
@import "extras";
@import "animated";

@ -0,0 +1,18 @@
{
"name": "YouCo",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@mdi/font": "^7.4.47"
}
},
"node_modules/@mdi/font": {
"version": "7.4.47",
"resolved": "https://registry.npmmirror.com/@mdi/font/-/font-7.4.47.tgz",
"integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==",
"license": "Apache-2.0"
}
}
}

@ -0,0 +1,5 @@
{
"dependencies": {
"@mdi/font": "^7.4.47"
}
}

@ -0,0 +1,12 @@
{
"name": "travel-db-migration",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}

@ -0,0 +1,12 @@
{
"name": "travel-db-migration",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

@ -0,0 +1,24 @@
# youco
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,45 @@
{
"name": "youco",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.8.3",
"vue": "^2.6.14",
"vue-router": "^3.6.5"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"vue-template-compiler": "^2.6.14"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser",
"requireConfigFile": false
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>YouCo - 旅行交友新方式</title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 MiB

@ -0,0 +1,260 @@
<template>
<div class="blindbox-result">
<div class="result-content">
<h2>盲盒交友结果</h2>
<div class="user-profile">
<div class="profile-header">
<img :src="userProfile.avatar" alt="用户头像" class="avatar">
<div class="basic-info">
<h3>{{ userProfile.name }}, {{ userProfile.age }} ({{ userProfile.gender }})</h3>
<p>{{ userProfile.location }}</p>
</div>
<div class="rating">
<span class="rating-text">{{ userProfile.rating.toFixed(1) }}</span>
<div class="stars">
<span v-for="i in 5" :key="i" :class="{'filled': i <= Math.round(userProfile.rating)}"></span>
</div>
</div>
</div>
<div class="profile-details">
<div class="detail-item">
<h4><i class="mdi mdi-heart"></i> 爱好</h4>
<p>{{ userProfile.hobbies.join(', ') }}</p>
</div>
<div class="detail-item">
<h4><i class="mdi mdi-airplane"></i> 旅行爱好</h4>
<p>{{ userProfile.travelPreferences.join(', ') }}</p>
</div>
<div class="detail-item">
<h4><i class="mdi mdi-translate"></i> 语言</h4>
<p>{{ userProfile.languages.join(', ') }}</p>
</div>
<div class="detail-item">
<h4><i class="mdi mdi-map-marker"></i> 旅行过的地方</h4>
<p>{{ userProfile.traveledPlaces.join(', ') }}</p>
</div>
<div class="detail-item">
<h4><i class="mdi mdi-bag-personal"></i> 旅行风格</h4>
<p>{{ userProfile.travelStyle }}</p>
</div>
<div class="detail-item">
<h4><i class="mdi mdi-account-details"></i> 个人简介</h4>
<p>{{ userProfile.bio }}</p>
</div>
<div class="detail-item">
<h4><i class="mdi mdi-phone"></i> 联系方式</h4>
<p v-for="(contact, index) in userProfile.contacts" :key="index">
<i :class="contact.icon"></i> {{ contact.type }}: {{ contact.value }}
</p>
</div>
<div class="detail-item">
<h4><i class="mdi mdi-calendar-clock"></i> 最近旅行</h4>
<p>{{ userProfile.recentTravel.join(' | ') }}</p>
</div>
</div>
<div class="reviews">
<h4><i class="mdi mdi-comment-text-multiple"></i> 驴友评价</h4>
<div v-for="(review, index) in userProfile.reviews" :key="index" class="review-item">
<p><strong>{{ review.name }}:</strong> {{ review.comment }}</p>
</div>
</div>
</div>
<button @click="$emit('close')" class="close-button">关闭</button>
</div>
</div>
</template>
<script>
import { userProfiles } from '../data/userProfiles';
export default {
name: 'BlindboxResult',
props: {
blindboxData: {
type: Object,
required: true
}
},
data() {
//
const randomProfile = userProfiles[Math.floor(Math.random() * userProfiles.length)];
return {
userProfile: randomProfile
}
}
}
</script>
<style scoped>
.blindbox-result {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.result-content {
background-color: white;
padding: 30px;
border-radius: 20px;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2);
max-width: 800px;
width: 95%;
max-height: 70vh;
overflow-y: auto;
}
h2 {
color: #333;
margin-bottom: 20px;
text-align: center;
font-size: 2em;
text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
}
.user-profile {
margin-bottom: 20px;
}
.profile-header {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.avatar {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
margin-right: 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.basic-info {
flex-grow: 1;
}
.basic-info h3 {
margin: 0;
color: #333;
font-size: 1.5em;
}
.basic-info p {
margin: 5px 0;
color: #666;
font-size: 1em;
}
.rating {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.rating-text {
font-size: 2em;
font-weight: bold;
color: #ffd700;
text-shadow: 1px 1px 2px rgba(0,0,0,0.2);
}
.stars {
font-size: 1.2em;
color: #ffd700;
}
.profile-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.detail-item {
background-color: #f9f9f9;
padding: 12px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.detail-item h4 {
margin: 0 0 8px;
color: #333;
font-size: 1.1em;
display: flex;
align-items: center;
}
.detail-item h4 i {
margin-right: 6px;
color: #4a90e2;
}
.detail-item p {
margin: 0;
color: #666;
font-size: 0.9em;
}
.reviews {
margin-top: 20px;
}
.review-item {
background-color: #f9f9f9;
padding: 10px;
border-radius: 8px;
margin-bottom: 10px;
}
.review-item p {
margin: 0;
font-size: 0.9em;
}
.close-button {
background-color: #007AFF;
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
margin-top: 20px;
width: 100%;
font-size: 1em;
transition: background-color 0.3s ease;
}
.close-button:hover {
background-color: #0056b3;
}
/* 添加一些装饰性元素 */
.result-content::before,
.result-content::after {
content: '✈';
position: absolute;
font-size: 2em;
color: #4a90e2;
opacity: 0.1;
}
.result-content::before {
top: 10px;
left: 10px;
transform: rotate(-30deg);
}
.result-content::after {
bottom: 10px;
right: 10px;
transform: rotate(30deg);
}
</style>

@ -0,0 +1,506 @@
<template>
<div class="budget-result-overlay">
<div class="budget-result">
<div class="header-actions">
<button class="close-button-mini" @click="$emit('close')">
<i class="mdi mdi-close"></i>
</button>
</div>
<div class="result-header">
<h2>{{ budgetData.destination }}旅行预算方案</h2>
<div class="budget-overview">
<div class="total-budget">
<span class="label">总预算</span>
<span class="amount">¥{{ budgetData.budget }}</span>
</div>
</div>
</div>
<div class="budget-details">
<!-- 预算分配 -->
<div class="allocation-section">
<h3>预算分配</h3>
<div class="allocation-items">
<div v-for="(item, category) in plan.budgetAllocation"
:key="category"
class="allocation-item">
<div class="category-header">
<span class="category-name">{{ getCategoryName(category) }}</span>
<span class="amount">¥{{ item.amount }}</span>
</div>
<div class="details">
<p class="type">{{ item.type }}</p>
<ul class="suggestions">
<li v-for="(detail, index) in item.details"
:key="index">{{ detail }}</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 旅行方案推荐 -->
<div class="destination-section">
<h3>旅行方案推荐</h3>
<div class="plan-details">
<div class="plan-overview">
<h4>推荐方案 - {{ plan.budgetLevel === 'free' ? '免费' : plan.budgetLevel === 'low' ? '经济' : plan.budgetLevel === 'medium' ? '舒适' : '豪华' }}</h4>
<p class="plan-description">{{ plan.destinationInfo.description }}</p>
</div>
<!-- 住宿推荐 -->
<div class="accommodation-section">
<h4>住宿推荐</h4>
<div class="accommodation-list">
<div v-for="(option, index) in plan.destinationInfo.accommodation.options"
:key="index"
class="accommodation-item">
<div class="accommodation-header">
<h5>{{ option.name }}</h5>
<span class="price">{{ option.price }}</span>
</div>
<p class="type">{{ option.type }}</p>
<p class="location">位置{{ option.location }}</p>
<div class="features">
<span v-for="(feature, idx) in option.features"
:key="idx"
class="feature-tag">
{{ feature }}
</span>
</div>
</div>
</div>
<div class="accommodation-tips">
<h5>住宿提示</h5>
<ul>
<li v-for="(tip, index) in plan.destinationInfo.accommodation.tips"
:key="index">
{{ tip }}
</li>
</ul>
</div>
</div>
<div class="core-attractions">
<h4>核心景点行程</h4>
<div class="attractions-list">
<div v-for="(attraction, index) in plan.destinationInfo.attractions"
:key="index"
class="attraction-item">
<h5>
{{ attraction.name }}
<span class="price">¥{{ attraction.price }}</span>
</h5>
<p>{{ attraction.description }}</p>
<p class="best-time">最佳游览时间{{ attraction.bestTime }}</p>
<div class="attraction-tips">
<span v-for="(tip, idx) in attraction.tips"
:key="idx"
class="tip-tag">
{{ tip }}
</span>
</div>
<a :href="attraction.link"
target="_blank"
class="booking-link">
预订链接
<i class="mdi mdi-open-in-new"></i>
</a>
</div>
</div>
</div>
<div class="recommended-activities">
<h4>特色体验</h4>
<div class="activities-list">
<div v-for="(activity, index) in plan.destinationInfo.activities"
:key="index"
class="activity-item">
<h5>{{ activity.name }}</h5>
<p>{{ activity.description }}</p>
<p class="price-range">参考价格: {{ activity.priceRange }}</p>
<p class="duration">建议游玩时长{{ activity.duration }}</p>
<p class="best-time">最佳体验时间{{ activity.bestTime }}</p>
<div class="activity-tips">
<span v-for="(tip, idx) in activity.tips"
:key="idx"
class="tip-tag">
{{ tip }}
</span>
</div>
</div>
</div>
</div>
<div class="dining-recommendations">
<h4>餐饮推荐</h4>
<div class="dining-list">
<div v-for="(dining, index) in plan.destinationInfo.dining"
:key="index"
class="dining-item">
<h5>{{ dining.name }}</h5>
<p>{{ dining.description }}</p>
<p class="price-range">人均: {{ dining.averagePrice }}</p>
<p class="best-time">推荐用餐时间{{ dining.bestTime }}</p>
<div class="recommendations">
<h6>推荐美食</h6>
<span v-for="(food, idx) in dining.recommendations"
:key="idx"
class="food-tag">
{{ food }}
</span>
</div>
</div>
</div>
</div>
<div class="travel-tips">
<h4>实用贴士</h4>
<ul>
<li v-for="(tip, index) in plan.destinationInfo.tips"
:key="index">{{ tip }}</li>
</ul>
</div>
</div>
</div>
</div>
<div class="action-buttons">
<button class="save-button">保存方案</button>
<button class="close-button" @click="$emit('close')"></button>
</div>
</div>
</div>
</template>
<script>
import { generatePlan } from '../data/travelPlans';
export default {
name: 'BudgetResult',
props: {
budgetData: {
type: Object,
required: true
}
},
computed: {
plan() {
return generatePlan(this.budgetData.destination, this.budgetData.budget);
}
},
methods: {
getCategoryName(category) {
const names = {
transportation: '交通',
accommodation: '住宿',
food: '餐饮',
activities: '活动'
};
return names[category] || category;
}
}
}
</script>
<style scoped>
.budget-result-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.budget-result {
background-color: white;
border-radius: 12px;
padding: 24px;
width: 80%;
max-width: 1200px;
max-height: 90vh;
overflow-y: auto;
position: relative;
}
.header-actions {
position: absolute;
right: 16px;
top: 16px;
}
.close-button-mini {
background: none;
border: none;
color: #666;
cursor: pointer;
padding: 8px;
border-radius: 50%;
transition: background-color 0.3s;
}
.close-button-mini:hover {
background-color: rgba(0, 0, 0, 0.1);
}
.result-header {
margin-bottom: 24px;
text-align: center;
}
.budget-overview {
margin-top: 12px;
}
.total-budget {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.total-budget .amount {
font-size: 24px;
font-weight: bold;
color: #28a745;
}
.allocation-section {
margin-bottom: 32px;
}
.allocation-items {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-top: 16px;
}
.allocation-item {
background-color: #f8f9fa;
border-radius: 8px;
padding: 16px;
}
.category-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.category-name {
font-weight: bold;
color: #333;
}
.amount {
color: #28a745;
font-weight: bold;
}
.type {
color: #666;
margin-bottom: 8px;
}
.suggestions {
list-style: none;
padding-left: 0;
}
.suggestions li {
color: #666;
margin-bottom: 4px;
font-size: 14px;
}
.destination-section {
background-color: #fff;
border-radius: 8px;
padding: 20px;
}
.plan-overview {
margin-bottom: 24px;
}
.accommodation-section {
margin: 24px 0;
background-color: #f8f9fa;
border-radius: 8px;
padding: 20px;
}
.accommodation-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-top: 16px;
}
.accommodation-item {
background-color: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.features {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.feature-tag {
background-color: #e9ecef;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
color: #666;
}
.attractions-list, .activities-list, .dining-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 16px;
}
.attraction-item, .activity-item, .dining-item {
background-color: #f8f9fa;
border-radius: 8px;
padding: 16px;
position: relative;
}
.best-time {
color: #666;
font-size: 14px;
margin: 8px 0;
}
.tip-tag, .food-tag {
display: inline-block;
background-color: #e9ecef;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
color: #666;
margin: 4px;
}
.booking-link {
display: inline-flex;
align-items: center;
color: #007bff;
text-decoration: none;
margin-top: 12px;
font-size: 14px;
}
.booking-link i {
margin-left: 4px;
}
.booking-link:hover {
text-decoration: underline;
}
.recommendations {
margin-top: 12px;
}
.recommendations h6 {
margin-bottom: 8px;
color: #333;
}
.travel-tips {
margin-top: 24px;
background-color: #f8f9fa;
border-radius: 8px;
padding: 16px;
}
.travel-tips ul {
list-style: none;
padding: 0;
margin: 0;
}
.travel-tips li {
color: #666;
margin-bottom: 8px;
padding-left: 20px;
position: relative;
}
.travel-tips li:before {
content: "•";
position: absolute;
left: 0;
color: #28a745;
}
.action-buttons {
display: flex;
gap: 16px;
margin-top: 24px;
}
.save-button, .close-button {
flex: 1;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
border: none;
transition: background-color 0.3s;
}
.save-button {
background-color: #28a745;
color: white;
}
.save-button:hover {
background-color: #218838;
}
.close-button {
background-color: #6c757d;
color: white;
}
.close-button:hover {
background-color: #5a6268;
}
@media (max-width: 768px) {
.budget-result {
width: 95%;
padding: 16px;
}
.allocation-items,
.attractions-list,
.activities-list,
.dining-list,
.accommodation-list {
grid-template-columns: 1fr;
}
.action-buttons {
flex-direction: column;
}
}
</style>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,458 @@
<template>
<div class="create-post-modal" v-if="show" @click.self="close">
<div class="modal-content">
<div class="modal-header">
<h3>发布动态</h3>
<button class="close-btn" @click="close">×</button>
</div>
<div class="modal-body">
<textarea
v-model="postContent"
placeholder="分享你的旅行故事..."
class="content-input"
rows="6"
></textarea>
<div class="image-upload">
<div class="upload-preview" v-if="previewImages.length">
<div v-for="(img, index) in previewImages"
:key="index"
class="preview-item">
<img :src="img" alt="预览图片">
<button class="remove-img" @click="removeImage(index)">×</button>
</div>
</div>
<label class="upload-btn" v-if="previewImages.length < 9">
<input
type="file"
accept="image/*"
multiple
@change="handleImageUpload"
ref="fileInput"
>
<span class="mdi mdi-image-plus"></span>
添加图片
</label>
</div>
<div class="tags-input">
<div class="tags-container">
<span v-for="(tag, index) in tags"
:key="index"
class="tag">
#{{ tag }}
<button @click="removeTag(index)" class="remove-tag">×</button>
</span>
<input
v-model="newTag"
@keyup.enter="addTag"
placeholder="添加标签 (回车确认)"
maxlength="20"
class="tag-input"
>
</div>
</div>
</div>
<div class="modal-footer">
<button class="cancel-btn" @click="close"></button>
<button class="publish-btn"
@click="publishPost"
:disabled="!isValid">
发布
</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'CreatePost',
props: {
show: {
type: Boolean,
default: false
}
},
data() {
return {
postContent: '',
previewImages: [],
tags: [],
newTag: ''
}
},
computed: {
isValid() {
return this.postContent.trim().length > 0
}
},
methods: {
close() {
this.$emit('close')
this.resetForm()
},
handleImageUpload(event) {
const files = event.target.files
if (files) {
for (let file of files) {
if (this.previewImages.length >= 9) break
const reader = new FileReader()
reader.onload = (e) => {
this.previewImages.push(e.target.result)
}
reader.readAsDataURL(file)
}
}
this.$refs.fileInput.value = '' // input
},
removeImage(index) {
this.previewImages.splice(index, 1)
},
addTag() {
const tag = this.newTag.trim()
if (tag && !this.tags.includes(tag) && this.tags.length < 5) {
this.tags.push(tag)
this.newTag = ''
}
},
removeTag(index) {
this.tags.splice(index, 1)
},
publishPost() {
if (!this.isValid) return
const post = {
content: this.postContent,
images: this.previewImages,
tags: this.tags,
time: '刚刚',
likes: 0,
comments: 0
}
this.$emit('publish', post)
this.resetForm()
this.close()
},
resetForm() {
this.postContent = ''
this.previewImages = []
this.tags = []
this.newTag = ''
}
}
}
</script>
<style scoped>
.create-post-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
backdrop-filter: blur(5px);
}
.modal-content {
background: white;
border-radius: 20px;
width: 800px;
height: 600px;
position: relative;
display: flex;
flex-direction: column;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
animation: modalSlideIn 0.3s ease;
overflow: hidden;
}
@keyframes modalSlideIn {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.modal-header {
padding: 20px 24px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
}
.modal-header h3 {
margin: 0;
font-size: 24px;
color: #333;
font-weight: 600;
}
.close-btn {
background: none;
border: none;
font-size: 28px;
cursor: pointer;
color: #999;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.close-btn:hover {
background: #f5f5f5;
color: #333;
}
.modal-body {
flex: 1;
padding: 24px;
overflow-y: auto;
overflow-x: hidden;
}
.content-input {
width: 100%;
border: none;
resize: none;
font-size: 16px;
line-height: 1.6;
padding: 16px;
border-radius: 12px;
background: #f8f9fa;
margin-bottom: 20px;
height: 120px;
}
.content-input:focus {
outline: none;
background: #fff;
box-shadow: 0 0 0 2px #e1f0ff;
}
.image-upload {
margin-bottom: 24px;
}
.upload-preview {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 16px;
}
.preview-item {
position: relative;
aspect-ratio: 1;
border-radius: 8px;
overflow: hidden;
}
.preview-item img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s;
}
.preview-item:hover img {
transform: scale(1.05);
}
.remove-img {
position: absolute;
top: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.6);
color: white;
border: none;
width: 26px;
height: 26px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.remove-img:hover {
background: rgba(0, 0, 0, 0.8);
transform: scale(1.1);
}
.upload-btn {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 15px 25px;
background: #f0f7ff;
color: #4a90e2;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s;
font-weight: 500;
}
.upload-btn:hover {
background: #e1f0ff;
transform: translateY(-1px);
}
.upload-btn input {
display: none;
}
.tags-input {
margin: 20px 0;
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
padding: 12px 16px;
background: #f8f9fa;
border-radius: 12px;
min-height: 48px;
align-items: center;
}
.tag {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: #e1f0ff;
color: #4a90e2;
border-radius: 16px;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
}
.tag:hover {
background: #d0e7ff;
transform: translateY(-1px);
}
.remove-tag {
background: none;
border: none;
color: #4a90e2;
font-size: 16px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
width: 16px;
height: 16px;
border-radius: 50%;
transition: all 0.2s ease;
}
.remove-tag:hover {
background: rgba(74, 144, 226, 0.1);
}
.tag-input {
flex: 1;
min-width: 120px;
border: none;
background: transparent;
font-size: 14px;
color: #333;
padding: 6px 0;
outline: none;
}
.tag-input::placeholder {
color: #999;
}
.modal-footer {
padding: 16px 24px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
background: #fff;
}
.cancel-btn, .publish-btn {
padding: 12px 30px;
border-radius: 25px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
font-weight: 500;
}
.cancel-btn {
background: #f5f5f5;
border: none;
color: #666;
}
.cancel-btn:hover {
background: #eee;
transform: translateY(-1px);
}
.publish-btn {
background: linear-gradient(45deg, #4a90e2, #63b3ed);
border: none;
color: white;
box-shadow: 0 4px 15px rgba(74, 144, 226, 0.3);
}
.publish-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(74, 144, 226, 0.4);
}
.publish-btn:disabled {
background: linear-gradient(45deg, #ccc, #ddd);
cursor: not-allowed;
box-shadow: none;
}
.modal-body::-webkit-scrollbar {
width: 6px;
}
.modal-body::-webkit-scrollbar-track {
background: transparent;
}
.modal-body::-webkit-scrollbar-thumb {
background: #ddd;
border-radius: 3px;
}
.modal-body::-webkit-scrollbar-thumb:hover {
background: #ccc;
}
</style>

@ -0,0 +1,739 @@
<template>
<div class="youco-home">
<div class="carousel">
<div class="content" v-for="(card, index) in cards" :key="index" :class="{ active: currentCard === index }">
<h1 class="title">即将开始的活动</h1>
<div class="event-info">
<h2>{{ card.event }}</h2>
<p>{{ card.date }}</p>
</div>
</div>
<div class="carousel-buttons">
<button v-for="(card, index) in cards" :key="index" @click="currentCard = index" :class="{ active: currentCard === index }"></button>
</div>
</div>
<!-- 修改旅行灵感部分 -->
<div class="travel-inspiration" ref="travelInspiration">
<h2>旅行灵感</h2>
<div class="inspiration-carousel">
<div class="inspiration-cards" :style="{ transform: `translateX(-${currentInspirationIndex * 100}%)` }">
<div class="inspiration-pair" v-for="(pair, index) in inspirationPairs" :key="index">
<div class="inspiration-card" v-for="inspiration in pair" :key="inspiration.title">
<img :src="inspiration.image" :alt="inspiration.title">
<div class="card-content">
<h3>{{ inspiration.title }}</h3>
<p>{{ inspiration.description }}</p>
<a href="#" class="read-more" @click.prevent="showDetails(inspiration)">探索更多</a>
</div>
</div>
</div>
</div>
<!-- 添加轮播控制按钮 -->
<div class="carousel-controls">
<button class="prev" @click="prevInspiration" :disabled="currentInspirationIndex === 0">
<span class="mdi mdi-chevron-left"></span>
</button>
<div class="carousel-dots">
<button v-for="(_, index) in inspirationPairs"
:key="index"
:class="{ active: currentInspirationIndex === index }"
@click="currentInspirationIndex = index">
</button>
</div>
<button class="next" @click="nextInspiration" :disabled="currentInspirationIndex === inspirationPairs.length - 1">
<span class="mdi mdi-chevron-right"></span>
</button>
</div>
</div>
</div>
<!-- 修改旅行小贴士部分的结构 -->
<div class="travel-tips-section">
<h2>旅行小贴士</h2>
<div class="travel-tips">
<ul>
<li>记得购买旅行保险以应对突发情况</li>
<li>提前了解目的地的文化习俗尊重当地传统</li>
<li>准备便携式充电器保持设备电量充足</li>
<li>使用我们的App随时与新朋友保持联系</li>
<li>尝试当地特色美食但注意饮食卫生</li>
<li>保持开放心态拥抱新的文化和体验</li>
</ul>
</div>
</div>
<!-- 新增模态框组件 -->
<div class="modal" v-if="selectedInspiration" @click="closeModal">
<div class="modal-content" @click.stop>
<button class="close-button" @click="closeModal">&times;</button>
<img :src="selectedInspiration.image" :alt="selectedInspiration.title">
<h2>{{ selectedInspiration.title }}</h2>
<p>{{ selectedInspiration.fullDescription }}</p>
<div class="additional-info" v-if="selectedInspiration.additionalInfo">
<h3>推荐行程</h3>
<ul>
<li v-for="(info, index) in selectedInspiration.additionalInfo" :key="index">
{{ info }}
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'YouCoHome',
data() {
return {
currentCard: 0,
selectedInspiration: null,
cards: [
{
event: '徒步长城',
date: 'Oct 31, 2024',
image: '@/assets/great_wall.png'
},
{
event: '越秀繁华',
date: 'Dec 15, 2024',
image: '@/assets/guangzhou.jpg'
},
{
event: '蓉城艺境',
date: 'Jan 20, 2025',
image: '@/assets/chengdu.jpg'
},
{
event: '大唐风韵',
date: 'Mar 5, 2025',
image: '@/assets/xian.jpg'
}
],
currentInspirationIndex: 0,
inspirations: [
{
title: '原始雨林探索',
description: '探索亚马逊热带雨林,感受地球之肺的神秘魅力。漫步在参天古木下,聆听大自然的原始呼唤。',
fullDescription: '亚马逊雨林是世界上最大的热带雨林,拥有数百万种动植物物种。在这里,您可以体验独特的生态系统,观察珍稀野生动物,了解原住民文化。我们精心设计的探索之旅将带您深入雨林腹地,体验夜间丛林探险,乘坐独木舟穿越河道,参观本地社区,学习雨林生存技能。',
additionalInfo: [
'推荐游览时间4-7天',
'最佳旅行季节6-9月旱季',
'必备装备:防水装备、防蚊虫用品',
'特色体验:丛林徒步、野生动物观察、原住民文化体验',
'住宿选择:生态度假村、丛林营地'
],
image: require('../assets/forest.jpg')
},
{
title: '阿拉斯加冰川',
description: '探访北美最壮观的冰川群,体验极地风光。乘坐破冰船近距离观赏千年冰川,感受大自然的鬼斧神工。',
fullDescription: '阿拉斯加的冰川是大自然最壮观的杰作之一。这里有北美最大的冰川群,绵延数百公里。您将有机会乘坐专业破冰船,近距离观察巍峨的冰川绝壁,聆听冰川破裂的轰鸣声,还可能看到冰川崩塌的震撼场面。冬季还有机会观赏到绚丽的北极光。',
additionalInfo: [
'推荐游览时间5-10天',
'最佳旅行季节5-9月',
'必备装备:保暖衣物、防水外套、专业相机',
'特色体验:冰川徒步、破冰船巡游、极光观赏',
'住宿选择:度假酒店、特色木屋'
],
image: require('../assets/glacer.jpg')
},
{
title: '托斯卡纳乡村',
description: '漫步意大利托斯卡纳乡间,感受欧洲最美田园风光。葡萄园、橄榄树和古老农庄,演绎着完美田园生活。',
fullDescription: '托斯卡纳是意大利最迷人的地区之一,这里有连绵起伏的丘陵、古老的葡萄园和橄榄树林。您可以参观中世纪古堡,品尝当地著名的基安蒂葡萄酒,学习意大利传统烹饪,感受真正的托斯卡纳乡村生活。每个小镇都有自己独特的魅力和历史故事。',
additionalInfo: [
'推荐游览时间7-14天',
'最佳旅行季节4-10月',
'特色体验:葡萄酒品鉴、烹饪课程、古堡参观',
'美食推荐:手工意面、托斯卡纳牛排、当地橄榄油',
'住宿选择:乡村农庄、精品酒店、古堡改建酒店'
],
image: require('../assets/bruno.jpg')
},
{
title: '迈阿密海滨',
description: '体验美国最热情的海滨城市,感受迈阿密独特的拉丁文化。阳光、沙滩、艺术区,演绎着缤纷都市生活。',
fullDescription: '迈阿密是美国最具活力的城市之一,这里完美融合了现代都市气息和拉丁美洲风情。南海滩区域的艺术装饰建筑群是世界最大的艺术装饰建筑群。温暖的气候、绵长的海滩、丰富的夜生活,以及充满活力的文化艺术区,让这里成为度假的理想之选。',
additionalInfo: [
'推荐游览时间4-7天',
'最佳旅行季节11-4月',
'必游景点:南海滩、小哈瓦那、温伍德墙艺术区',
'特色体验:海滩派对、古巴美食、艺术画廊巡礼',
'住宿选择:海滨度假酒店、精品设计酒店'
],
image: require('../assets/miami.jpg')
},
{
title: '益阳古城',
description: '探访湖南千年古城,感受浓郁的湘楚文化。古街、老巷、茶馆,诉说着悠久的历史故事。',
fullDescription: '益阳古城拥有两千多年的历史文化积淀,是湖南省重要的历史文化名城。这里保存着大量明清时期的古建筑,街巷布局保持着古城原貌。您可以漫步在青石板铺就的古街上,品尝地道的益阳美食,感受独特的茶文化,探寻古城的历史印记。',
additionalInfo: [
'推荐游览时间2-3天',
'最佳旅行季节3-5月、9-11月',
'必游景点:益阳古城墙、清代古街区、梓山湖',
'特色体验:茶文化体验、湘菜品鉴、古建筑探访',
'住宿选择:精品民宿、特色客栈'
],
image: require('../assets/yiyang.jpg')
},
{
title: '马尔代夫度假',
description: '入住印度洋上的奢华水上别墅,体验世界顶级的海岛度假。水清沙白、椰林摇曳,尽享热带天堂。',
fullDescription: '马尔代夫是世界上最著名的海岛度假胜地之一,这里有着纯净的碧海蓝天、洁白的沙滩和丰富的海洋生态。每个岛屿都是一个独立的度假村,提供私密而奢华的度假体验。您可以住在水上别墅里,随时跳入清澈的海水中,探索绚丽的珊瑚礁,与热带鱼群共舞。',
additionalInfo: [
'推荐游览时间5-7天',
'最佳旅行季节12-4月',
'特色体验浮潜、深潜、SPA、日落巡游',
'必备物品:防晒用品、水下相机、泳装',
'住宿选择:水上别墅、沙滩别墅、豪华度假村'
],
image: require('../assets/staples_center.jpg')
}
]
}
},
mounted() {
//
window.addEventListener('scroll', this.handleScroll);
},
beforeDestroy() {
//
window.removeEventListener('scroll', this.handleScroll);
},
computed: {
inspirationPairs() {
const pairs = [];
for (let i = 0; i < this.inspirations.length; i += 2) {
pairs.push(this.inspirations.slice(i, i + 2));
}
return pairs;
}
},
methods: {
showDetails(inspiration) {
this.selectedInspiration = inspiration;
document.body.style.overflow = 'hidden'; //
},
closeModal() {
this.selectedInspiration = null;
document.body.style.overflow = ''; //
},
prevInspiration() {
if (this.currentInspirationIndex > 0) {
this.currentInspirationIndex--;
}
},
nextInspiration() {
if (this.currentInspirationIndex < this.inspirationPairs.length - 1) {
this.currentInspirationIndex++;
}
},
handleScroll() {
const inspirationElement = this.$refs.travelInspiration;
if (inspirationElement) {
const rect = inspirationElement.getBoundingClientRect();
const isInView = rect.top <= window.innerHeight / 2 && rect.bottom >= window.innerHeight / 2;
//
if (isInView) {
this.$emit('update-nav', 'destination');
}
}
}
}
}
</script>
<style scoped>
.youco-home {
font-family: Arial, sans-serif;
width: 100%;
max-width: 100%;
padding: 0;
margin: 0;
background-color: transparent;
}
.carousel {
position: relative;
width: 100%;
height: 0;
padding-bottom: 40%;
overflow: hidden;
border-radius: 16px;
}
.content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 0.5s ease;
overflow: hidden;
}
.content.active {
opacity: 1;
}
.content::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-size: cover;
background-position: center;
}
.content:nth-child(1)::before {
background-image: url('@/assets/great_wall.png');
}
.content:nth-child(2)::before {
background-image: url('@/assets/guangzhou.jpg');
}
.content:nth-child(3)::before {
background-image: url('@/assets/chengdu.jpg');
}
.content:nth-child(4)::before {
background-image: url('@/assets/xian.jpg');
}
.title, .event-info h2, .event-info p {
color: #ffffff;
font-weight: bold;
margin: 0;
padding: 0;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
.title {
position: absolute;
top: 20px;
left: 20px;
font-size: 2em;
}
.event-info {
position: absolute;
bottom: 20px;
left: 20px;
}
.event-info h2 {
font-size: 2em;
margin-bottom: 5px;
}
.event-info p {
font-size: 1.5em;
}
.carousel-buttons {
position: absolute;
bottom: 20px;
right: 20px;
display: flex;
z-index: 10;
}
.carousel-buttons button {
width: 12px;
height: 12px;
border-radius: 50%;
border: none;
background-color: rgba(255, 255, 255, 0.5);
margin: 0 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.carousel-buttons button.active {
background-color: #ffffff;
}
/* 修改旅行灵感样式 */
.travel-inspiration {
padding: 20px 0;
background-color: transparent;
width: 100%;
}
.travel-inspiration h2 {
font-size: 24px;
color: #333;
margin-bottom: 20px;
padding: 0 0px;
}
.inspiration-carousel {
position: relative;
overflow: hidden;
padding: 0 0px;
}
.inspiration-cards {
display: flex;
transition: transform 0.5s ease;
}
.inspiration-pair {
display: flex;
gap: 16px;
flex: 0 0 100%;
}
.inspiration-card {
flex: 1;
background-color: rgba(255, 255, 255, 0.7);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.inspiration-card img {
width: 100%;
height: 200px;
object-fit: cover;
}
.card-content {
padding: 15px;
}
.card-content h3 {
font-size: 18px;
color: #333;
margin-bottom: 10px;
}
.card-content p {
font-size: 14px;
color: #666;
margin-bottom: 15px;
}
.read-more {
display: inline-block;
color: #007bff;
text-decoration: none;
font-size: 14px;
}
@media (max-width: 768px) {
.inspiration-card {
width: 100%;
}
}
/* 修改旅行小贴士样式 */
.travel-tips-section {
padding: 0; /* 移除顶部内边距 */
width: 100%;
margin-top: -40px; /* 添加负的上外边距来上移整个部分 */
}
.travel-tips-section h2 {
font-size: 24px;
color: #333;
margin-bottom: 16px; /* 稍微减少标题下方的间距 */
padding: 0 0px;
}
.travel-tips {
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 16px; /* 稍微减少内边距 */
margin: 0 0px; /* 添加左右外边距以保持与其他部分的对齐 */
}
.travel-tips ul {
list-style-type: none;
padding: 0;
margin: 0;
}
.travel-tips li {
font-size: 14px;
color: #666;
margin-bottom: 12px;
padding-left: 20px;
position: relative;
}
.travel-tips li::before {
content: '•';
position: absolute;
left: 0;
color: #007bff;
}
@media (max-width: 768px) {
.travel-tips-section {
margin-top: -10px; /* 在移动设备上稍微减少上移的距离 */
}
.travel-tips {
padding: 12px; /* 在移动设备上进一步减少内边距 */
margin: 0 12px; /* 在移动设备上调整左右外边距 */
}
}
/* 添加页脚样式 */
.footer {
background-color: #333;
color: #fff;
padding: 40px 0 20px;
margin-top: 40px;
width: 100%;
box-sizing: border-box;
/* 移除之前的定位样式 */
position: relative;
left: 50%;
transform: translateX(-50%);
width: 100vw;
max-width: 100%;
overflow-x: hidden;
}
.footer-content {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px; /* 保持左右内边距 */
}
.footer-section {
flex: 1;
min-width: 200px;
margin-bottom: 20px;
}
.footer-section h3 {
font-size: 18px;
margin-bottom: 15px;
}
.footer-section ul {
list-style: none;
padding: 0;
}
.footer-section ul li {
margin-bottom: 8px;
}
.footer-section ul li a {
color: #ccc;
text-decoration: none;
}
.social-icons {
display: flex;
gap: 10px;
}
.social-icon {
display: inline-block;
width: 30px;
height: 30px;
background-color: #555;
color: #fff;
text-align: center;
line-height: 30px;
border-radius: 50%;
text-decoration: none;
}
.subscribe-form {
display: flex;
margin-top: 10px;
}
.subscribe-form input {
flex: 1;
padding: 8px;
border: none;
border-radius: 4px 0 0 4px;
}
.subscribe-form button {
padding: 8px 15px;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
}
.footer-bottom {
text-align: center;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #555;
}
@media (max-width: 768px) {
.footer-content {
flex-direction: column;
padding: 0 15px; /* 在移动设备上调整内边距 */
}
.footer-section {
margin-bottom: 30px;
}
}
.carousel-controls {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
gap: 20px;
}
.carousel-controls button {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #4a90e2;
padding: 8px;
border-radius: 50%;
transition: all 0.3s ease;
}
.carousel-controls button:disabled {
color: #ccc;
cursor: not-allowed;
}
.carousel-controls button:not(:disabled):hover {
background-color: rgba(74, 144, 226, 0.1);
}
.carousel-dots {
display: flex;
gap: 8px;
}
.carousel-dots button {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #ccc;
border: none;
padding: 0;
cursor: pointer;
transition: background-color 0.3s ease;
}
.carousel-dots button.active {
background-color: #4a90e2;
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: white;
padding: 30px;
border-radius: 12px;
max-width: 800px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
position: relative;
}
.close-button {
position: absolute;
top: 15px;
right: 15px;
font-size: 24px;
border: none;
background: none;
cursor: pointer;
padding: 5px;
color: #333;
}
.modal-content img {
width: 100%;
height: 300px;
object-fit: cover;
border-radius: 8px;
margin-bottom: 20px;
}
.modal-content h2 {
color: #333;
margin-bottom: 15px;
}
.modal-content p {
color: #666;
line-height: 1.6;
margin-bottom: 20px;
}
.additional-info {
border-top: 1px solid #eee;
padding-top: 20px;
}
.additional-info h3 {
color: #333;
margin-bottom: 15px;
}
.additional-info ul {
list-style-type: none;
padding: 0;
}
.additional-info li {
color: #666;
margin-bottom: 10px;
padding-left: 20px;
position: relative;
}
.additional-info li::before {
content: '•';
position: absolute;
left: 0;
color: #007bff;
}
@media (max-width: 768px) {
.modal-content {
padding: 20px;
width: 95%;
}
.modal-content img {
height: 200px;
}
}
</style>

@ -0,0 +1,166 @@
<template>
<div class="help-page">
<div class="help-container">
<h1>帮助中心</h1>
<div class="help-section">
<h2>常见问题</h2>
<div class="faq-item" v-for="(faq, index) in faqs" :key="index">
<div class="question" @click="faq.isOpen = !faq.isOpen">
<span class="mdi mdi-help-circle"></span>
{{ faq.question }}
<span class="mdi" :class="faq.isOpen ? 'mdi-chevron-up' : 'mdi-chevron-down'"></span>
</div>
<div class="answer" v-show="faq.isOpen">
{{ faq.answer }}
</div>
</div>
</div>
<div class="help-section">
<h2>联系我们</h2>
<div class="contact-info">
<p><span class="mdi mdi-email"></span> 邮箱support@youco.com</p>
<p><span class="mdi mdi-phone"></span> 电话400-123-4567</p>
<p><span class="mdi mdi-clock"></span> 服务时间周一至周日 9:00-18:00</p>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'HelpCenter',
data() {
return {
faqs: [
{
question: '如何修改个人资料?',
answer: '点击右上角头像 → 选择"设置" → 在"个人资料"标签页中修改相关信息。',
isOpen: false
},
{
question: '什么是盲盒交友?',
answer: '盲盒交友是一项随机匹配同行伙伴的功能。您可以设置目的地、日期和预算,系统会为您匹配志同道合的旅伴。',
isOpen: false
},
{
question: '如何使用预算规划?',
answer: '在首页选择"预算规划",输入目的地和总预算,系统会为您生成合理的旅行花费方案。',
isOpen: false
},
{
question: '如何查看我的旅行足迹?',
answer: '点击右上角头像 → 选择"个人主页" → 在页面中可以查看您的所有旅行记录。',
isOpen: false
}
]
}
}
}
</script>
<style scoped>
.help-page {
padding: 40px 20px;
background-color: #f5f7fa;
min-height: calc(100vh - 60px);
margin-top: 60px;
}
.help-container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
}
h1 {
margin: 0 0 30px;
font-size: 24px;
color: #333;
text-align: center;
}
.help-section {
margin-bottom: 40px;
}
h2 {
font-size: 20px;
color: #4a90e2;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #f0f0f0;
}
.faq-item {
margin-bottom: 15px;
}
.question {
display: flex;
align-items: center;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s;
}
.question:hover {
background: #f0f0f0;
}
.question .mdi {
margin-right: 10px;
color: #4a90e2;
}
.question .mdi-chevron-down,
.question .mdi-chevron-up {
margin-left: auto;
margin-right: 0;
}
.answer {
padding: 15px;
color: #666;
line-height: 1.6;
border-left: 3px solid #4a90e2;
margin: 10px 0 10px 20px;
background: #f8f9fa;
}
.contact-info {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
}
.contact-info p {
margin: 10px 0;
color: #666;
display: flex;
align-items: center;
}
.contact-info .mdi {
margin-right: 10px;
color: #4a90e2;
}
@media (max-width: 768px) {
.help-page {
padding: 20px 10px;
}
.help-container {
padding: 20px;
}
}
</style>

@ -0,0 +1,73 @@
<template>
<div class="login-page">
<div class="login-container">
<h2>用户登录</h2>
<div class="form-group">
<label>用户名</label>
<input type="text" v-model="loginForm.username" class="form-input">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" v-model="loginForm.password" class="form-input">
</div>
<div class="form-actions">
<button @click="handleLogin" class="login-btn">登录</button>
<button @click="handleCancel" class="cancel-btn">取消</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'LoginPage',
data() {
return {
loginForm: {
username: '',
password: ''
}
}
},
methods: {
handleLogin() {
//
if (this.loginForm.username && this.loginForm.password) {
//
localStorage.setItem('isLoggedIn', 'true');
//
this.$router.push('/discover');
} else {
alert('请输入用户名和密码');
}
},
handleCancel() {
//
this.$router.push('/discover');
}
}
}
</script>
<style scoped>
.login-page {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(to right, #ffd1dc, #ffecd2);
}
.login-container {
background: white;
border-radius: 12px;
padding: 24px;
width: 400px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
/* 复用 App.vue 中的表单样式 */
.form-group, .form-input, .form-actions, .login-btn, .cancel-btn {
/* 这些样式已在 App.vue 中定义 */
}
</style>

@ -0,0 +1,191 @@
<template>
<div class="matching-animation">
<div class="animation-content">
<div class="radar-circle"></div>
<div class="scanning-line"></div>
<div class="matching-text">
<span>正在寻找最佳旅伴</span>
<div class="dots">
<span>.</span>
<span>.</span>
<span>.</span>
</div>
</div>
<div class="matching-stats">
<div class="stat-item">
<span class="label">已扫描</span>
<span class="value">{{ scannedCount }}</span>
</div>
<div class="stat-item">
<span class="label">匹配度</span>
<span class="value">{{ matchRate }}%</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'MatchingAnimation',
data() {
return {
scannedCount: 0,
matchRate: 0,
animationInterval: null
}
},
mounted() {
this.startAnimation();
},
methods: {
startAnimation() {
let count = 0;
this.animationInterval = setInterval(() => {
count++;
this.scannedCount = Math.min(count * 20, 100);
this.matchRate = Math.min(count * 25, 98);
if (count >= 4) {
clearInterval(this.animationInterval);
setTimeout(() => {
this.$emit('matchComplete');
}, 500);
}
}, 300);
}
},
beforeDestroy() {
if (this.animationInterval) {
clearInterval(this.animationInterval);
}
}
}
</script>
<style scoped>
.matching-animation {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.animation-content {
position: relative;
width: 300px;
height: 300px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.radar-circle {
width: 200px;
height: 200px;
border: 2px solid #4CAF50;
border-radius: 50%;
position: relative;
animation: pulse 2s infinite;
}
.scanning-line {
position: absolute;
width: 2px;
height: 100px;
background: linear-gradient(to bottom, #4CAF50, transparent);
top: 50px;
left: 50%;
transform-origin: bottom;
animation: scan 2s linear infinite;
}
.matching-text {
margin-top: 30px;
color: white;
font-size: 1.2em;
display: flex;
align-items: center;
}
.dots span {
opacity: 0;
animation: dots 1.4s infinite;
margin-left: 2px;
}
.dots span:nth-child(2) {
animation-delay: 0.2s;
}
.dots span:nth-child(3) {
animation-delay: 0.4s;
}
.matching-stats {
margin-top: 20px;
display: flex;
gap: 20px;
}
.stat-item {
color: white;
text-align: center;
}
.stat-item .label {
display: block;
font-size: 0.9em;
color: #888;
}
.stat-item .value {
display: block;
font-size: 1.4em;
font-weight: bold;
color: #4CAF50;
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes scan {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes dots {
0%, 20% {
opacity: 0;
}
40% {
opacity: 1;
}
100% {
opacity: 0;
}
}
</style>

@ -0,0 +1,306 @@
<template>
<div class="personal-home">
<!-- 个人信息头部 -->
<div class="profile-header">
<div class="profile-cover"></div>
<div class="profile-info">
<div class="avatar">
<img :src="userData.userInfo.avatar" :alt="userData.userInfo.username">
</div>
<div class="user-details">
<h1>{{ userData.userInfo.username }}</h1>
<p class="location">
<span class="mdi mdi-map-marker"></span>
{{ userData.userInfo.location }}
</p>
<p class="join-date">加入于 {{ userData.userInfo.joinDate }}</p>
</div>
<div class="level-badge">
<span class="mdi mdi-star"></span>
Level {{ userData.userInfo.level }}
</div>
</div>
</div>
<!-- 统计数据 -->
<div class="stats-container">
<div class="stat-item" v-for="(value, key) in userData.stats" :key="key">
<div class="stat-value">{{ value }}</div>
<div class="stat-label">{{ getStatLabel(key) }}</div>
</div>
</div>
<!-- 成就展示 -->
<div class="achievements-section">
<h2>我的成就</h2>
<div class="achievements-grid">
<div
v-for="achievement in userData.achievements"
:key="achievement.id"
class="achievement-card"
>
<span class="mdi" :class="achievement.icon"></span>
<h3>{{ achievement.title }}</h3>
<p>{{ achievement.description }}</p>
</div>
</div>
</div>
<!-- 旅行记录 -->
<div class="travel-history">
<h2>旅行足迹</h2>
<div class="travel-grid">
<div
v-for="trip in userData.travelHistory"
:key="trip.id"
class="trip-card"
>
<h3>{{ trip.destination }}</h3>
<p class="trip-date">{{ formatDate(trip.date) }}</p>
<div class="trip-stats">
<span><span class="mdi mdi-camera"></span> {{ trip.photos }}</span>
<span><span class="mdi mdi-star"></span> {{ trip.rating }}</span>
</div>
</div>
</div>
</div>
<!-- 收藏的目的地 -->
<div class="saved-destinations">
<h2>收藏的目的地</h2>
<div class="destinations-grid">
<div
v-for="destination in userData.savedDestinations"
:key="destination.id"
class="destination-card"
:style="{ backgroundImage: `url(${destination.imageUrl})` }"
>
<div class="destination-info">
<h3>{{ destination.name }}</h3>
<p>{{ destination.type }}</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { personalHomeData } from '../data/PersonalHome'
export default {
name: 'PersonalHome',
data() {
return {
userData: personalHomeData
}
},
methods: {
getStatLabel(key) {
const labels = {
followers: '关注者',
following: '关注中',
posts: '发布',
likes: '获赞'
}
return labels[key]
},
formatDate(dateString) {
return new Date(dateString).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
}
}
</script>
<style scoped>
.personal-home {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
margin-top: 60px;
}
.profile-header {
position: relative;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
}
.profile-cover {
height: 200px;
background: linear-gradient(135deg, #4a90e2, #87ceeb);
}
.profile-info {
display: flex;
align-items: flex-end;
padding: 20px;
margin-top: -60px;
}
.avatar {
width: 120px;
height: 120px;
border-radius: 50%;
border: 4px solid white;
overflow: hidden;
margin-right: 20px;
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.user-details {
flex: 1;
}
.user-details h1 {
margin: 0;
color: #333;
font-size: 24px;
}
.location {
color: #666;
margin: 5px 0;
}
.join-date {
color: #999;
font-size: 14px;
}
.level-badge {
background: #4a90e2;
color: white;
padding: 8px 16px;
border-radius: 20px;
font-weight: bold;
}
.stats-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin: 20px 0;
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #4a90e2;
}
.stat-label {
color: #666;
margin-top: 5px;
}
.achievements-section,
.travel-history,
.saved-destinations {
background: white;
padding: 20px;
border-radius: 12px;
margin: 20px 0;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
}
.achievements-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
margin-top: 20px;
}
.achievement-card {
padding: 20px;
border-radius: 8px;
background: #f8f9fa;
text-align: center;
}
.achievement-card .mdi {
font-size: 32px;
color: #4a90e2;
}
.travel-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}
.trip-card {
padding: 15px;
border-radius: 8px;
background: #f8f9fa;
}
.trip-stats {
display: flex;
justify-content: space-between;
margin-top: 10px;
color: #666;
}
.destinations-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.destination-card {
height: 200px;
border-radius: 8px;
background-size: cover;
background-position: center;
position: relative;
overflow: hidden;
}
.destination-info {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 20px;
background: linear-gradient(transparent, rgba(0,0,0,0.7));
color: white;
}
.destination-info h3 {
margin: 0;
}
@media (max-width: 768px) {
.profile-info {
flex-direction: column;
align-items: center;
text-align: center;
}
.stats-container {
grid-template-columns: repeat(2, 1fr);
}
}
</style>

@ -0,0 +1,355 @@
<template>
<div class="settings-page">
<div class="settings-container">
<!-- 设置页面标题 -->
<h1 class="settings-title">账号设置</h1>
<!-- 设置选项卡 -->
<div class="settings-tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab-button', { active: activeTab === tab.id }]"
@click="activeTab = tab.id"
>
<span class="mdi" :class="tab.icon"></span>
{{ tab.name }}
</button>
</div>
<!-- 基本信息设置 -->
<div v-if="activeTab === 'profile'" class="settings-section">
<h2>个人资料</h2>
<div class="form-group">
<!-- <label>头像</label> -->
<!-- <div class="avatar-upload">
<img :src="formData.avatar" alt="用户头像" class="preview-avatar">
<input
type="file"
accept="image/*"
@change="handleAvatarChange"
ref="avatarInput"
>
<button @click="$refs.avatarInput.click()" class="upload-btn">
更换头像
</button>
</div> -->
</div>
<div class="form-group">
<label>用户名</label>
<input type="text" v-model="formData.username">
</div>
<div class="form-group">
<label>所在地</label>
<input type="text" v-model="formData.location">
</div>
<div class="form-group">
<label>个人简介</label>
<textarea v-model="formData.bio" rows="4"></textarea>
</div>
</div>
<!-- 账号安全设置 -->
<div v-if="activeTab === 'security'" class="settings-section">
<h2>账号安全</h2>
<div class="form-group">
<label>当前密码</label>
<input type="password" v-model="formData.currentPassword">
</div>
<div class="form-group">
<label>新密码</label>
<input type="password" v-model="formData.newPassword">
</div>
<div class="form-group">
<label>确认新密码</label>
<input type="password" v-model="formData.confirmPassword">
</div>
</div>
<!-- 隐私设置 -->
<div v-if="activeTab === 'privacy'" class="settings-section">
<h2>隐私设置</h2>
<div class="switch-group">
<label>
<span>个人主页可见性</span>
<div class="switch">
<input type="checkbox" v-model="formData.profileVisible">
<span class="slider"></span>
</div>
</label>
</div>
<div class="switch-group">
<label>
<span>允许陌生人私信</span>
<div class="switch">
<input type="checkbox" v-model="formData.allowMessages">
<span class="slider"></span>
</div>
</label>
</div>
</div>
<!-- 通知设置 -->
<div v-if="activeTab === 'notifications'" class="settings-section">
<h2>通知设置</h2>
<div class="switch-group">
<label>
<span>系统通知</span>
<div class="switch">
<input type="checkbox" v-model="formData.systemNotifications">
<span class="slider"></span>
</div>
</label>
</div>
<div class="switch-group">
<label>
<span>私信通知</span>
<div class="switch">
<input type="checkbox" v-model="formData.messageNotifications">
<span class="slider"></span>
</div>
</label>
</div>
</div>
<!-- 保存按钮 -->
<div class="settings-actions">
<button @click="saveSettings" class="save-button">保存更改</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'UserSettings',
data() {
return {
activeTab: 'profile',
tabs: [
{ id: 'profile', name: '个人资料', icon: 'mdi-account' },
{ id: 'security', name: '账号安全', icon: 'mdi-shield-lock' },
{ id: 'privacy', name: '隐私设置', icon: 'mdi-eye' },
{ id: 'notifications', name: '通知设置', icon: 'mdi-bell' }
],
formData: {
avatar: '/assets/images/Person.jpg',
username: 'Jackson',
location: '天津',
bio: '',
currentPassword: '',
newPassword: '',
confirmPassword: '',
profileVisible: true,
allowMessages: true,
systemNotifications: true,
messageNotifications: true
}
}
},
methods: {
handleAvatarChange(event) {
const file = event.target.files[0]
if (file) {
const reader = new FileReader()
reader.onload = (e) => {
this.formData.avatar = e.target.result
}
reader.readAsDataURL(file)
}
},
saveSettings() {
//
console.log('保存设置:', this.formData)
this.$notify({
type: 'success',
message: '设置已保存'
})
}
}
}
</script>
<style scoped>
.settings-page {
padding: 40px 20px;
background-color: #f5f7fa;
min-height: calc(100vh - 60px);
}
.settings-container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
}
.settings-title {
margin: 0 0 30px;
font-size: 24px;
color: #333;
}
.settings-tabs {
display: flex;
gap: 20px;
margin-bottom: 30px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.tab-button {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border: none;
background: none;
color: #666;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
}
.tab-button.active {
color: #4a90e2;
font-weight: 500;
}
.settings-section {
margin-bottom: 30px;
}
.settings-section h2 {
margin: 0 0 20px;
font-size: 18px;
color: #333;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #666;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.form-group textarea {
resize: vertical;
}
.avatar-upload {
display: flex;
align-items: center;
gap: 20px;
}
.preview-avatar {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
}
.upload-btn {
padding: 8px 16px;
background: #4a90e2;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background 0.3s ease;
}
.upload-btn:hover {
background: #357abd;
}
.switch-group {
margin-bottom: 20px;
}
.switch-group label {
display: flex;
justify-content: space-between;
align-items: center;
}
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #4a90e2;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.settings-actions {
margin-top: 30px;
text-align: right;
}
.save-button {
padding: 10px 24px;
background: #4a90e2;
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s ease;
}
.save-button:hover {
background: #357abd;
}
</style>

@ -0,0 +1,189 @@
<template>
<footer class="site-footer">
<div class="footer-content">
<div class="footer-section">
<h3>关于我们</h3>
<p>旅游交友网致力于为旅行者提供独特的社交体验,让每次旅行都充满乐趣</p>
</div>
<div class="footer-section">
<h3>快速链接</h3>
<a href="#">使用条款</a>
<a href="#">隐私政策</a>
<a href="#">常见问题</a>
<a href="#">联系我们</a>
</div>
<div class="footer-section">
<h3>关注我们</h3>
<div class="social-links">
<a href="#" class="social-link">
<span class="mdi mdi-sina-weibo"></span>
</a>
<a href="#" class="social-link">
<span class="mdi mdi-music-note"></span>
</a>
<a href="#" class="social-link">
<span class="mdi mdi-wechat"></span>
</a>
</div>
</div>
<div class="footer-section">
<h3>订阅我们</h3>
<div class="subscribe-form">
<input type="email" placeholder="您的邮箱" />
<button>订阅</button>
</div>
</div>
</div>
<div class="footer-bottom">
<p>© 2024 旅游交友网 保留所有权利</p>
</div>
</footer>
</template>
<script>
export default {
name: 'SiteFooter'
}
</script>
<style scoped>
.site-footer {
background-color: #2d2d2d;
color: #808080;
padding: 40px 0 20px;
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.footer-section {
flex: 1;
min-width: 200px;
margin: 0 15px 20px;
}
.footer-section h3 {
color: #fff;
font-size: 16px;
margin-bottom: 15px;
font-weight: bold;
}
.footer-section p {
line-height: 1.6;
margin: 0;
}
.footer-section a {
color: #808080;
text-decoration: none;
display: block;
margin-bottom: 8px;
transition: color 0.3s;
}
.footer-section a:hover {
color: #fff;
}
.social-links {
display: flex;
justify-content: flex-start;
gap: 40px;
margin-top: 15px;
padding-left: 0;
}
.social-link {
color: #808080;
text-decoration: none;
transition: all 0.3s ease;
display: flex;
align-items: center;
}
.social-link .mdi {
font-size: 32px;
}
.social-link:hover {
color: #fff;
transform: translateY(-2px);
}
.subscribe-form {
display: flex;
gap: 8px;
}
.subscribe-form input {
flex: 1;
padding: 8px;
border: none;
border-radius: 4px;
background: #404040;
color: #fff;
}
.subscribe-form input::placeholder {
color: #808080;
}
.subscribe-form button {
padding: 8px 16px;
border: none;
border-radius: 4px;
background: #4a90e2;
color: #fff;
cursor: pointer;
transition: background-color 0.3s;
}
.subscribe-form button:hover {
background: #357abd;
}
.footer-bottom {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #404040;
}
.footer-bottom p {
margin: 0;
font-size: 12px;
}
@media (max-width: 768px) {
.footer-content {
flex-direction: column;
}
.footer-section {
margin: 0 0 30px;
text-align: center;
}
.social-links {
justify-content: center;
}
.subscribe-form {
flex-direction: column;
}
.subscribe-form button {
width: 100%;
}
}
</style>

@ -0,0 +1,89 @@
export const personalHomeData = {
// 用户基本信息
userInfo: {
id: 1,
username: "Jackson",
avatar: require('@/assets/Person.jpg'),
level: 5,
joinDate: "2023-01-01",
location: "天津"
},
// 用户统计数据
stats: {
followers: 4259741,
following: 256,
posts: 42,
likes: 1547492
},
// 用户成就
achievements: [
{
id: 1,
title: "探索达人",
icon: "mdi-compass",
description: "已探索超过10个城市"
},
{
id: 2,
title: "社交达人",
icon: "mdi-account-group",
description: "已结识超过50位好友"
}
],
// 用户旅行记录
travelHistory: [
{
id: 1,
destination: "北京",
date: "2024-02-15",
photos: 12,
rating: 4.5
},
{
id: 2,
destination: "成都",
date: "2024-01-20",
photos: 100,
rating: 10
},
{
id: 3,
destination: "天津",
date: "2023-01-20",
photos: 14,
rating: 2
},
{
id: 4,
destination: "东京",
date: "2022-01-20",
photos: 21,
rating: 0.1
}
],
// 用户收藏的目的地
savedDestinations: [
{
id: 1,
name: "成都",
type: "幸福之都",
imageUrl: require('@/assets/pixian.png')
},
{
id: 2,
name: "三亚",
type: "海滨度假",
imageUrl: require('@/assets/sanya.png')
},
{
id: 3,
name: "丽江",
type: "古城文化",
imageUrl: require('@/assets/lijiang.png')
}
]
}

@ -0,0 +1,375 @@
export const communityData = {
recommendPosts: [
{
id: 1,
username: '旅行者小王',
userAvatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=150&h=150&fit=crop',
time: '2小时前',
content: '今天在云南大理遇到了超美的日出,分享给大家!大理真的是一个很适合发呆放空的地方,古城的清晨特别安静,适合拍照。这次任性地在洱海边待了整整一周,每天早起看日出,傍晚追着夕阳,感觉整个人都被这座城市治愈了。',
images: [
'https://images.unsplash.com/photo-1587474260584-136574528ed5?w=800&auto=format&fit=crop',
require('@/assets/amo.jpg'),
'https://images.unsplash.com/photo-1528127269322-539801943592?w=800&auto=format&fit=crop'
],
tags: ['大理', '旅行', '日出', '风景'],
likes: 128,
comments: [
{
id: 1,
username: '旅行达人',
userAvatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=150&h=150&fit=crop',
text: '太美了!请问这是在哪里拍的?',
time: '1小时前'
},
{
id: 2,
username: '摄影爱好者',
userAvatar: 'https://images.unsplash.com/photo-1599566150163-29194dcaad36?w=150&h=150&fit=crop',
text: '构图很棒,光线处理得也很好!',
time: '30分钟前'
}
],
showComments: false,
newComment: '',
isLiked: false,
isCollected: false
},
{
id: 2,
username: '背包客张三',
userAvatar: 'https://images.unsplash.com/photo-1599566150163-29194dcaad36?w=150&h=150&fit=crop',
time: '3小时前',
content: '分享一个超实用的旅行小贴士出门在外一定要带充电宝特别是在户外徒步的时候拍照耗电特别快。推荐大家可以准备一个20000毫安的大容量充电宝基本可以满足3-4天的用电需求。',
images: [
'https://images.unsplash.com/photo-1501555088652-021faa106b9b?w=800&auto=format&fit=crop',
'https://images.unsplash.com/photo-1527004013197-933c4bb611b3?w=800&auto=format&fit=crop'
],
tags: ['旅行贴士', '装备推荐', '户外'],
likes: 256,
comments: 45,
showComments: false,
newComment: '',
isLiked: false,
isCollected: false
},
{
id: 3,
username: '美食探险家',
userAvatar: 'https://images.unsplash.com/photo-1527980965255-d3b416303d12?w=150&h=150&fit=crop',
time: '5小时前',
content: '探店成都最地道的火锅店这家隐藏在小巷子里的火锅店据说已经开了30多年了。老板是地道的成都人锅底的配方都是祖传的。麻辣鲜香让人欲罢不能特别推荐他家的毛肚和鸭肠新鲜弹牙配上特制的蘸料绝了',
images: [
'https://images.unsplash.com/photo-1569718212165-3a8278d5f624?w=800&auto=format&fit=crop',
'https://images.unsplash.com/photo-1555126634-323283e090fa?w=800&auto=format&fit=crop',
'https://images.unsplash.com/photo-1563245372-f21724e3856d?w=800&auto=format&fit=crop'
],
tags: ['成都', '美食', '火锅', '探店'],
likes: 512,
comments: 89,
showComments: false,
newComment: '',
isLiked: false,
isCollected: false
},
{
id: 4,
username: '摄影师李四',
userAvatar: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?w=150&h=150&fit=crop',
time: '8小时前',
content: '分享一组西藏的星空照片。在海拔4000米的地方抬头就能看到银。这次专门带了全画幅相机去拍星空效果真的很震撼设备索尼A7M3 + 14mm F1.8后期用LR调色。大家觉得效果怎么样',
images: [
'https://images.unsplash.com/photo-1519681393784-d120267933ba?w=800&auto=format&fit=crop',
'https://images.unsplash.com/photo-1515705576963-95cad62945b6?w=800&auto=format&fit=crop'
],
tags: ['西藏', '摄影', '星空', '风光'],
likes: 1024,
comments: 156,
showComments: false,
newComment: '',
isLiked: false,
isCollected: false
},
{
id: 5,
username: '小鱼儿',
userAvatar: 'https://images.unsplash.com/photo-1580489944761-15a19d654956?w=150&h=150&fit=crop',
time: '12小时前',
content: '三亚的海滩真的太美了!碧海蓝天,椰林树影,感觉整个人都被治愈了。特别推荐来蜈支洲岛浮潜,水质特别好,能看到很多小鱼,还有漂亮的珊瑚。',
images: [
'https://images.unsplash.com/photo-1510414842594-a61c69b5ae57?w=800&auto=format&fit=crop',
'https://images.unsplash.com/photo-1544551763-46a013bb70d5?w=800&auto=format&fit=crop',
'https://images.unsplash.com/photo-1505881502353-a1986add3762?w=800&auto=format&fit=crop'
],
tags: ['三亚', '海滩', '浮潜', '度假'],
likes: 768,
comments: 92,
showComments: false,
newComment: '',
isLiked: false,
isCollected: false
}
],
latestPosts: [
{
id: 1,
username: '摄影师王五',
userAvatar: 'https://images.unsplash.com/photo-1568602471122-7832951cc4c5?w=150&h=150&fit=crop',
time: '10分钟前',
content: '刚刚在莫干山拍到的日出,分享给大家!',
images: [
'https://images.unsplash.com/photo-1470252649378-9c29740c9fa8?w=800&auto=format&fit=crop'
],
tags: ['莫干山', '摄影', '日出'],
likes: 12,
comments: [],
showComments: false,
newComment: '',
isLiked: false,
isCollected: false
},
{
id: 2,
username: '美食家李六',
userAvatar: 'https://images.unsplash.com/photo-1557862921-37829c790f19?w=150&h=150&fit=crop',
time: '30分钟前',
content: '今天探店杭州新开的一家创意菜馆,味道真的很赞!',
images: [
'https://images.unsplash.com/photo-1547573854-74d2a71d0826?w=800&auto=format&fit=crop',
'https://images.unsplash.com/photo-1504674900247-0877df9cc836?w=800&auto=format&fit=crop'
],
tags: ['杭州', '美食', '探店'],
likes: 45,
comments: [],
showComments: false,
newComment: '',
isLiked: false,
isCollected: false
}
],
followingPosts: [
{
id: 1,
username: '背包客阿杰',
userAvatar: 'https://images.unsplash.com/photo-1607346256330-dee7af15f7c5?w=150&h=150&fit=crop',
time: '2小时前',
content: '终于到达西藏了!拉萨的天空真的很蓝,空气很清新。',
images: [
'https://images.unsplash.com/photo-1494548162494-384bba4ab999?w=800&auto=format&fit=crop',
'https://images.unsplash.com/photo-1461696114087-397271a7aedc?w=800&auto=format&fit=crop'
],
tags: ['西藏', '拉萨', '旅行'],
likes: 234,
comments: [],
showComments: false,
newComment: '',
isLiked: true,
isCollected: true
},
{
id: 2,
username: '美食猎人',
userAvatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop',
time: '4小时前',
content: '今天探访了一家开了40年的老字号面馆老板今年已经70多岁了但依然坚持每天凌晨4点起床和面。这碗阳春面简单却蕴含着最朴实的美味。',
images: [
'https://images.unsplash.com/photo-1582878826629-29b7ad1cdc43?w=800&auto=format&fit=crop'
],
tags: ['美食', '老字号', '面食'],
likes: 456,
comments: [],
showComments: false,
newComment: '',
isLiked: false,
isCollected: true
},
{
id: 3,
username: '摄影师老王',
userAvatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop',
time: '6小时前',
content: '分享一组今天在黄山拍摄的照片。云海翻腾,宛如仙境。等了整整三天才等到这样的天气,一切都值得!',
images: [
require('@/assets/huangshan_1.jpg'),
require('@/assets/huangshan_2.jpg'),
require('@/assets/huangshan_3.jpg')
],
tags: ['黄山', '摄影', '风光', '云海'],
likes: 892,
comments: [],
showComments: false,
newComment: '',
isLiked: true,
isCollected: false
},
{
id: 4,
username: '旅行达人小李',
userAvatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop',
time: '8小时前',
content: '越南河内的36个古街每一条街都有自己的特色。今天走访了丝绸街和银器街传统工艺品的制作过程真是令人着迷。',
images: [
require('@/assets/henei_1.jpg'),
require('@/assets/henei_2.jpg')
],
tags: ['越南', '河内', '古街', '工艺品'],
likes: 345,
comments: [],
showComments: false,
newComment: '',
isLiked: false,
isCollected: true
},
{
id: 5,
username: '酒店测评师',
userAvatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=150&h=150&fit=crop',
time: '12小时前',
content: '马尔代夫康莱德酒店深度体验。水上villa的日落view简直绝了早餐种类超多而且可以选择在水下餐厅享用看着热带鱼游来游去太享受了',
images: [
'https://images.unsplash.com/photo-1520250497591-112f2f40a3f4?w=800&auto=format&fit=crop',
require('@/assets/sunset.jpg'),
require('@/assets/seaset.jpg')
],
tags: ['马尔代夫', '酒店', '康莱德', '奢旅'],
likes: 678,
comments: [],
showComments: false,
newComment: '',
isLiked: true,
isCollected: true
}
],
hotRanking: [
{
id: 1,
username: '旅行达人',
userAvatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop',
title: '2024最全的川藏线自驾攻略',
views: 25678,
likes: 3421
},
{
id: 2,
username: '美食猎人',
userAvatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop',
title: '探秘成都隐藏的米其林级小店',
views: 18965,
likes: 2876
},
{
id: 3,
username: '摄影师小王',
userAvatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop',
title: '新手入门风光摄影教程',
views: 15634,
likes: 2543
}
],
hotTopics: [
{ id: 1, name: '春季赏花攻略', count: '2345' },
{ id: 2, name: '周末短途游', count: '1234' },
{ id: 3, name: '美食探店', count: '986' },
{ id: 4, name: '摄影技巧分享', count: '756' },
{ id: 5, name: '酒店测评', count: '687' },
{ id: 6, name: '穷游分享', count: '543' },
{ id: 7, name: '旅行装备推荐', count: '432' },
{ id: 8, name: '自驾游路线', count: '321' }
],
userProfile: {
avatar: 'https://images.unsplash.com/photo-1566873535350-a3f5d4a804b7?w=150&h=150&fit=crop',
nickname: '快乐旅行者',
bio: '在路上,遇见最美的风景',
stats: {
following: 128,
followers: 256,
posts: 64
}
},
hotPosts: [
{
id: 1,
username: '旅行达人',
userAvatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop',
time: '1天前',
content: '川藏线自驾攻略分享历时15天的川藏线自驾终于圆满结束这里整理了一份超详细的攻略包括路线规划、补给点、住宿推荐等希望能帮助到准备去的朋友们',
images: [
'https://images.unsplash.com/photo-1516638489986-0c17c234db55?w=800&auto=format&fit=crop',
'https://images.unsplash.com/photo-1537522306408-8435f315b2e3?w=800&auto=format&fit=crop',
'https://images.unsplash.com/photo-1501555088652-021faa106b9b?w=800&auto=format&fit=crop'
],
tags: ['川藏线', '自驾', '攻略', '西藏'],
likes: 3421,
comments: [
{
id: 1,
username: '路过的小明',
userAvatar: 'https://images.unsplash.com/photo-1599566150163-29194dcaad36?w=150&h=150&fit=crop',
text: '太详细了!请问补给点的具体位置能分享一下吗?',
time: '20小时前'
}
],
showComments: false,
newComment: '',
isLiked: false,
isCollected: false
},
{
id: 2,
username: '美食猎人',
userAvatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop',
time: '2天前',
content: '成都隐藏米其林级美食探店这家开了30年的老店藏在巷子里每天只卖5小时想吃至少要排队2小时但是绝对值得',
images: [
'https://images.unsplash.com/photo-1563245372-f21724e3856d?w=800&auto=format&fit=crop',
'https://images.unsplash.com/photo-1555126634-323283e090fa?w=800&auto=format&fit=crop'
],
tags: ['成都', '美食', '探店'],
likes: 2876,
comments: [
{
id: 1,
username: '吃货小王',
userAvatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=150&h=150&fit=crop',
text: '看起来太诱人了!具体地址能分享一下吗?',
time: '1天前'
}
],
showComments: false,
newComment: '',
isLiked: false,
isCollected: false
},
{
id: 3,
username: '摄影师小王',
userAvatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop',
time: '3天前',
content: '风光摄影入门教程!分享一些实用的构图技巧和后期调色经验,新手也能拍出大片感!',
images: [
'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=800&auto=format&fit=crop',
'https://images.unsplash.com/photo-1640881762786-2c506e9f97ea?w=800&auto=format&fit=crop'
],
tags: ['摄影', '教程', '风光'],
likes: 2543,
comments: [
{
id: 1,
username: '摄影爱好者',
userAvatar: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?w=150&h=150&fit=crop',
text: '太实用了!请问用什么相机拍摄的呢?',
time: '2天前'
}
],
showComments: false,
newComment: '',
isLiked: false,
isCollected: false
}
]
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,202 @@
export const userProfiles = [
{
avatar: 'https://ui-avatars.com/api/?name=张三&background=0D8ABC&color=fff&size=128',
name: '张三',
gender: '男',
age: 28,
location: '北京',
hobbies: ['摄影', '徒步', '美食'],
travelPreferences: ['自然风光', '文化体验', '美食探索'],
languages: ['中文', '英语', '日语'],
traveledPlaces: ['日本', '泰国', '法国', '美国'],
travelStyle: '背包客',
bio: '热爱旅行和摄影的自由职业者,喜欢探索不同文化和美食。',
contacts: [
{ type: 'WeChat', value: 'traveler123', icon: 'mdi mdi-wechat' },
{ type: 'Email', value: 'traveler@example.com', icon: 'mdi mdi-email' },
{ type: 'Phone', value: '+86 123 4567 8900', icon: 'mdi mdi-phone' }
],
recentTravel: ['10/2024 云南', '09/2024 西藏', '08/2024 青海'],
rating: 4.5,
reviews: [
{ name: '李四', comment: '很好的旅伴,有趣又靠谱!' },
{ name: '王五', comment: '张三是个很棒的摄影师,给我拍了很多美照。' },
{ name: '赵六', comment: '性格开朗,很会照顾人,下次还想一起旅行。' }
]
},
{
avatar: 'https://ui-avatars.com/api/?name=李梅&background=FF69B4&color=fff&size=128',
name: '李梅',
gender: '女',
age: 25,
location: '上海',
hobbies: ['瑜伽', '潜水', '绘画'],
travelPreferences: ['海岛度假', '艺术展览', '当地市集'],
languages: ['中文', '英语', '法语'],
traveledPlaces: ['马尔代夫', '巴厘岛', '意大利', '西班牙'],
travelStyle: '精致旅行者',
bio: '自由插画师,喜欢用画笔记录旅行中的美好瞬间。',
contacts: [
{ type: 'WeChat', value: 'meimei_art', icon: 'mdi mdi-wechat' },
{ type: 'Email', value: 'mei@example.com', icon: 'mdi mdi-email' },
{ type: 'Phone', value: '+86 135 7890 1234', icon: 'mdi mdi-phone' }
],
recentTravel: ['08/2024 马尔代夫', '07/2024 巴厘岛', '06/2024 普吉岛'],
rating: 4.8,
reviews: [
{ name: '小王', comment: '很有艺术气息的旅伴,给大家画了很多漂亮的速写。' },
{ name: '小张', comment: '性格温和,很会规划行程。' },
{ name: '小李', comment: '一起潜水很开心,是个很棒的伙伴!' }
]
},
{
avatar: 'https://ui-avatars.com/api/?name=王浩&background=4CAF50&color=fff&size=128',
name: '王浩',
gender: '男',
age: 32,
location: '深圳',
hobbies: ['攀岩', '露营', '咖啡品鉴'],
travelPreferences: ['户外探险', '极限运动', '咖啡文化'],
languages: ['中文', '英语', '西班牙语'],
traveledPlaces: ['尼泊尔', '新西兰', '哥伦比亚', '肯尼亚'],
travelStyle: '冒险家',
bio: '咖啡师兼户外运动爱好者,喜欢挑战自我,探索未知。',
contacts: [
{ type: 'WeChat', value: 'coffee_hao', icon: 'mdi mdi-wechat' },
{ type: 'Email', value: 'hao@example.com', icon: 'mdi mdi-email' },
{ type: 'Phone', value: '+86 139 2468 1357', icon: 'mdi mdi-phone' }
],
recentTravel: ['09/2024 尼泊尔', '08/2024 西藏', '07/2024 新疆'],
rating: 4.7,
reviews: [
{ name: '小明', comment: '专业的户外向导,让人感觉很安全。' },
{ name: '小红', comment: '带的咖啡特别好喝,露营体验很棒!' },
{ name: '小青', comment: '经验丰富,是理想的探险伙伴。' }
]
},
{
avatar: 'https://ui-avatars.com/api/?name=陈静&background=9C27B0&color=fff&size=128',
name: '陈静',
gender: '女',
age: 27,
location: '杭州',
hobbies: ['读书', '品茶', '古建筑探索'],
travelPreferences: ['文化古迹', '茶道体验', '古镇漫步'],
languages: ['中文', '英语', '韩语'],
traveledPlaces: ['韩国', '日本', '越南', '柬埔寨'],
travelStyle: '文化体验者',
bio: '古建筑摄影师,热爱东方文化,擅长茶艺。喜欢慢节奏的深度旅行。',
contacts: [
{ type: 'WeChat', value: 'tea_culture', icon: 'mdi mdi-wechat' },
{ type: 'Email', value: 'jing@example.com', icon: 'mdi mdi-email' },
{ type: 'Phone', value: '+86 150 9876 5432', icon: 'mdi mdi-phone' }
],
recentTravel: ['07/2024 苏州', '06/2024 杭州', '05/2024 扬州'],
rating: 4.6,
reviews: [
{ name: '小陈', comment: '很有见地的文化导览,茶艺分享让人印象深刻。' },
{ name: '小林', comment: '对古建筑的了解很专业,拍照技术一流。' },
{ name: '小周', comment: '性格温婉,很适合文化之旅。' }
]
},
{
avatar: 'https://ui-avatars.com/api/?name=刘阳&background=FF5722&color=fff&size=128',
name: '刘阳',
gender: '男',
age: 30,
location: '成都',
hobbies: ['街舞', '音乐制作', '滑板'],
travelPreferences: ['音乐节', '街头文化', '美食探店'],
languages: ['中文', '英语'],
traveledPlaces: ['美国', '英国', '德国', '澳大利亚'],
travelStyle: '潮流达人',
bio: '音乐制作人,街舞老师。喜欢在旅行中寻找音乐灵感和街头文化。',
contacts: [
{ type: 'WeChat', value: 'yang_music', icon: 'mdi mdi-wechat' },
{ type: 'Email', value: 'yang@example.com', icon: 'mdi mdi-email' },
{ type: 'Phone', value: '+86 138 8888 9999', icon: 'mdi mdi-phone' }
],
recentTravel: ['06/2024 纽约', '05/2024 洛杉矶', '04/2024 拉斯维加斯'],
rating: 4.9,
reviews: [
{ name: '阿杰', comment: '超级有趣的旅伴,教会了我们很多街舞动作!' },
{ name: '小美', comment: '对音乐的热爱感染了整个团队,很棒的体验。' },
{ name: '大卫', comment: '带我们探索了很多地下音乐场景,见识很广。' }
]
},
{
avatar: 'https://ui-avatars.com/api/?name=林雨&background=E91E63&color=fff&size=128',
name: '林雨',
gender: '女',
age: 24,
location: '广州',
hobbies: ['舞蹈', '摄影', '手工艺'],
travelPreferences: ['小众景点', '文艺咖啡', '手作工坊'],
languages: ['中文', '英语', '粤语'],
traveledPlaces: ['台湾', '新加坡', '马来西亚', '韩国'],
travelStyle: '文艺青年',
bio: '舞蹈教师,手作达人。喜欢在旅行中寻找灵感,记录生活中的美好时刻。',
contacts: [
{ type: 'WeChat', value: 'rain_dance', icon: 'mdi mdi-wechat' },
{ type: 'Email', value: 'yurain@example.com', icon: 'mdi mdi-email' },
{ type: 'Phone', value: '+86 133 5555 6666', icon: 'mdi mdi-phone' }
],
recentTravel: ['05/2024 台北', '04/2024 高雄', '03/2024 花莲'],
rating: 4.7,
reviews: [
{ name: '小凡', comment: '很有创意的旅伴,教会了我们很多手工艺!' },
{ name: '阿琳', comment: '带我们探访了很多文艺小店,眼光很独特。' },
{ name: '小杨', comment: '性格活泼,很会照相,给我们留下了美好的回忆。' }
]
},
{
avatar: 'https://ui-avatars.com/api/?name=小月&background=FF9800&color=fff&size=128',
name: '小月',
gender: '女',
age: 23,
location: '重庆',
hobbies: ['动漫', 'cosplay', '甜点制作'],
travelPreferences: ['动漫展', '主题乐园', '美食街'],
languages: ['中文', '日语', '英语'],
traveledPlaces: ['日本', '韩国', '香港', '新加坡'],
travelStyle: '二次元控',
bio: '甜点师兼coser喜欢动漫文化梦想环游世界所有的动漫圣地。',
contacts: [
{ type: 'WeChat', value: 'sweet_moon', icon: 'mdi mdi-wechat' },
{ type: 'Email', value: 'moon@example.com', icon: 'mdi mdi-email' },
{ type: 'Phone', value: '+86 155 6666 7777', icon: 'mdi mdi-phone' }
],
recentTravel: ['09/2024 东京', '08/2024 大阪', '07/2024 京都'],
rating: 4.8,
reviews: [
{ name: '小K', comment: '超可爱的旅伴!一起逛秋叶原很开心!' },
{ name: '阿童木', comment: '做的甜点超好吃,对动漫很有研究。' },
{ name: '二次元', comment: 'cos很棒是个有趣的灵魂。' }
]
},
{
avatar: 'https://ui-avatars.com/api/?name=张科&background=2196F3&color=fff&size=128',
name: '张科',
gender: '男',
age: 29,
location: '深圳',
hobbies: ['编程', '机器人制作', '科幻电影'],
travelPreferences: ['科技展览', '未来城市', '太空博物馆'],
languages: ['中文', '英语', 'Python', 'JavaScript'],
traveledPlaces: ['硅谷', '东京', '新加坡', '迪拜'],
travelStyle: '科技达人',
bio: '人工智能工程师,热爱科技,喜欢探索未来城市。',
contacts: [
{ type: 'WeChat', value: 'tech_zhang', icon: 'mdi mdi-wechat' },
{ type: 'Email', value: 'tech@example.com', icon: 'mdi mdi-email' },
{ type: 'Phone', value: '+86 188 8888 8888', icon: 'mdi mdi-phone' }
],
recentTravel: ['10/2024 硅谷', '09/2024 波士顿', '08/2024 西雅图'],
rating: 4.9,
reviews: [
{ name: '小智', comment: '对科技的热情很有感染力,讲解很专业!' },
{ name: '达芬奇', comment: '带我们参观了很多高科技公司,见识很广。' },
{ name: '未来客', comment: '很棒的科技之旅向导!' }
]
}
];

@ -0,0 +1,19 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import '@mdi/font/css/materialdesignicons.css'
Vue.config.productionTip = false
// 添加路由调试
router.afterEach((to, from) => {
console.log('Route changed:', {
from: from.path,
to: to.path
})
})
new Vue({
router,
render: h => h(App)
}).$mount('#app')

@ -0,0 +1,67 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import PersonalHome from '../components/PersonalHome.vue'
import UserSettings from '../components/Settings.vue'
import HelpCenter from '../components/Help.vue'
import LoginPage from '../components/LoginPage.vue' // 添加这行
Vue.use(VueRouter)
const routes = [
{
path: '/',
redirect: '/discover'
},
{
path: '/personal-home',
name: 'PersonalHome',
component: PersonalHome
},
{
path: '/settings',
name: 'UserSettings',
component: UserSettings
},
{
path: '/help',
name: 'HelpCenter',
component: HelpCenter
},
{
path: '/login',
name: 'Login',
component: LoginPage
},
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
// 处理路由错误
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => {
if (err.name !== 'NavigationDuplicated') {
throw err
}
})
}
/* // 添加导航守卫
router.beforeEach((to, from, next) => {
const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true'
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!isLoggedIn) {
next('/login')
} else {
next()
}
} else {
next()
}
}) */
export default router

@ -0,0 +1,177 @@
import { userProfiles } from '@/data/userProfiles';
/**
* 计算预算匹配度
* @param {number} userBudget - 用户预算
* @param {number} targetBudget - 目标用户预算
* @returns {number} 匹配度分数 (0-1)
*/
function calculateBudgetScore(userBudget, targetBudget) {
const difference = Math.abs(userBudget - targetBudget);
const maxBudget = Math.max(userBudget, targetBudget);
return 1 - (difference / maxBudget);
}
/**
* 计算日期匹配度
* @param {string} userDate - 用户期望日期
* @param {string[]} targetDates - 目标用户可用日期数组
* @returns {number} 匹配度分数 (0-1)
*/
function calculateDateScore(userDate, targetDates) {
const userDateTime = new Date(userDate).getTime();
// 检查是否有完全匹配的日期
if (targetDates.includes(userDate)) {
return 1;
}
// 计算最接近的日期差异
const differences = targetDates.map(date => {
const targetTime = new Date(date).getTime();
return Math.abs(targetTime - userDateTime);
});
const minDifference = Math.min(...differences);
const maxAcceptableDifference = 30 * 24 * 60 * 60 * 1000; // 30天的毫秒数
return Math.max(0, 1 - (minDifference / maxAcceptableDifference));
}
/**
* 计算目的地匹配度
* @param {string} userDestination - 用户目的地
* @param {string[]} targetDestinations - 目标用户意向目的地数组
* @returns {number} 匹配度分数 (0-1)
*/
function calculateDestinationScore(userDestination, targetDestinations) {
return targetDestinations.includes(userDestination) ? 1 : 0;
}
/**
* 主匹配算法
* @param {Object} userPreferences - 用户偏好
* @returns {Array} 匹配结果数组按匹配度排序
*/
export function findMatches(userPreferences) {
const matches = userProfiles.map(targetUser => {
// 计算各维度的匹配分数
const budgetScore = calculateBudgetScore(
userPreferences.budget,
targetUser.travelPreferences.budget
);
const dateScore = calculateDateScore(
userPreferences.date,
targetUser.travelPreferences.availableDates
);
const destinationScore = calculateDestinationScore(
userPreferences.destination,
targetUser.travelPreferences.destinations
);
// 计算总匹配度 (可以调整各维度的权重)
const totalScore = (
budgetScore * 0.3 + // 预算权重 30%
dateScore * 0.3 + // 日期权重 30%
destinationScore * 0.4 // 目的地权重 40%
);
return {
user: targetUser,
matchScore: totalScore,
details: {
budgetScore,
dateScore,
destinationScore
}
};
});
// 按匹配度降序排序并过滤掉匹配度低的结果
return matches
.filter(match => match.matchScore > 0.6) // 只返回匹配度超过60%的结果
.sort((a, b) => b.matchScore - a.matchScore);
}
/**
* 获取匹配建议
* @param {Object} matchResult - 匹配结果
* @returns {Object} 匹配建议
*/
export function getMatchingSuggestions(matchResult) {
const { details } = matchResult;
const suggestions = {
budget: null,
date: null,
destination: null
};
// 根据各维度的匹配分数给出建议
if (details.budgetScore < 0.7) {
suggestions.budget = '建议适当调整预算以增加匹配机会';
}
if (details.dateScore < 0.7) {
suggestions.date = '可以考虑更灵活的出行时间';
}
if (details.destinationScore < 1) {
suggestions.destination = '可以考虑探索更多目的地选择';
}
return suggestions;
}
/**
* 生成匹配报告
* @param {Object} matchResult - 匹配结果
* @returns {Object} 匹配报告
*/
export function generateMatchReport(matchResult) {
const { user, matchScore, details } = matchResult;
return {
matchScore: Math.round(matchScore * 100),
userInfo: {
name: user.name,
age: user.age,
gender: user.gender,
interests: user.interests,
avatar: user.avatar,
budget: user.travelPreferences.budget
},
compatibility: {
budget: Math.round(details.budgetScore * 100),
date: Math.round(details.dateScore * 100),
destination: Math.round(details.destinationScore * 100)
},
suggestions: getMatchingSuggestions(matchResult),
travelPreferences: {
...user.travelPreferences,
budget: user.travelPreferences.budget
}
};
}
/**
* 获取推荐的替代日期
* @param {string} userDate - 用户期望日期
* @param {Object} matchResult - 匹配结果
* @returns {string[]} 推荐日期数组
*/
export function getRecommendedDates(userDate, matchResult) {
const { user } = matchResult;
const userDateTime = new Date(userDate).getTime();
return user.travelPreferences.availableDates
.map(date => ({
date,
difference: Math.abs(new Date(date).getTime() - userDateTime)
}))
.sort((a, b) => a.difference - b.difference)
.slice(0, 3)
.map(item => item.date);
}

@ -0,0 +1,4 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
})
Loading…
Cancel
Save