first commit

This commit is contained in:
aaron 2025-04-16 22:27:39 +08:00
commit 9e8d0f0e11
17 changed files with 794 additions and 0 deletions

2
.env Normal file
View File

@ -0,0 +1,2 @@
VITE_APP_TITLE=美搭
VITE_APP_API_URL=http://127.0.0.1:8000

2
.env.production Normal file
View File

@ -0,0 +1,2 @@
VITE_APP_TITLE=美搭
VITE_APP_API_URL=https://meida-api.beefast.co

38
.gitignore vendored Normal file
View File

@ -0,0 +1,38 @@
# 依赖包
node_modules
npm-debug.log
yarn-debug.log
yarn-error.log
package-lock.json
# 打包输出
/dist
/build
# 本地环境文件
.env.local
.env.*.local
# 日志文件
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# 编辑器目录和文件
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# 操作系统文件
.DS_Store
Thumbs.db
# 测试覆盖率
/coverage

32
Dockerfile Normal file
View File

@ -0,0 +1,32 @@
# 构建阶段
FROM node:18-alpine as build-stage
# 设置工作目录
WORKDIR /app
# 复制package.json和package-lock.json
COPY package*.json ./
# 安装依赖
RUN npm install
# 复制项目文件
COPY . .
# 构建应用
RUN npm run build
# 生产阶段
FROM nginx:stable-alpine as production-stage
# 将构建好的文件复制到nginx目录
COPY --from=build-stage /app/dist /usr/share/nginx/html
# 复制nginx配置文件
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 暴露80端口
EXPOSE 80
# 启动nginx
CMD ["nginx", "-g", "daemon off;"]

67
README.md Normal file
View File

@ -0,0 +1,67 @@
# 美搭项目展示平台
这是一个基于Vue 3开发的美搭时尚穿搭项目展示平台用于展示用户的穿搭历史和效果。
## 功能特点
- 展示用户的穿搭历史记录
- 查看搭配效果和专业评分
- 响应式设计,适配各种设备
## 开发技术
- Vue 3
- Vue Router 4
- Axios
- Vite
## 安装与运行
### 安装依赖
```bash
npm install
```
### 启动开发服务器
```bash
npm run dev
```
### 构建生产版本
```bash
npm run build
```
### 预览生产构建
```bash
npm run serve
```
## 环境配置
项目配置了两个环境的API接口
- 开发环境http://127.0.0.1:8000
- 生产环境https://meida-api.beefast.co/
## 项目结构
```
meida-portal/
├── public/ # 静态资源
├── src/ # 源代码
│ ├── api/ # API接口
│ ├── assets/ # 资源文件
│ ├── components/ # 通用组件
│ ├── router/ # 路由配置
│ ├── views/ # 页面视图
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
├── index.html # HTML模板
├── vite.config.js # Vite配置
└── package.json # 项目配置
```

17
docker-compose.yml Normal file
View File

@ -0,0 +1,17 @@
version: '3'
services:
meida-portal:
build:
context: .
dockerfile: Dockerfile
container_name: meida-portal
ports:
- "80:80"
restart: always
networks:
- meida-network
networks:
meida-network:
driver: bridge

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>美搭 - 时尚穿搭展示平台</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

37
nginx.conf Normal file
View File

@ -0,0 +1,37 @@
server {
listen 80;
server_name localhost;
#access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# 开发环境API代理配置
# location /api/ {
# proxy_pass http://127.0.0.1:8000/;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
# 生产环境API代理配置
# 如需使用生产环境API请取消下面的注释并注释上面的配置
# location /api/ {
# proxy_pass https://meida-api.beefast.co/;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
# redirect server error pages to the static page /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "meida-portal",
"version": "1.0.0",
"description": "美搭项目展示平台",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"keywords": ["vue", "meida", "fashion"],
"author": "",
"license": "ISC",
"dependencies": {
"@vitejs/plugin-vue": "^5.2.3",
"axios": "^1.8.4",
"vite": "^6.3.0",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
}
}

84
src/App.vue Normal file
View File

@ -0,0 +1,84 @@
<template>
<div class="app">
<header class="header">
<div class="container">
<h1 class="logo">美搭</h1>
<nav class="nav">
<router-link to="/" class="nav-link">首页</router-link>
<router-link to="/tryon-history" class="nav-link">穿搭历史</router-link>
</nav>
</div>
</header>
<main class="main">
<div class="container">
<router-view />
</div>
</main>
<footer class="footer">
<div class="container">
<p>© {{ new Date().getFullYear() }} 美搭 - 时尚穿搭展示平台</p>
</div>
</footer>
</div>
</template>
<script setup>
// App
</script>
<style>
.header {
background-color: #1a1a1a;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
padding: 15px 0;
}
.header .container {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 24px;
font-weight: bold;
color: #fe2c55;
}
.nav {
display: flex;
gap: 20px;
}
.nav-link {
text-decoration: none;
color: #f0f0f0;
font-weight: 500;
padding: 5px 10px;
border-radius: 4px;
transition: all 0.3s ease;
}
.nav-link:hover {
background-color: #2a2a2a;
}
.router-link-active {
color: #fe2c55;
background-color: #2a2a2a;
}
.main {
min-height: calc(100vh - 140px);
padding: 30px 0;
}
.footer {
background-color: #1a1a1a;
padding: 20px 0;
text-align: center;
color: #999;
}
</style>

38
src/api/index.js Normal file
View File

@ -0,0 +1,38 @@
import axios from 'axios'
// 创建axios实例
const api = axios.create({
baseURL: import.meta.env.PROD ? 'https://meida-api.beefast.co/' : 'http://127.0.0.1:8000/',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
// 响应拦截器
api.interceptors.response.use(
response => {
// 如果返回的状态码为200说明接口请求成功可以正常拿到数据
if (response.status === 200) {
return response.data
}
// 否则的话抛出错误
return Promise.reject(new Error('网络请求错误'))
},
error => {
let message = error.message || '请求失败'
if (error.response && error.response.data) {
message = error.response.data.message || message
}
console.error(`API错误: ${message}`)
return Promise.reject(error)
}
)
// 定义API方法
export default {
// 获取所有试穿历史
getAllTryonHistories() {
return api.get('/api/v1/tryon/histories/all')
}
}

69
src/assets/main.css Normal file
View File

@ -0,0 +1,69 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 16px;
color: #f0f0f0;
line-height: 1.5;
background-color: #181818;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.card {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
overflow: hidden;
background-color: #252525;
transition: transform 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
}
.btn-primary {
background-color: #fe2c55;
color: white;
}
.btn-primary:hover {
background-color: #e6254d;
}
.text-center {
text-align: center;
}
.mt-10 {
margin-top: 10px;
}
.mt-20 {
margin-top: 20px;
}
.mb-10 {
margin-bottom: 10px;
}
.mb-20 {
margin-bottom: 20px;
}

9
src/main.js Normal file
View File

@ -0,0 +1,9 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './assets/main.css'
const app = createApp(App)
app.use(router)
app.mount('#app')

22
src/router/index.js Normal file
View File

@ -0,0 +1,22 @@
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
const routes = [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/tryon-history',
name: 'tryon-history',
component: () => import('../views/TryonHistory.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router

82
src/views/Home.vue Normal file
View File

@ -0,0 +1,82 @@
<template>
<div class="home">
<div class="hero">
<h1>欢迎使用美搭</h1>
<p>您的个人时尚穿搭助手</p>
<router-link to="/tryon-history" class="btn btn-primary">查看穿搭历史</router-link>
</div>
<div class="features mt-20">
<h2 class="text-center mb-20">我们的特色服务</h2>
<div class="feature-grid">
<div class="feature-card card">
<div class="feature-icon">🔍</div>
<h3>智能搭配</h3>
<p>AI智能分析您的身材特点为您推荐最合适的穿搭方案</p>
</div>
<div class="feature-card card">
<div class="feature-icon">👚</div>
<h3>虚拟试穿</h3>
<p>无需实际试穿在线体验不同服装的效果</p>
</div>
<div class="feature-card card">
<div class="feature-icon">💯</div>
<h3>评分点评</h3>
<p>专业评分系统为您的穿搭提供专业意见</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
//
</script>
<style scoped>
.hero {
text-align: center;
padding: 60px 0;
background-color: #252525;
border-radius: 8px;
margin-bottom: 40px;
}
.hero h1 {
font-size: 36px;
margin-bottom: 10px;
color: #f0f0f0;
}
.hero p {
font-size: 18px;
color: #bbb;
margin-bottom: 20px;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
}
.feature-card {
padding: 30px;
text-align: center;
}
.feature-icon {
font-size: 48px;
margin-bottom: 20px;
}
.feature-card h3 {
font-size: 20px;
margin-bottom: 10px;
color: #f0f0f0;
}
.feature-card p {
color: #bbb;
}
</style>

236
src/views/TryonHistory.vue Normal file
View File

@ -0,0 +1,236 @@
<template>
<div class="tryon-history">
<div v-if="loading" class="loading">
<p>加载中...</p>
</div>
<div v-else-if="error" class="error">
<p>{{ error }}</p>
<button @click="fetchHistories" class="btn btn-primary">重试</button>
</div>
<div v-else-if="histories.length === 0" class="empty">
<p>暂无穿搭历史记录</p>
</div>
<div v-else class="waterfall-container">
<div v-for="history in histories" :key="history.id" class="waterfall-item">
<div class="history-card card">
<div class="result-image">
<img :src="history.completion_url" alt="穿搭效果" />
</div>
<div class="history-details">
<div class="score">
<span class="score-value" :class="getScoreClass(history.score)">{{ history.score }}</span>
</div>
<div class="comment">
<p>{{ history.comment }}</p>
</div>
<div class="source-images">
<h4>搭配来源</h4>
<div class="image-row">
<div class="person-image">
<img :src="history.person_image_url" alt="用户照片" />
</div>
<div v-if="history.top_clothing_url" class="top-image">
<img :src="history.top_clothing_url" alt="上衣" />
</div>
<div v-if="history.bottom_clothing_url" class="bottom-image">
<img :src="history.bottom_clothing_url" alt="下装" />
</div>
</div>
</div>
<div class="date">
<small>{{ formatDate(history.create_time) }}</small>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import api from '../api'
const histories = ref([])
const loading = ref(false)
const error = ref(null)
// 穿
const fetchHistories = async () => {
loading.value = true
error.value = null
try {
const response = await api.getAllTryonHistories()
if (response.code === 200) {
histories.value = response.data
} else {
error.value = response.message || '获取试穿历史失败'
}
} catch (err) {
error.value = '网络错误,请稍后重试'
console.error('获取试穿历史出错:', err)
} finally {
loading.value = false
}
}
//
const formatDate = (dateString) => {
const date = new Date(dateString)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
// CSS
const getScoreClass = (score) => {
if (score >= 90) return 'score-excellent'
if (score >= 80) return 'score-good'
if (score >= 60) return 'score-average'
return 'score-poor'
}
onMounted(() => {
fetchHistories()
})
</script>
<style scoped>
.page-title {
margin-bottom: 30px;
color: #f0f0f0;
text-align: center;
}
.loading, .error, .empty {
text-align: center;
padding: 40px 0;
color: #bbb;
}
/* 小红书风格瀑布流布局 */
.waterfall-container {
column-count: 1;
column-gap: 15px;
width: 100%;
}
.waterfall-item {
break-inside: avoid;
margin-bottom: 15px;
}
.history-card {
overflow: hidden;
border-radius: 8px;
background-color: #252525;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
.result-image img {
width: 100%;
display: block;
object-fit: cover;
border-radius: 8px 8px 0 0;
}
.history-details {
padding: 15px;
}
.score {
margin-bottom: 12px;
}
.score-value {
display: inline-block;
font-size: 16px;
font-weight: bold;
padding: 2px 8px;
border-radius: 4px;
}
.score-excellent {
color: #fff;
background-color: #fe2c55;
}
.score-good {
color: #fff;
background-color: #fe6d42;
}
.score-average {
color: #fff;
background-color: #fd9426;
}
.score-poor {
color: #fff;
background-color: #b91c1c;
}
.comment {
margin-bottom: 15px;
line-height: 1.5;
color: #f0f0f0;
font-size: 14px;
}
.source-images h4 {
margin-bottom: 10px;
font-size: 14px;
color: #bbb;
}
.image-row {
display: flex;
gap: 8px;
margin-bottom: 15px;
}
.person-image img, .top-image img, .bottom-image img {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 4px;
border: 1px solid #333;
}
.date {
font-size: 12px;
color: #888;
text-align: right;
}
/* 响应式布局 */
@media (min-width: 576px) {
.waterfall-container {
column-count: 2;
}
}
@media (min-width: 768px) {
.waterfall-container {
column-count: 3;
}
}
@media (min-width: 1200px) {
.waterfall-container {
column-count: 4;
}
}
</style>

25
vite.config.js Normal file
View File

@ -0,0 +1,25 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, './src'),
},
},
server: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
rewrite: (path) => path
}
}
}
});