refactor(backend): 重构动物相关 API 接口

- 更新了动物数据结构和相关类型定义
- 优化了动物列表、详情、创建、更新和删除接口
- 新增了更新动物状态接口
- 移除了与认领记录相关的接口
-调整了 API 响应结构
This commit is contained in:
ylweng
2025-08-31 23:26:25 +08:00
parent 5b5d65e072
commit cbee609e78
25 changed files with 3232 additions and 375 deletions

View File

@@ -1,13 +1,19 @@
<template>
<a-config-provider :locale="zhCN">
<component :is="layout">
<router-view />
</component>
<div v-if="appStore.state.initialized" class="app-container">
<component :is="layout">
<router-view />
</component>
</div>
<div v-else class="app-loading">
<a-spin size="large" />
<p>正在初始化应用...</p>
</div>
</a-config-provider>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { useAppStore } from '@/stores/app'
import MainLayout from '@/layouts/MainLayout.vue'
@@ -29,7 +35,9 @@ const layout = computed(() => {
})
// 初始化应用
appStore.initialize()
onMounted(async () => {
await appStore.initialize()
})
// 开发环境调试信息
if (import.meta.env.DEV) {
@@ -42,4 +50,17 @@ if (import.meta.env.DEV) {
min-height: 100vh;
background-color: #f5f5f5;
}
.app-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
}
.app-loading p {
margin-top: 16px;
color: #666;
}
</style>

View File

@@ -55,6 +55,27 @@ export interface AnimalUpdateData {
status?: string
}
// 动物认领相关类型
export interface AnimalClaim {
id: number
animal_id: number
animal_name: string
user_id: number
user_name: string
status: string
reason: string
created_at: string
updated_at: string
}
export interface AnimalClaimQueryParams {
page?: number
limit?: number
status?: string
animal_id?: number
user_id?: number
}
// 获取动物列表
export const getAnimals = (params?: AnimalQueryParams) =>
request.get<{ success: boolean; code: number; message: string; data: { animals: Animal[]; pagination: any } }>('/animals', { params })
@@ -79,11 +100,26 @@ export const deleteAnimal = (id: number) =>
export const updateAnimalStatus = (id: number, status: string) =>
request.put<{ success: boolean; code: number; message: string }>(`/animals/${id}/status`, { status })
// 获取动物认领列表
export const getAnimalClaims = (params?: AnimalClaimQueryParams) =>
request.get<{ success: boolean; code: number; message: string; data: { claims: AnimalClaim[]; pagination: any } }>('/animal-claims', { params })
// 批准动物认领
export const approveAnimalClaim = (id: number, remark: string) =>
request.put<{ success: boolean; code: number; message: string }>(`/animal-claims/${id}/approve`, { remark })
// 拒绝动物认领
export const rejectAnimalClaim = (id: number, remark: string) =>
request.put<{ success: boolean; code: number; message: string }>(`/animal-claims/${id}/reject`, { remark })
export default {
getAnimals,
getAnimal,
createAnimal,
updateAnimal,
deleteAnimal,
updateAnimalStatus
updateAnimalStatus,
getAnimalClaims,
approveAnimalClaim,
rejectAnimalClaim
}

View File

@@ -85,10 +85,35 @@ export const deleteOrder = (id: number) =>
export const updateOrderStatus = (id: number, status: string) =>
request.put<{ success: boolean; code: number; message: string }>(`/orders/${id}/status`, { status })
// 获取订单统计信息
export const getOrderStatistics = () =>
request.get<{ success: boolean; code: number; message: string; data: any }>('/orders/statistics')
// 发货订单
export const shipOrder = (id: number, data: { tracking_no: string; shipping_company: string }) =>
request.put<{ success: boolean; code: number; message: string }>(`/orders/${id}/ship`, data)
// 完成订单
export const completeOrder = (id: number) =>
request.put<{ success: boolean; code: number; message: string }>(`/orders/${id}/complete`)
// 取消订单
export const cancelOrder = (id: number) =>
request.put<{ success: boolean; code: number; message: string }>(`/orders/${id}/cancel`)
// 退款订单
export const refundOrder = (id: number, data: { refund_amount: number; refund_reason: string }) =>
request.put<{ success: boolean; code: number; message: string }>(`/orders/${id}/refund`, data)
export default {
getOrders,
getOrder,
updateOrder,
deleteOrder,
updateOrderStatus
updateOrderStatus,
getOrderStatistics,
shipOrder,
completeOrder,
cancelOrder,
refundOrder
}

View File

@@ -32,11 +32,31 @@ export interface ServiceUpdateData {
// 获取系统服务列表
export const getServices = (params?: ServiceQueryParams) =>
request.get<{ success: boolean; code: number; message: string; data: { services: Service[]; pagination: any } }>('/system/services', { params })
request.get<{ success: boolean; code: number; message: string; data: { services: Service[]; pagination: any } }>('/admin/system/services', { params })
// 更新系统服务状态
export const updateServiceStatus = (id: string, data: ServiceUpdateData) =>
request.put<{ success: boolean; code: number; message: string }>(`/system/services/${id}/status`, data)
request.put<{ success: boolean; code: number; message: string }>(`/admin/system/services/${id}/status`, data)
// 启动服务
export const startService = (id: string) =>
request.post<{ success: boolean; code: number; message: string }>(`/admin/system/services/${id}/start`)
// 停止服务
export const stopService = (id: string) =>
request.post<{ success: boolean; code: number; message: string }>(`/admin/system/services/${id}/stop`)
// 获取系统信息
export const getSystemInfo = () =>
request.get<{ success: boolean; code: number; message: string; data: any }>('/admin/system/info')
// 获取数据库状态
export const getDatabaseStatus = () =>
request.get<{ success: boolean; code: number; message: string; data: any }>('/admin/system/database-status')
// 获取缓存状态
export const getCacheStatus = () =>
request.get<{ success: boolean; code: number; message: string; data: any }>('/admin/system/cache-status')
// 定义系统配置相关类型
export interface SystemConfig {
@@ -62,25 +82,40 @@ export interface SystemConfigUpdateData {
// 获取系统配置列表
export const getSystemConfigs = (params?: SystemConfigQueryParams) =>
request.get<{ success: boolean; code: number; message: string; data: { configs: SystemConfig[]; pagination: any } }>('/system-configs', { params })
request.get<{ success: boolean; code: number; message: string; data: { configs: SystemConfig[]; pagination: any } }>('/admin/system-configs', { params })
// 更新系统配置
export const updateSystemConfig = (id: string, data: SystemConfigUpdateData) =>
request.put<{ success: boolean; code: number; message: string }>(`/system-configs/${id}`, data)
request.put<{ success: boolean; code: number; message: string }>(`/admin/system-configs/${id}`, data)
// 获取系统统计信息
export const getSystemStats = () =>
request.get<{ success: boolean; code: number; message: string; data: any }>('/system/stats')
request.get<{ success: boolean; code: number; message: string; data: any }>('/admin/system/stats')
// 获取系统日志
export const getSystemLogs = (params?: { page?: number; limit?: number; level?: string }) =>
request.get<{ success: boolean; code: number; message: string; data: { logs: any[]; pagination: any } }>('/system/logs', { params })
request.get<{ success: boolean; code: number; message: string; data: { logs: any[]; pagination: any } }>('/admin/system/logs', { params })
// 获取系统设置
export const getSystemSettings = () =>
request.get<{ success: boolean; code: number; message: string; data: any }>('/admin/system/settings')
// 更新系统设置
export const updateSystemSettings = (data: any) =>
request.put<{ success: boolean; code: number; message: string }>(`/admin/system/settings`, data)
export default {
getServices,
updateServiceStatus,
startService,
stopService,
getSystemInfo,
getDatabaseStatus,
getCacheStatus,
getSystemConfigs,
updateSystemConfig,
getSystemStats,
getSystemLogs
}
getSystemLogs,
getSystemSettings,
updateSystemSettings
}

View File

@@ -72,11 +72,21 @@ export const deleteTravel = (id: number) =>
export const updateTravelStatus = (id: number, status: string) =>
request.put<{ success: boolean; code: number; message: string }>(`/travels/${id}/status`, { status })
// 获取结伴游计划列表
export const getTravelPlans = (params?: TravelQueryParams) =>
request.get<{ success: boolean; code: number; message: string; data: { travels: Travel[]; pagination: any } }>('/travel-plans', { params })
// 关闭结伴游计划
export const closeTravelPlan = (id: number) =>
request.put<{ success: boolean; code: number; message: string }>(`/travel-plans/${id}/close`)
export default {
getTravels,
getTravel,
createTravel,
updateTravel,
deleteTravel,
updateTravelStatus
updateTravelStatus,
getTravelPlans,
closeTravelPlan
}

View File

@@ -6,7 +6,7 @@
>
<template #extra>
<a-space>
<a-button>刷新</a-button>
<a-button @click="loadDashboardData">刷新</a-button>
<a-button type="primary">导出数据</a-button>
</a-space>
</template>
@@ -20,7 +20,7 @@
<a-card>
<a-statistic
title="总用户数"
:value="11284"
:value="dashboardData.userCount"
:precision="0"
suffix="人"
>
@@ -34,7 +34,7 @@
<a-card>
<a-statistic
title="商家数量"
:value="356"
:value="dashboardData.merchantCount"
:precision="0"
suffix="家"
>
@@ -48,7 +48,7 @@
<a-card>
<a-statistic
title="旅行计划"
:value="1287"
:value="dashboardData.travelCount"
:precision="0"
suffix="个"
>
@@ -62,7 +62,7 @@
<a-card>
<a-statistic
title="动物认领"
:value="542"
:value="dashboardData.animalCount"
:precision="0"
suffix="只"
>
@@ -138,19 +138,44 @@
</template>
<script setup lang="ts">
import {
UserOutlined,
ShopOutlined,
CompassOutlined,
HeartOutlined,
BarChartOutlined,
PieChartOutlined
} from '@ant-design/icons-vue'
import { UserOutlined, ShopOutlined, CompassOutlined, HeartOutlined, BarChartOutlined, PieChartOutlined } from '@ant-design/icons-vue'
import { ref, onMounted } from 'vue'
import { useAppStore } from '@/stores/app'
import { getSystemStats } from '@/api/system'
import { getUsers } from '@/api/user'
import { getMerchants } from '@/api/merchant'
import { getTravels } from '@/api/travel'
import { getAnimals } from '@/api/animal'
// 定义仪表板数据结构
interface DashboardData {
userCount: number
merchantCount: number
travelCount: number
animalCount: number
orderCount: number
todayUserCount: number
todayOrderCount: number
}
const appStore = useAppStore()
const appVersion = import.meta.env.VITE_APP_VERSION || '1.0.0'
const environment = import.meta.env.MODE || 'development'
const startupTime = new Date().toLocaleString()
// 仪表板数据
const dashboardData = ref<DashboardData>({
userCount: 0,
merchantCount: 0,
travelCount: 0,
animalCount: 0,
orderCount: 0,
todayUserCount: 0,
todayOrderCount: 0
})
// 最近活动(暂时保持静态数据)
const recentActivities = [
{
title: '新用户注册',
@@ -177,6 +202,71 @@ const recentActivities = [
time: '15分钟前'
}
]
// 加载仪表板数据
const loadDashboardData = async () => {
try {
// 检查用户是否已登录
if (!appStore.state.user) {
// 尝试重新初始化应用状态
await appStore.initialize()
// 如果仍然没有用户信息,则跳转到登录页
if (!appStore.state.user) {
window.location.href = '/login'
return
}
}
// 获取系统统计信息
const statsResponse = await getSystemStats()
if (statsResponse.success) {
// 从statsResponse.data中获取统计数据
const data = statsResponse.data
dashboardData.value.userCount = data.userCount
dashboardData.value.merchantCount = data.merchantCount
dashboardData.value.travelCount = data.travelCount
dashboardData.value.animalCount = data.animalCount
dashboardData.value.orderCount = data.orderCount
dashboardData.value.todayUserCount = data.todayUserCount
dashboardData.value.todayOrderCount = data.todayOrderCount
}
// 如果系统统计API不可用则使用独立的API调用来获取各类统计数据
if (!statsResponse.success) {
// 获取用户总数
const userResponse = await getUsers({ page: 1, pageSize: 1 })
if (userResponse.data.success) {
dashboardData.value.userCount = userResponse.data.pagination?.total || 0
}
// 获取商家总数
const merchantResponse = await getMerchants({ page: 1, limit: 1 })
if (merchantResponse.data.success) {
dashboardData.value.merchantCount = merchantResponse.data.pagination?.total || 0
}
// 获取旅行计划总数
const travelResponse = await getTravels({ page: 1, limit: 1 })
if (travelResponse.data.success) {
dashboardData.value.travelCount = travelResponse.data.pagination?.total || 0
}
// 获取动物总数
const animalResponse = await getAnimals({ page: 1, limit: 1 })
if (animalResponse.data.success) {
dashboardData.value.animalCount = animalResponse.data.pagination?.total || 0
}
}
} catch (error) {
console.error('加载仪表板数据失败:', error)
}
}
// 组件挂载时加载数据
onMounted(() => {
loadDashboardData()
})
</script>
<style scoped>
@@ -199,47 +289,18 @@ const recentActivities = [
}
.chart-card,
.activity-card,
.info-card {
border-radius: 8px;
height: 100%;
}
.chart-placeholder {
height: 300px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #999;
font-size: 16px;
text-align: center;
padding: 40px 0;
color: #ccc;
}
.chart-placeholder .anticon {
font-size: 48px;
margin-bottom: 16px;
color: #1890ff;
}
:deep(.ant-card-head) {
border-bottom: 1px solid #f0f0f0;
}
:deep(.ant-card-body) {
padding: 24px;
}
:deep(.ant-statistic) {
text-align: center;
}
:deep(.ant-statistic-title) {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
:deep(.ant-statistic-content) {
font-size: 24px;
font-weight: 600;
}
</style>

View File

@@ -1,5 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import { useAppStore } from '@/stores/app'
// 基础路由
const routes: RouteRecordRaw[] = [
@@ -125,7 +126,14 @@ const router = createRouter({
// 路由守卫
router.beforeEach(async (to, from, next) => {
const { meta } = to
const isAuthenticated = localStorage.getItem('admin_token') !== null
const appStore = useAppStore()
// 确保应用已初始化
if (!appStore.state.initialized) {
await appStore.initialize()
}
const isAuthenticated = !!appStore.state.user
// 检查是否需要认证
if (meta.requiresAuth && !isAuthenticated) {

View File

@@ -32,16 +32,27 @@ export const useAppStore = defineStore('app', () => {
if (token) {
// 获取用户信息
const response = await authAPI.getCurrentUser()
// 修复数据结构访问问题
if (response?.data?.admin) {
// 统一处理接口响应格式
if (!response || typeof response !== 'object') {
throw new Error('获取用户信息失败:接口返回格式异常')
}
// 确保响应数据格式为 { data: { admin: object } }
if (response.data && typeof response.data === 'object' && response.data.admin) {
state.user = response.data.admin
} else if (response?.data?.user) {
state.user = response.data.user
} else {
throw new Error('获取用户信息失败:响应数据格式不符合预期')
}
}
} catch (error) {
console.error('初始化失败:', error)
console.error('初始化失败:', {
error: error,
timestamp: new Date().toISOString(),
token: localStorage.getItem('admin_token')
})
clearUser()
throw error // 抛出错误以便调用方处理
} finally {
state.loading = false
state.initialized = true
@@ -54,13 +65,29 @@ export const useAppStore = defineStore('app', () => {
try {
const response = await authAPI.login(credentials)
// 保存token
localStorage.setItem('admin_token', response.data.token)
// 保存token - 修复数据结构访问问题
if (response?.data?.token) {
localStorage.setItem('admin_token', response.data.token)
} else if (response?.token) {
localStorage.setItem('admin_token', response.token)
} else {
throw new Error('登录响应中缺少token')
}
// 设置用户信息
state.user = response.data.admin
// 设置用户信息 - 修复数据结构访问问题
if (response?.data?.admin) {
state.user = response.data.admin
} else if (response?.admin) {
state.user = response.admin
} else {
throw new Error('登录响应中缺少用户信息')
}
return response
} catch (error) {
// 登录失败时清除可能存在的token
clearUser()
throw error
} finally {
state.loading = false
}

View File

@@ -92,6 +92,23 @@ app.get('/health', (req, res) => {
});
});
// 系统统计路由
app.get('/system-stats', (req, res) => {
const stats = {
status: 'OK',
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV || 'development',
nodeVersion: process.version,
memoryUsage: process.memoryUsage(),
uptime: process.uptime(),
cpuCount: require('os').cpus().length,
platform: process.platform,
architecture: process.arch
};
res.status(200).json(stats);
});
// API路由
app.use('/api/v1/auth', authRoutes);
app.use('/api/v1/users', userRoutes);

View File

@@ -0,0 +1,90 @@
// 系统统计控制器
const systemStatsService = require('../../services/admin/systemStats');
/**
* 获取系统统计数据
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
*/
exports.getSystemStats = async (req, res, next) => {
try {
const stats = await systemStatsService.getSystemStats();
res.json({
success: true,
code: 200,
message: '获取系统统计数据成功',
data: stats,
timestamp: new Date().toISOString()
});
} catch (error) {
next(error);
}
};
/**
* 获取用户统计数据
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
*/
exports.getUserStats = async (req, res, next) => {
try {
const stats = await systemStatsService.getUserStats();
res.json({
success: true,
code: 200,
message: '获取用户统计数据成功',
data: stats,
timestamp: new Date().toISOString()
});
} catch (error) {
next(error);
}
};
/**
* 获取订单统计数据
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
*/
exports.getOrderStats = async (req, res, next) => {
try {
const stats = await systemStatsService.getOrderStats();
res.json({
success: true,
code: 200,
message: '获取订单统计数据成功',
data: stats,
timestamp: new Date().toISOString()
});
} catch (error) {
next(error);
}
};
/**
* 获取系统信息
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
*/
exports.getSystemInfo = async (req, res, next) => {
try {
const info = await systemStatsService.getSystemInfo();
res.json({
success: true,
code: 200,
message: '获取系统信息成功',
data: info,
timestamp: new Date().toISOString()
});
} catch (error) {
next(error);
}
};

View File

@@ -21,7 +21,7 @@ const options = {
},
servers: [
{
url: 'http://localhost:3000/api/v1',
url: 'http://localhost:3100/api/v1',
description: '开发环境'
},
{
@@ -91,235 +91,199 @@ const options = {
}
}
},
// 用户模型
User: {
type: 'object',
properties: {
id: {
type: 'integer',
example: 1
},
username: {
type: 'string',
example: 'testuser'
},
nickname: {
type: '极速版string',
example: '测试用户'
},
email: {
type: 'string',
example: 'test@example.com'
},
phone: {
type: 'string',
example: '13800138000'
},
avatar: {
type: 'string',
example: 'https://example.com/avatar.jpg'
},
gender: {
type: 'string',
enum: ['male', 'female', 'unknown'],
example: 'male'
},
birthday: {
type: 'string',
format: 'date',
example: '1990-01-01'
},
points: {
type: 'integer',
example: 1000
},
level: {
type: 'integer',
example: 3
},
status: {
type: 'string',
enum: ['active', 'inactive', 'banned'],
example: 'active'
},
created_at: {
type: 'string',
format: 'date-time'
},
updated_at: {
type: 'string',
format: 'date-time'
},
last_login_at: {
type: 'string',
format: 'date-time',
description: '最后登录时间'
}
}
},
// 管理员模型
Admin: {
type: 'object',
properties: {
id: {
type: 'integer',
description: '管理员ID'
example: 1
},
username: {
type: 'string',
description: '用户名'
example: 'admin'
},
email: {
type: 'string',
description: '邮箱'
example: 'admin@jiebanke.com'
},
nickname: {
type: 'string',
description: '昵称'
example: '管理员'
},
avatar: {
type: 'string',
description: '头像URL'
example: 'https://example.com/avatar.jpg'
},
role: {
type: 'string',
description: '角色'
example: 'super_admin'
},
status: {
type: 'integer',
description: '状态 (1:启用, 0:禁用)'
type: 'string',
example: 'active'
},
last_login: {
type: 'string',
format: 'date-time',
description: '最后登录时间'
example: '2023-01-01T00:00:00Z'
},
created_at: {
type: 'string',
format: 'date-time',
description: '创建时间'
example: '2023-01-01T00:00:00Z'
},
updated_at: {
type: 'string',
format: 'date-time',
description: '更新时间'
example: '2023-01-01T00:00:00Z'
}
}
},
// 分页模型
Pagination: {
// 系统统计数据模型
SystemStats: {
type: 'object',
properties: {
userCount: {
type: 'integer',
description: '用户总数'
},
merchantCount: {
type: 'integer',
description: '商家数量'
},
travelCount: {
type: 'integer',
description: '旅行计划数'
},
animalCount: {
type: 'integer',
description: '动物数量'
},
orderCount: {
type: 'integer',
description: '订单总数'
},
todayUserCount: {
type: 'integer',
description: '今日新增用户数'
},
todayOrderCount: {
type: 'integer',
description: '今日新增订单数'
}
}
},
// 用户统计数据模型
UserStats: {
type: 'object',
properties: {
total: {
type: 'integer',
example: 100
description: '用户总数'
},
page: {
type: 'integer',
example: 1
byType: {
type: 'array',
description: '按类型统计',
items: {
type: 'object',
properties: {
user_type: {
type: 'string'
},
count: {
type: 'integer'
}
}
}
},
pageSize: {
byDate: {
type: 'array',
description: '按日期统计',
items: {
type: 'object',
properties: {
date: {
type: 'string'
},
count: {
type: 'integer'
}
}
}
}
}
},
// 订单统计数据模型
OrderStats: {
type: 'object',
properties: {
total: {
type: 'integer',
example: 20
description: '订单总数'
},
totalPages: {
totalAmount: {
type: 'number',
description: '订单总金额'
},
byStatus: {
type: 'array',
description: '按状态统计',
items: {
type: 'object',
properties: {
status: {
type: 'string'
},
count: {
type: 'integer'
}
}
}
},
byDate: {
type: 'array',
description: '按日期统计',
items: {
type: 'object',
properties: {
date: {
type: 'string'
},
count: {
type: 'integer'
},
amount: {
type: 'number'
}
}
}
}
}
},
// 系统信息模型
SystemInfo: {
type: 'object',
properties: {
nodeVersion: {
type: 'string',
description: 'Node.js版本'
},
platform: {
type: 'string',
description: '运行平台'
},
arch: {
type: 'string',
description: '系统架构'
},
uptime: {
type: 'integer',
example: 5
}
}
}
},
parameters: {
// 通用分页参数
PageParam: {
in: 'query',
name: 'page',
schema: {
type: 'integer',
minimum: 1,
default: 1
},
description: '页码'
},
PageSizeParam: {
in: 'query',
name: 'pageSize',
schema: {
type: 'integer',
minimum: 1,
maximum: 100,
default: 20
},
description: '每页数量'
}
},
responses: {
// 通用响应
UnauthorizedError: {
description: '未授权访问',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ErrorResponse'
},
example: {
success: false,
code: 401,
message: '未授权访问',
error: 'Token已过期或无效',
timestamp: '2025-01-01T00:00:00.000Z'
}
}
}
},
ForbiddenError: {
description: '禁止访问',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ErrorResponse'
},
example: {
success: false,
code: 403,
message: '禁止访问',
error: '权限不足',
timestamp: '2025-01-01T00:00:00.000Z'
}
}
}
},
NotFoundError: {
description: '资源不存在',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ErrorResponse'
},
example: {
success: false,
code: 404,
message: '资源不存在',
error: '请求的资源不存在',
timestamp: '2025-01-01T00:00:00.000Z'
}
}
}
},
ValidationError: {
description: '参数验证错误',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ErrorResponse'
},
example: {
success: false,
code: 400,
message: '参数验证错误',
error: '用户名必须为4-20个字符',
timestamp: '2025-01-01T00:00:00.000Z'
}
description: '运行时间(秒)'
},
databaseVersion: {
type: 'string',
description: '数据库版本'
}
}
}
@@ -333,20 +297,8 @@ const options = {
},
apis: [
'./src/routes/*.js',
'./src/controllers/*.js',
'./src/models/*.js'
'./src/docs/*.js'
]
};
const specs = swaggerJsdoc(options);
module.exports = {
swaggerUi,
specs,
serve: swaggerUi.serve,
setup: swaggerUi.setup(specs, {
explorer: true,
customCss: '.swagger-ui .topbar { display: none }',
customSiteTitle: '结伴客系统 API文档'
})
};
module.exports = swaggerJsdoc(options);

View File

@@ -1,7 +1,143 @@
/**
* 系统统计控制器
* @module controllers/admin/systemStats
*/
/**
* 获取系统统计数据
* @function getSystemStats
* @param {Object} req - Express请求对象
* @param {Object} res - Express响应对象
* @param {Function} next - Express中间件next函数
*/
exports.getSystemStats = async (req, res, next) => {
try {
// 这里应该从数据库获取真实数据
const stats = {
userCount: 1000,
merchantCount: 50,
travelCount: 200,
animalCount: 300,
orderCount: 1500,
todayUserCount: 20,
todayOrderCount: 35
};
res.json({
success: true,
code: 200,
message: '获取成功',
data: stats
});
} catch (error) {
next(error);
}
};
/**
* 获取用户统计数据
* @function getUserStats
* @param {Object} req - Express请求对象
* @param {Object} res - Express响应对象
* @param {Function} next - Express中间件next函数
*/
exports.getUserStats = async (req, res, next) => {
try {
// 这里应该从数据库获取真实数据
const userStats = {
total: 1000,
byType: [
{ user_type: '普通用户', count: 900 },
{ user_type: '商家', count: 50 },
{ user_type: '管理员', count: 50 }
],
byDate: [
{ date: '2023-04-01', count: 10 },
{ date: '2023-04-02', count: 15 },
{ date: '2023-04-03', count: 20 }
]
};
res.json({
success: true,
code: 200,
message: '获取成功',
data: userStats
});
} catch (error) {
next(error);
}
};
/**
* 获取订单统计数据
* @function getOrderStats
* @param {Object} req - Express请求对象
* @param {Object} res - Express响应对象
* @param {Function} next - Express中间件next函数
*/
exports.getOrderStats = async (req, res, next) => {
try {
// 这里应该从数据库获取真实数据
const orderStats = {
total: 1500,
totalAmount: 100000,
byStatus: [
{ status: '待支付', count: 100 },
{ status: '已支付', count: 1200 },
{ status: '已取消', count: 200 }
],
byDate: [
{ date: '2023-04-01', count: 50, amount: 2500 },
{ date: '2023-04-02', count: 75, amount: 4000 },
{ date: '2023-04-03', count: 100, amount: 6000 }
]
};
res.json({
success: true,
code: 200,
message: '获取成功',
data: orderStats
});
} catch (error) {
next(error);
}
};
/**
* 获取系统信息
* @function getSystemInfo
* @param {Object} req - Express请求对象
* @param {Object} res - Express响应对象
* @param {Function} next - Express中间件next函数
*/
exports.getSystemInfo = async (req, res, next) => {
try {
// 获取系统信息
const systemInfo = {
nodeVersion: process.version,
platform: process.platform,
arch: process.arch,
uptime: Math.floor(process.uptime()),
databaseVersion: 'MySQL 8.0.28' // 这里应该从数据库获取真实版本信息
};
res.json({
success: true,
code: 200,
message: '获取成功',
data: systemInfo
});
} catch (error) {
next(error);
}
};
// 管理员路由
const express = require('express');
const router = express.Router();
const adminController = require('../controllers/admin');
const systemStatsController = require('../controllers/admin/systemStats');
const { authenticateAdmin } = require('../middleware/auth');
/**
@@ -62,16 +198,14 @@ const { authenticateAdmin } = require('../middleware/auth');
router.post('/login', adminController.login);
// 需要认证的接口
router.use(authenticateAdmin);
/**
* @swagger
* /admin/profile:
* get:
* summary: 获取当前管理员信息
* summary: 获取管理员个人信息
* tags: [Admin]
* security:
* - bearerAuth: []
* - BearerAuth: []
* responses:
* 200:
* description: 获取成功
@@ -93,10 +227,12 @@ router.use(authenticateAdmin);
* $ref: '#/components/schemas/Admin'
* 401:
* description: 未授权
* 404:
* description: 管理员不存在
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
router.get('/profile', adminController.getProfile);
router.get('/profile', authenticateAdmin, adminController.getProfile);
/**
* @swagger
@@ -324,4 +460,227 @@ router.put('/:id', adminController.update);
*/
router.delete('/:id', adminController.delete);
/**
* @swagger
* /admin/system/stats:
* get:
* summary: 获取系统统计数据
* tags: [Admin]
* security:
* - BearerAuth: []
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* code:
* type: integer
* message:
* type: string
* data:
* type: object
* properties:
* userCount:
* type: integer
* description: 用户总数
* merchantCount:
* type: integer
* description: 商家数量
* travelCount:
* type: integer
* description: 旅行计划数
* animalCount:
* type: integer
* description: 动物数量
* orderCount:
* type: integer
* description: 订单总数
* todayUserCount:
* type: integer
* description: 今日新增用户数
* todayOrderCount:
* type: integer
* description: 今日新增订单数
* 401:
* description: 未授权
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
router.get('/system/stats', authenticateAdmin, systemStatsController.getSystemStats);
/**
* @swagger
* /admin/system/user-stats:
* get:
* summary: 获取用户统计数据
* tags: [Admin]
* security:
* - BearerAuth: []
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* code:
* type: integer
* message:
* type: string
* data:
* type: object
* properties:
* total:
* type: integer
* description: 用户总数
* byType:
* type: array
* description: 按类型统计
* items:
* type: object
* properties:
* user_type:
* type: string
* count:
* type: integer
* byDate:
* type: array
* description: 按日期统计
* items:
* type: object
* properties:
* date:
* type: string
* count:
* type: integer
* 401:
* description: 未授权
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
router.get('/system/user-stats', authenticateAdmin, systemStatsController.getUserStats);
/**
* @swagger
* /admin/system/order-stats:
* get:
* summary: 获取订单统计数据
* tags: [Admin]
* security:
* - BearerAuth: []
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* code:
* type: integer
* message:
* type: string
* data:
* type: object
* properties:
* total:
* type: integer
* description: 订单总数
* totalAmount:
* type: number
* description: 订单总金额
* byStatus:
* type: array
* description: 按状态统计
* items:
* type: object
* properties:
* status:
* type: string
* count:
* type: integer
* byDate:
* type: array
* description: 按日期统计
* items:
* type: object
* properties:
* date:
* type: string
* count:
* type: integer
* amount:
* type: number
* 401:
* description: 未授权
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
router.get('/system/order-stats', authenticateAdmin, systemStatsController.getOrderStats);
/**
* @swagger
* /admin/system/info:
* get:
* summary: 获取系统信息
* tags: [Admin]
* security:
* - BearerAuth: []
* responses:
* 200:
* description: 获取成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* code:
* type: integer
* message:
* type: string
* data:
* type: object
* properties:
* nodeVersion:
* type: string
* description: Node.js版本
* platform:
* type: string
* description: 运行平台
* arch:
* type: string
* description: 系统架构
* uptime:
* type: integer
* description: 运行时间(秒)
* databaseVersion:
* type: string
* description: 数据库版本
* 401:
* description: 未授权
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
router.get('/system/info', authenticateAdmin, systemStatsController.getSystemInfo);
module.exports = router;

View File

@@ -0,0 +1,164 @@
// 系统统计服务
const database = require('../../config/database');
class SystemStatsService {
/**
* 获取系统统计数据
* @returns {Promise<Object>} 系统统计数据
*/
async getSystemStats() {
try {
// 获取用户总数
const userCountResult = await database.query('SELECT COUNT(*) as count FROM users');
const userCount = userCountResult[0].count;
// 获取商家总数
const merchantCountResult = await database.query('SELECT COUNT(*) as count FROM merchants');
const merchantCount = merchantCountResult[0].count;
// 获取旅行计划总数
const travelCountResult = await database.query('SELECT COUNT(*) as count FROM travel_plans');
const travelCount = travelCountResult[0].count;
// 获取动物总数
const animalCountResult = await database.query('SELECT COUNT(*) as count FROM animals');
const animalCount = animalCountResult[0].count;
// 获取订单总数
const orderCountResult = await database.query('SELECT COUNT(*) as count FROM orders');
const orderCount = orderCountResult[0].count;
// 获取今日新增用户数
const todayUserCountResult = await database.query(
'SELECT COUNT(*) as count FROM users WHERE DATE(created_at) = CURDATE()'
);
const todayUserCount = todayUserCountResult[0].count;
// 获取今日新增订单数
const todayOrderCountResult = await database.query(
'SELECT COUNT(*) as count FROM orders WHERE DATE(ordered_at) = CURDATE()'
);
const todayOrderCount = todayOrderCountResult[0].count;
return {
userCount,
merchantCount,
travelCount,
animalCount,
orderCount,
todayUserCount,
todayOrderCount
};
} catch (error) {
throw new Error(`获取系统统计数据失败: ${error.message}`);
}
}
/**
* 获取用户统计信息
* @returns {Promise<Object>} 用户统计数据
*/
async getUserStats() {
try {
// 用户总数
const totalResult = await database.query('SELECT COUNT(*) as total FROM users');
// 按用户类型统计
const byTypeResult = await database.query(`
SELECT
user_type,
COUNT(*) as count
FROM users
GROUP BY user_type
`);
// 按注册时间统计近7天
const byDateResult = await database.query(`
SELECT
DATE(created_at) as date,
COUNT(*) as count
FROM users
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY DATE(created_at)
ORDER BY date ASC
`);
return {
total: totalResult[0].total,
byType: byTypeResult,
byDate: byDateResult
};
} catch (error) {
throw new Error(`获取用户统计数据失败: ${error.message}`);
}
}
/**
* 获取订单统计信息
* @returns {Promise<Object>} 订单统计数据
*/
async getOrderStats() {
try {
// 订单总数和总金额
const totalResult = await database.query(`
SELECT
COUNT(*) as total,
COALESCE(SUM(total_amount), 0) as totalAmount
FROM orders
`);
// 按状态统计
const byStatusResult = await database.query(`
SELECT
status,
COUNT(*) as count
FROM orders
GROUP BY status
`);
// 按日期统计近7天
const byDateResult = await database.query(`
SELECT
DATE(ordered_at) as date,
COUNT(*) as count,
COALESCE(SUM(total_amount), 0) as amount
FROM orders
WHERE ordered_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY DATE(ordered_at)
ORDER BY date ASC
`);
return {
total: totalResult[0].total,
totalAmount: parseFloat(totalResult[0].totalAmount || 0),
byStatus: byStatusResult,
byDate: byDateResult
};
} catch (error) {
throw new Error(`获取订单统计数据失败: ${error.message}`);
}
}
/**
* 获取系统信息
* @returns {Promise<Object>} 系统信息
*/
async getSystemInfo() {
try {
// 数据库信息
const dbInfo = await database.query('SELECT VERSION() as version');
return {
nodeVersion: process.version,
platform: process.platform,
arch: process.arch,
uptime: Math.floor(process.uptime()),
databaseVersion: dbInfo[0].version
};
} catch (error) {
throw new Error(`获取系统信息失败: ${error.message}`);
}
}
}
module.exports = new SystemStatsService();

View File

@@ -34,7 +34,14 @@ const endpoints = {
LIST: '/travel/list',
DETAIL: '/travel/detail',
CREATE: '/travel/create',
UPDATE: '/travel/update',
DELETE: '/travel/delete',
JOIN: '/travel/join',
QUIT: '/travel/quit',
LIKE: '/travel/like',
UNLIKE: '/travel/unlike',
COMMENT: '/travel/comment',
COMMENTS: '/travel/comments',
MY_PLANS: '/travel/my-plans',
SEARCH: '/travel/search'
},

View File

@@ -39,8 +39,29 @@ export const travelService = {
// 创建旅行计划
create: (data) => request.post(endpoints.TRAVEL.CREATE, data),
// 更新旅行计划
update: (id, data) => request.put(`${endpoints.TRAVEL.UPDATE}/${id}`, data),
// 删除旅行计划
deletePlan: (id) => request.delete(`${endpoints.TRAVEL.DELETE}/${id}`),
// 加入旅行计划
join: (travelId) => request.post(`${endpoints.TRAVEL.JOIN}/${travelId}`),
joinPlan: (travelId) => request.post(`${endpoints.TRAVEL.JOIN}/${travelId}`),
// 退出旅行计划
quitPlan: (travelId) => request.post(`${endpoints.TRAVEL.QUIT}/${travelId}`),
// 点赞旅行计划
likePlan: (travelId) => request.post(`${endpoints.TRAVEL.LIKE}/${travelId}`),
// 取消点赞旅行计划
unlikePlan: (travelId) => request.post(`${endpoints.TRAVEL.UNLIKE}/${travelId}`),
// 添加评论
addComment: (travelId, data) => request.post(`${endpoints.TRAVEL.COMMENT}/${travelId}`, data),
// 获取评论列表
getComments: (travelId, params = {}) => request.get(`${endpoints.TRAVEL.COMMENTS}/${travelId}`, params),
// 获取我的旅行计划
getMyPlans: (params = {}) => request.get(endpoints.TRAVEL.MY_PLANS, params),

View File

@@ -12,19 +12,19 @@
"build:mp-weixin": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin vue-cli-service uni-build"
},
"dependencies": {
"@dcloudio/uni-app": "^3.0.0",
"@dcloudio/uni-components": "^3.0.0",
"@dcloudio/uni-h5": "^3.0.0",
"@dcloudio/uni-mp-weixin": "^3.0.0",
"@dcloudio/uni-app": "^3.4.19",
"@dcloudio/uni-components": "^3.4.19",
"@dcloudio/uni-h5": "^3.4.19",
"@dcloudio/uni-mp-weixin": "^3.4.19",
"vue": "^3.2.0",
"vuex": "^4.0.0"
},
"devDependencies": {
"@dcloudio/types": "^3.0.0",
"@dcloudio/uni-cli-shared": "^3.0.0",
"@dcloudio/uni-migration": "^3.0.0",
"@dcloudio/uni-template-compiler": "^3.0.0",
"@dcloudio/vue-cli-plugin-uni": "^3.0.0",
"@dcloudio/types": "^3.4.19",
"@dcloudio/uni-cli-shared": "^3.4.19",
"@dcloudio/uni-migration": "^3.4.19",
"@dcloudio/uni-template-compiler": "^3.4.19",
"@dcloudio/vue-cli-plugin-uni": "^3.4.19",
"@vue/cli-service": "^5.0.0",
"cross-env": "^7.0.0",
"sass": "^1.32.0",

View File

@@ -55,6 +55,22 @@
"navigationBarBackgroundColor": "#007aff",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/travel/publish",
"style": {
"navigationBarTitleText": "发布旅行",
"navigationBarBackgroundColor": "#007aff",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/travel/comments",
"style": {
"navigationBarTitleText": "评论详情",
"navigationBarBackgroundColor": "#007aff",
"navigationBarTextStyle": "white"
}
}
],
"globalStyle": {

View File

@@ -0,0 +1,552 @@
<template>
<view class="comments-page">
<!-- 头部导航 -->
<view class="header">
<view class="back-btn" @click="goBack">
<uni-icons type="arrowleft" size="24" color="#333"></uni-icons>
</view>
<text class="title">全部评论 ({{ totalCount }})</text>
<view class="placeholder"></view>
</view>
<!-- 评论列表 -->
<scroll-view
class="comments-list"
scroll-y
@scrolltolower="loadMore"
:scroll-with-animation="true"
>
<view v-if="loading && comments.length === 0" class="loading-container">
<uni-loading size="32" color="#007aff"></uni-loading>
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="comments.length === 0" class="empty-container">
<uni-icons type="chat" size="64" color="#ccc"></uni-icons>
<text class="empty-text">暂无评论</text>
</view>
<view v-else>
<view
v-for="comment in comments"
:key="comment.id"
class="comment-item"
>
<image
:src="comment.user.avatar || '/static/avatar/default.png'"
class="comment-avatar"
mode="aspectFill"
></image>
<view class="comment-content">
<view class="comment-header">
<text class="comment-user">{{ comment.user.nickname }}</text>
<text class="comment-time">{{ formatTime(comment.createTime) }}</text>
</view>
<text class="comment-text">{{ comment.content }}</text>
<view class="comment-actions">
<view class="action-btn" @click="likeComment(comment)">
<uni-icons
:type="comment.isLiked ? 'heart-filled' : 'heart'"
size="16"
:color="comment.isLiked ? '#ff4757' : '#999'"
></uni-icons>
<text class="action-count">{{ comment.likeCount || 0 }}</text>
</view>
<view class="action-btn" @click="replyComment(comment)">
<uni-icons type="chat" size="16" color="#999"></uni-icons>
<text class="action-count">回复</text>
</view>
</view>
<!-- 回复列表 -->
<view v-if="comment.replies && comment.replies.length > 0" class="replies-container">
<view
v-for="reply in comment.replies"
:key="reply.id"
class="reply-item"
>
<text class="reply-user">{{ reply.user.nickname }}</text>
<text class="reply-text">: {{ reply.content }}</text>
<text class="reply-time">{{ formatTime(reply.createTime) }}</text>
</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="hasMore" class="load-more">
<uni-loading size="24" color="#999" v-if="loadingMore"></uni-loading>
<text class="load-more-text" v-else>上拉加载更多</text>
</view>
<view v-else class="no-more">
<text>没有更多评论了</text>
</view>
</view>
</scroll-view>
<!-- 底部输入框 -->
<view class="comment-input-container">
<input
v-model="newComment"
class="comment-input"
placeholder="写下你的评论..."
:focus="isFocus"
@focus="onInputFocus"
@blur="onInputBlur"
@confirm="submitComment"
/>
<view
class="send-btn"
:class="{ active: newComment.trim() }"
@click="submitComment"
>
<text>发送</text>
</view>
</view>
<!-- 回复面板 -->
<view
v-if="showReplyPanel"
class="reply-panel"
:style="{ bottom: inputHeight + 'px' }"
>
<text class="reply-hint">回复 @{{ replyTarget.user.nickname }}</text>
<view class="close-reply" @click="cancelReply">
<uni-icons type="close" size="20" color="#999"></uni-icons>
</view>
</view>
</view>
</template>
<script>
import travelService from '@/api/services'
export default {
data() {
return {
travelId: null,
comments: [],
totalCount: 0,
loading: false,
loadingMore: false,
hasMore: true,
page: 1,
pageSize: 10,
newComment: '',
isFocus: false,
inputHeight: 0,
showReplyPanel: false,
replyTarget: null
}
},
onLoad(options) {
this.travelId = options.id
this.loadComments()
},
onReady() {
this.getInputHeight()
},
methods: {
async loadComments() {
if (this.loading) return
this.loading = true
try {
const params = {
page: this.page,
pageSize: this.pageSize
}
const res = await travelService.getComments(this.travelId, params)
const { list, total } = res.data
if (this.page === 1) {
this.comments = list
} else {
this.comments = [...this.comments, ...list]
}
this.totalCount = total
this.hasMore = this.comments.length < total
} catch (error) {
uni.showToast({
title: '加载评论失败',
icon: 'none'
})
} finally {
this.loading = false
this.loadingMore = false
}
},
async loadMore() {
if (this.loadingMore || !this.hasMore) return
this.loadingMore = true
this.page += 1
await this.loadComments()
},
async submitComment() {
if (!this.newComment.trim()) {
uni.showToast({
title: '评论内容不能为空',
icon: 'none'
})
return
}
try {
let commentData = { content: this.newComment.trim() }
// 如果是回复
if (this.showReplyPanel && this.replyTarget) {
commentData.replyTo = this.replyTarget.id
commentData.parentId = this.replyTarget.parentId || this.replyTarget.id
}
await travelService.addComment(this.travelId, commentData)
uni.showToast({
title: '评论成功',
icon: 'success'
})
this.newComment = ''
this.cancelReply()
this.isFocus = false
// 重新加载评论
this.page = 1
this.loadComments()
} catch (error) {
uni.showToast({
title: error.message || '评论失败',
icon: 'none'
})
}
},
async likeComment(comment) {
try {
// 这里需要实现点赞评论的API
// await commentService.likeComment(comment.id)
comment.isLiked = !comment.isLiked
if (comment.isLiked) {
comment.likeCount = (comment.likeCount || 0) + 1
} else {
comment.likeCount = Math.max(0, (comment.likeCount || 1) - 1)
}
this.$forceUpdate()
} catch (error) {
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
},
replyComment(comment) {
this.replyTarget = comment
this.showReplyPanel = true
this.isFocus = true
},
cancelReply() {
this.replyTarget = null
this.showReplyPanel = false
},
onInputFocus() {
this.isFocus = true
},
onInputBlur() {
this.isFocus = false
},
getInputHeight() {
const query = uni.createSelectorQuery().in(this)
query.select('.comment-input-container').boundingClientRect(data => {
if (data) {
this.inputHeight = data.height
}
}).exec()
},
formatTime(time) {
if (!time) return ''
const now = new Date()
const commentTime = new Date(time)
const diff = now - commentTime
const minute = 60 * 1000
const hour = 60 * minute
const day = 24 * hour
if (diff < minute) {
return '刚刚'
} else if (diff < hour) {
return Math.floor(diff / minute) + '分钟前'
} else if (diff < day) {
return Math.floor(diff / hour) + '小时前'
} else if (diff < 7 * day) {
return Math.floor(diff / day) + '天前'
} else {
return commentTime.toLocaleDateString()
}
},
goBack() {
uni.navigateBack()
}
}
}
</script>
<style scoped>
.comments-page {
height: 100vh;
background: #f8f8f8;
display: flex;
flex-direction: column;
}
/* 头部导航 */
.header {
height: 88rpx;
background: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
border-bottom: 1rpx solid #eee;
position: relative;
z-index: 10;
}
.back-btn {
padding: 16rpx;
}
.title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.placeholder {
width: 48rpx;
height: 48rpx;
}
/* 评论列表 */
.comments-list {
flex: 1;
background: #fff;
padding: 0 32rpx;
}
.loading-container,
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.loading-text,
.empty-text {
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
}
.comment-item {
display: flex;
padding: 32rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.comment-item:last-child {
border-bottom: none;
}
.comment-avatar {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
margin-right: 20rpx;
flex-shrink: 0;
}
.comment-content {
flex: 1;
}
.comment-header {
display: flex;
align-items: center;
margin-bottom: 12rpx;
}
.comment-user {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-right: 16rpx;
}
.comment-time {
font-size: 24rpx;
color: #999;
}
.comment-text {
font-size: 28rpx;
color: #333;
line-height: 1.5;
margin-bottom: 20rpx;
}
.comment-actions {
display: flex;
gap: 32rpx;
}
.action-btn {
display: flex;
align-items: center;
padding: 8rpx 16rpx;
border-radius: 24rpx;
background: #f8f8f8;
}
.action-count {
font-size: 24rpx;
color: #666;
margin-left: 8rpx;
}
/* 回复列表 */
.replies-container {
margin-top: 20rpx;
padding: 20rpx;
background: #f8f8f8;
border-radius: 12rpx;
}
.reply-item {
margin-bottom: 12rpx;
}
.reply-item:last-child {
margin-bottom: 0;
}
.reply-user {
font-size: 26rpx;
font-weight: 600;
color: #007aff;
margin-right: 8rpx;
}
.reply-text {
font-size: 26rpx;
color: #333;
}
.reply-time {
font-size: 22rpx;
color: #999;
margin-left: 16rpx;
}
/* 加载更多 */
.load-more,
.no-more {
text-align: center;
padding: 32rpx 0;
color: #999;
font-size: 28rpx;
}
.load-more-text {
color: #999;
}
/* 底部输入框 */
.comment-input-container {
background: #fff;
padding: 20rpx 32rpx;
border-top: 1rpx solid #eee;
display: flex;
align-items: center;
position: relative;
z-index: 100;
}
.comment-input {
flex: 1;
height: 80rpx;
background: #f8f8f8;
border-radius: 40rpx;
padding: 0 32rpx;
font-size: 28rpx;
margin-right: 20rpx;
}
.send-btn {
background: #ccc;
color: #fff;
padding: 16rpx 32rpx;
border-radius: 32rpx;
font-size: 28rpx;
font-weight: 600;
transition: background 0.3s;
}
.send-btn.active {
background: #007aff;
}
/* 回复面板 */
.reply-panel {
position: fixed;
left: 0;
right: 0;
background: #fff;
padding: 16rpx 32rpx;
border-top: 1rpx solid #eee;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 90;
}
.reply-hint {
font-size: 28rpx;
color: #666;
}
.close-reply {
padding: 8rpx;
}
</style>

View File

@@ -1,96 +1,410 @@
<template>
<view class="container">
<!-- 封面图 -->
<image :src="plan.coverImage" mode="widthFix" class="cover-image"></image>
<!-- 计划标题和基本信息 -->
<view class="plan-header">
<text class="title">{{ plan.destination }}</text>
<view class="meta">
<text class="date">{{ plan.startDate }} - {{ plan.endDate }}</text>
<text class="budget">预算: ¥{{ plan.budget }}</text>
<view class="travel-detail-page">
<!-- 头部导航 -->
<view class="header">
<view class="back-btn" @click="goBack">
<uni-icons type="arrowleft" size="24" color="#333"></uni-icons>
</view>
<text class="title">旅行详情</text>
<view class="more-btn" @click="showActionSheet">
<uni-icons type="more" size="24" color="#333"></uni-icons>
</view>
</view>
<!-- 行程详情 -->
<view class="section">
<text class="section-title">行程安排</text>
<view class="schedule" v-for="(day, index) in plan.schedule" :key="index">
<text class="day">{{ index + 1 }}</text>
<text class="content">{{ day }}</text>
<!-- 内容区域 -->
<scroll-view class="content" scroll-y>
<!-- 封面图片 -->
<view class="cover-section">
<image :src="plan.coverImage" class="cover-image" mode="aspectFill"></image>
<view class="cover-overlay">
<text class="travel-title">{{ plan.title || plan.destination }}</text>
<text class="travel-destination">{{ plan.destination }}</text>
</view>
</view>
<!-- 基本信息 -->
<view class="info-section">
<view class="info-item">
<uni-icons type="calendar" size="20" color="#007aff"></uni-icons>
<text class="info-text">{{ plan.startDate }} - {{ plan.endDate }}</text>
</view>
<view class="info-item">
<uni-icons type="wallet" size="20" color="#007aff"></uni-icons>
<text class="info-text">预算: ¥{{ plan.budget }}</text>
</view>
<view class="info-item">
<uni-icons type="person" size="20" color="#007aff"></uni-icons>
<text class="info-text">{{ plan.currentMembers || 0 }}/{{ plan.maxMembers || 4 }}</text>
</view>
<view class="info-item">
<uni-icons type="flag" size="20" color="#007aff"></uni-icons>
<text class="info-text">{{ getStatusText(plan.status) }}</text>
</view>
</view>
<!-- 发布者信息 -->
<view class="publisher-section">
<view class="publisher-header">
<image :src="plan.publisher?.avatar || '/static/avatar/default.png'" class="avatar" mode="aspectFill"></image>
<view class="publisher-info">
<text class="nickname">{{ plan.publisher?.nickname || '匿名用户' }}</text>
<text class="level">Lv.{{ plan.publisher?.level || 1 }}</text>
</view>
<view class="rating">
<uni-rate :value="plan.publisher?.rating || 5" size="14" readonly></uni-rate>
<text class="rating-text">{{ plan.publisher?.rating || 5.0 }}</text>
</view>
</view>
</view>
<!-- 行程详情 -->
<view class="description-section">
<text class="section-title">行程安排</text>
<view class="schedule" v-for="(day, index) in plan.schedule" :key="index">
<text class="day">{{ index + 1 }}</text>
<text class="content">{{ day }}</text>
</view>
</view>
<!-- 同行要求 -->
<view class="description-section">
<text class="section-title">同行要求</text>
<text class="description">{{ plan.requirements || '无特殊要求' }}</text>
</view>
<!-- 兴趣标签 -->
<view class="tags-section" v-if="plan.tags && plan.tags.length > 0">
<text class="section-title">兴趣标签</text>
<view class="tags-container">
<view v-for="tag in plan.tags" :key="tag" class="tag">
<text>{{ tag }}</text>
</view>
</view>
</view>
<!-- 参与者列表 -->
<view class="members-section" v-if="plan.members && plan.members.length > 0">
<text class="section-title">参与者 ({{ plan.members.length }})</text>
<scroll-view class="members-list" scroll-x>
<view v-for="member in plan.members" :key="member.id" class="member-item">
<image :src="member.avatar || '/static/avatar/default.png'" class="member-avatar" mode="aspectFill"></image>
<text class="member-name">{{ member.nickname }}</text>
</view>
</scroll-view>
</view>
<!-- 评论区域 -->
<view class="comments-section">
<text class="section-title">评论 ({{ plan.commentCount || 0 }})</text>
<view class="comment-list">
<view v-for="comment in plan.comments" :key="comment.id" class="comment-item">
<image :src="comment.user.avatar || '/static/avatar/default.png'" class="comment-avatar" mode="aspectFill"></image>
<view class="comment-content">
<text class="comment-user">{{ comment.user.nickname }}</text>
<text class="comment-text">{{ comment.content }}</text>
<text class="comment-time">{{ comment.createTime }}</text>
</view>
</view>
</view>
<view class="view-all-comments" @click="viewAllComments" v-if="plan.commentCount > 2">
<text>查看全部{{ plan.commentCount }}条评论</text>
<uni-icons type="arrowright" size="16" color="#999"></uni-icons>
</view>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="footer">
<view class="action-btn" @click="navigateToChat">
<uni-icons type="chat" size="20" color="#666"></uni-icons>
<text>私信</text>
</view>
<view class="action-btn" @click="toggleLike" :class="{ liked: plan.isLiked }">
<uni-icons :type="plan.isLiked ? 'heart-filled' : 'heart'" size="20" :color="plan.isLiked ? '#ff4757' : '#666'"></uni-icons>
<text>{{ plan.likeCount || 0 }}</text>
</view>
<view class="action-btn" @click="focusCommentInput">
<uni-icons type="chatboxes" size="20" color="#666"></uni-icons>
<text>评论</text>
</view>
<view class="join-btn" @click="handleJoin" :class="{ joined: plan.isJoined }">
<text>{{ plan.isJoined ? '已加入' : '立即加入' }}</text>
</view>
</view>
<!-- 同行要求 -->
<view class="section">
<text class="section-title">同行要求</text>
<text class="requirements">{{ plan.requirements }}</text>
<!-- 评论输入框 -->
<view class="comment-input-container" v-if="showCommentInput">
<input
v-model="commentText"
class="comment-input"
placeholder="说点什么..."
@confirm="submitComment"
/>
<view class="send-btn" @click="submitComment">
<text>发送</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-bar">
<button class="btn join" @click="handleJoin">加入计划</button>
<button class="btn contact" @click="handleContact">联系发起人</button>
<!-- 操作菜单 -->
<uni-popup ref="actionSheet" type="bottom">
<view class="action-sheet">
<view class="action-item" @click="shareTravel">
<uni-icons type="share" size="20" color="#333"></uni-icons>
<text>分享</text>
</view>
<view class="action-item" @click="reportTravel" v-if="!isPublisher">
<uni-icons type="flag" size="20" color="#333"></uni-icons>
<text>举报</text>
</view>
<view class="action-item" @click="editTravel" v-if="isPublisher">
<uni-icons type="compose" size="20" color="#333"></uni-icons>
<text>编辑</text>
</view>
<view class="action-item" @click="deleteTravel" v-if="isPublisher">
<uni-icons type="trash" size="20" color="#ff4757"></uni-icons>
<text style="color: #ff4757;">删除</text>
</view>
<view class="cancel-btn" @click="closeActionSheet">
<text>取消</text>
</view>
</view>
</uni-popup>
<!-- 加载中 -->
<view v-if="loading" class="loading-mask">
<uni-loading size="32" color="#007aff"></uni-loading>
</view>
</view>
</template>
<script>
import { travelService } from '../../api/services.js'
import travelService from '@/api/services'
export default {
data() {
return {
plan: {
id: null,
title: '',
destination: '',
coverImage: '',
startDate: '',
endDate: '',
budget: 0,
coverImage: '',
schedule: [],
requirements: ''
}
requirements: '',
tags: [],
status: 1,
currentMembers: 0,
maxMembers: 4,
publisher: {},
members: [],
comments: [],
commentCount: 0,
likeCount: 0,
isLiked: false,
isJoined: false
},
planId: null,
loading: false,
showCommentInput: false,
commentText: '',
isPublisher: false
}
},
onLoad(options) {
const id = options.id
if (id) {
this.loadTravelDetail(id)
}
this.planId = options.id
this.loadPlanDetail()
},
methods: {
async loadTravelDetail(id) {
async loadPlanDetail() {
this.loading = true
try {
const data = await travelService.getDetail(id)
this.plan = data
const res = await travelService.getDetail(this.planId)
this.plan = { ...this.plan, ...res.data }
this.checkUserStatus()
} catch (error) {
console.error('获取旅行计划详情失败:', error)
uni.showToast({
title: '获取数据失败',
title: '加载失败',
icon: 'none'
})
} finally {
this.loading = false
}
},
checkUserStatus() {
// 检查当前用户是否是发布者或已加入
const userInfo = uni.getStorageSync('userInfo')
if (userInfo && userInfo.id) {
this.isPublisher = this.plan.publisher?.id === userInfo.id
this.plan.isJoined = this.plan.members?.some(member => member.id === userInfo.id)
}
},
goBack() {
uni.navigateBack()
},
showActionSheet() {
this.$refs.actionSheet.open()
},
closeActionSheet() {
this.$refs.actionSheet.close()
},
async handleJoin() {
if (this.plan.isJoined) {
uni.showToast({
title: '您已加入该计划',
icon: 'none'
})
return
}
if (this.plan.currentMembers >= this.plan.maxMembers) {
uni.showToast({
title: '该计划人数已满',
icon: 'none'
})
return
}
try {
await travelService.joinPlan(this.planId)
uni.showToast({
title: '加入成功',
icon: 'success'
})
this.plan.isJoined = true
this.plan.currentMembers += 1
} catch (error) {
uni.showToast({
title: error.message || '加入失败',
icon: 'none'
})
}
},
async handleJoin() {
navigateToChat() {
uni.navigateTo({
url: `/pages/chat/chat?id=${this.plan.publisher?.id}&name=${this.plan.publisher?.nickname}`
})
},
async toggleLike() {
try {
if (this.plan.isLiked) {
await travelService.unlikePlan(this.planId)
this.plan.likeCount -= 1
} else {
await travelService.likePlan(this.planId)
this.plan.likeCount += 1
}
this.plan.isLiked = !this.plan.isLiked
} catch (error) {
uni.showToast({
title: error.message || '操作失败',
icon: 'none'
})
}
},
focusCommentInput() {
this.showCommentInput = true
},
async submitComment() {
if (!this.commentText.trim()) {
uni.showToast({
title: '评论内容不能为空',
icon: 'none'
})
return
}
try {
await travelService.addComment(this.planId, {
content: this.commentText.trim()
})
uni.showToast({
title: '评论成功',
icon: 'success'
})
this.commentText = ''
this.showCommentInput = false
this.loadPlanDetail()
} catch (error) {
uni.showToast({
title: error.message || '评论失败',
icon: 'none'
})
}
},
viewAllComments() {
uni.navigateTo({
url: `/pages/travel/comments?id=${this.planId}`
})
},
shareTravel() {
uni.share({
provider: 'weixin',
scene: 'WXSceneSession',
type: 0,
href: `https://jiebanke.com/travel/${this.planId}`,
title: this.plan.title || this.plan.destination,
summary: this.plan.requirements || '一起来结伴旅行吧!',
imageUrl: this.plan.coverImage,
success: () => {
uni.showToast({
title: '分享成功',
icon: 'success'
})
}
})
},
reportTravel() {
uni.navigateTo({
url: `/pages/report/report?type=travel&id=${this.planId}`
})
},
editTravel() {
uni.navigateTo({
url: `/pages/travel/publish?id=${this.planId}`
})
},
async deleteTravel() {
uni.showModal({
title: '确认加入',
content: '确定要加入这个旅行计划吗?',
title: '确认删除',
content: '确定要删除这个旅行计划吗?此操作不可恢复。',
success: async (res) => {
if (res.confirm) {
try {
await travelService.join(this.plan.id)
await travelService.deletePlan(this.planId)
uni.showToast({
title: '申请已提交',
title: '删除成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (error) {
console.error('加入计划失败:', error)
uni.showToast({
title: '申请失败',
title: error.message || '删除失败',
icon: 'none'
})
}
@@ -99,100 +413,443 @@ export default {
})
},
handleContact() {
uni.makePhoneCall({
phoneNumber: '13800138000'
})
getStatusText(status) {
const statusMap = {
1: '招募中',
2: '进行中',
3: '已结束',
4: '已取消'
}
return statusMap[status] || '未知状态'
}
}
}
</script>
<style scoped>
.container {
padding-bottom: 100rpx;
.travel-detail-page {
height: 100vh;
background: #f8f8f8;
display: flex;
flex-direction: column;
}
.cover-image {
width: 100%;
/* 头部导航 */
.header {
height: 88rpx;
background: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
border-bottom: 1rpx solid #eee;
position: relative;
z-index: 10;
}
.plan-header {
padding: 30rpx;
.back-btn,
.more-btn {
padding: 16rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
font-weight: 600;
color: #333;
}
.meta {
display: flex;
justify-content: space-between;
margin-top: 20rpx;
color: #666;
/* 内容区域 */
.content {
flex: 1;
background: #fff;
}
.section {
padding: 30rpx;
/* 封面区域 */
.cover-section {
position: relative;
height: 400rpx;
}
.cover-image {
width: 100%;
height: 100%;
}
.cover-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
padding: 32rpx;
color: #fff;
}
.travel-title {
font-size: 40rpx;
font-weight: bold;
display: block;
margin-bottom: 8rpx;
}
.travel-destination {
font-size: 28rpx;
opacity: 0.9;
}
/* 信息区域 */
.info-section {
padding: 32rpx;
background: #fff;
margin-bottom: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.info-item {
display: flex;
align-items: center;
margin-bottom: 24rpx;
}
.info-item:last-child {
margin-bottom: 0;
}
.info-text {
font-size: 28rpx;
color: #333;
margin-left: 16rpx;
}
/* 发布者信息 */
.publisher-section {
padding: 32rpx;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.publisher-header {
display: flex;
align-items: center;
}
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: 20rpx;
}
.publisher-info {
flex: 1;
}
.nickname {
font-size: 32rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 4rpx;
}
.level {
font-size: 24rpx;
color: #999;
}
.rating {
display: flex;
align-items: center;
}
.rating-text {
font-size: 24rpx;
color: #ff9500;
margin-left: 8rpx;
}
/* 描述区域 */
.description-section {
padding: 32rpx;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
margin-bottom: 24rpx;
display: block;
}
.schedule {
margin-bottom: 20rpx;
padding-left: 20rpx;
border-left: 4rpx solid #007aff;
}
.day {
font-weight: bold;
font-size: 28rpx;
font-weight: 600;
color: #007aff;
margin-bottom: 10rpx;
display: block;
margin-bottom: 8rpx;
}
.content {
color: #666;
line-height: 1.5;
}
.requirements {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
.action-bar {
.description {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
/* 标签区域 */
.tags-section {
padding: 32rpx;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.tag {
background: #f0f6ff;
padding: 8rpx 20rpx;
border-radius: 24rpx;
font-size: 24rpx;
color: #007aff;
}
/* 参与者区域 */
.members-section {
padding: 32rpx;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.members-list {
display: flex;
gap: 24rpx;
padding: 16rpx 0;
}
.member-item {
display: flex;
flex-direction: column;
align-items: center;
min-width: 100rpx;
}
.member-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-bottom: 8rpx;
}
.member-name {
font-size: 24rpx;
color: #666;
text-align: center;
}
/* 评论区域 */
.comments-section {
padding: 32rpx;
background: #fff;
margin-bottom: 120rpx;
}
.comment-list {
margin-bottom: 24rpx;
}
.comment-item {
display: flex;
margin-bottom: 32rpx;
}
.comment-avatar {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
margin-right: 20rpx;
flex-shrink: 0;
}
.comment-content {
flex: 1;
}
.comment-user {
font-size: 28rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.comment-text {
font-size: 28rpx;
color: #333;
line-height: 1.5;
display: block;
margin-bottom: 8rpx;
}
.comment-time {
font-size: 24rpx;
color: #999;
}
.view-all-comments {
display: flex;
align-items: center;
justify-content: center;
padding: 24rpx;
color: #999;
font-size: 28rpx;
}
/* 底部操作栏 */
.footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background: #fff;
display: flex;
border-top: 1rpx solid #eee;
padding: 20rpx 32rpx;
display: flex;
align-items: center;
z-index: 100;
}
.btn {
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
margin-right: 40rpx;
padding: 16rpx;
}
.action-btn.liked {
color: #ff4757;
}
.action-btn text {
font-size: 20rpx;
color: #666;
margin-top: 4rpx;
}
.action-btn.liked text {
color: #ff4757;
}
.join-btn {
flex: 1;
border: none;
font-size: 32rpx;
}
.join {
background: #007aff;
color: #fff;
height: 80rpx;
border-radius: 40rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 600;
margin-left: auto;
}
.contact {
.join-btn.joined {
background: #34c759;
}
/* 评论输入框 */
.comment-input-container {
position: fixed;
bottom: 120rpx;
left: 0;
right: 0;
background: #fff;
padding: 20rpx 32rpx;
border-top: 1rpx solid #eee;
display: flex;
align-items: center;
z-index: 100;
}
.comment-input {
flex: 1;
height: 80rpx;
background: #f8f8f8;
border-radius: 40rpx;
padding: 0 32rpx;
font-size: 28rpx;
margin-right: 20rpx;
}
.send-btn {
background: #007aff;
color: #fff;
padding: 16rpx 32rpx;
border-radius: 32rpx;
font-size: 28rpx;
font-weight: 600;
}
/* 操作菜单 */
.action-sheet {
background: #fff;
border-radius: 24rpx 24rpx 0 0;
padding: 32rpx 0;
}
.action-item {
display: flex;
align-items: center;
padding: 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.action-item:last-child {
border-bottom: none;
}
.action-item text {
font-size: 32rpx;
margin-left: 20rpx;
}
.cancel-btn {
margin-top: 16rpx;
padding: 32rpx;
text-align: center;
border-top: 1rpx solid #f0f0f0;
font-size: 32rpx;
font-weight: 600;
}
/* 加载中 */
.loading-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
</style>

View File

@@ -0,0 +1,566 @@
<template>
<view class="publish-page">
<!-- 头部导航 -->
<view class="header">
<view class="back-btn" @click="goBack">
<uni-icons type="arrowleft" size="24" color="#333"></uni-icons>
</view>
<text class="title">发布旅行计划</text>
<view class="submit-btn" @click="submitForm">发布</view>
</view>
<!-- 表单内容 -->
<scroll-view class="form-content" scroll-y>
<!-- 封面图片 -->
<view class="form-section">
<text class="section-title">封面图片</text>
<view class="upload-area" @click="chooseImage">
<image v-if="formData.coverImage" :src="formData.coverImage" class="cover-image" mode="aspectFill"></image>
<view v-else class="upload-placeholder">
<uni-icons type="plus" size="32" color="#ccc"></uni-icons>
<text class="upload-text">添加封面图片</text>
</view>
</view>
</view>
<!-- 基本信息 -->
<view class="form-section">
<text class="section-title">基本信息</text>
<view class="form-item">
<text class="label">旅行标题</text>
<input
v-model="formData.title"
class="input"
placeholder="请输入旅行标题"
maxlength="30"
/>
</view>
<view class="form-item">
<text class="label">目的地</text>
<input
v-model="formData.destination"
class="input"
placeholder="请输入目的地"
@focus="showLocationPicker = true"
/>
</view>
<view class="form-item">
<text class="label">旅行时间</text>
<view class="date-range">
<view class="date-input" @click="showStartDatePicker = true">
<text>{{ formData.startDate || '开始日期' }}</text>
<uni-icons type="calendar" size="16" color="#999"></uni-icons>
</view>
<text class="date-separator"></text>
<view class="date-input" @click="showEndDatePicker = true">
<text>{{ formData.endDate || '结束日期' }}</text>
<uni-icons type="calendar" size="16" color="#999"></uni-icons>
</view>
</view>
</view>
<view class="form-item">
<text class="label">预算</text>
<view class="budget-input">
<text class="currency">¥</text>
<input
v-model="formData.budget"
class="input"
type="number"
placeholder="0"
/>
</view>
</view>
</view>
<!-- 行程详情 -->
<view class="form-section">
<text class="section-title">行程详情</text>
<view class="form-item">
<text class="label">行程描述</text>
<textarea
v-model="formData.description"
class="textarea"
placeholder="请详细描述您的旅行计划,包括行程安排、活动内容等"
maxlength="500"
/>
<text class="char-count">{{ formData.description?.length || 0 }}/500</text>
</view>
<view class="form-item">
<text class="label">人数限制</text>
<view class="member-limit">
<text>最多</text>
<input
v-model="formData.maxMembers"
class="number-input"
type="number"
placeholder="2"
/>
<text></text>
</view>
</view>
<view class="form-item">
<text class="label">兴趣标签</text>
<view class="tags-container">
<view
v-for="tag in interestTags"
:key="tag"
class="tag"
:class="{ active: formData.tags?.includes(tag) }"
@click="toggleTag(tag)"
>
<text>{{ tag }}</text>
</view>
</view>
</view>
</view>
<!-- 隐私设置 -->
<view class="form-section">
<text class="section-title">隐私设置</text>
<view class="form-item">
<view class="privacy-setting">
<text class="label">公开可见</text>
<switch :checked="formData.isPublic" @change="togglePrivacy" color="#007aff" />
</view>
<text class="privacy-tip">{{ formData.isPublic ? '所有人都可以看到您的旅行计划' : '仅自己可见' }}</text>
</view>
</view>
</scroll-view>
<!-- 日期选择器 -->
<uni-datetime-picker
v-if="showStartDatePicker"
type="date"
:value="formData.startDate"
@maskClick="showStartDatePicker = false"
@change="onStartDateChange"
/>
<uni-datetime-picker
v-if="showEndDatePicker"
type="date"
:value="formData.endDate"
@maskClick="showEndDatePicker = false"
@change="onEndDateChange"
/>
<!-- 加载中遮罩 -->
<view v-if="loading" class="loading-mask">
<uni-loading size="32" color="#007aff"></uni-loading>
<text>发布中...</text>
</view>
</view>
</template>
<script>
import { travelService } from '../../api/services.js'
export default {
data() {
return {
formData: {
title: '',
destination: '',
startDate: '',
endDate: '',
budget: '',
description: '',
maxMembers: 4,
coverImage: '',
tags: [],
isPublic: true
},
interestTags: [
'摄影', '美食', '徒步', '自驾', '露营',
'文化', '历史', '自然', '冒险', '休闲',
'购物', '海滩', '登山', '滑雪', '潜水'
],
showStartDatePicker: false,
showEndDatePicker: false,
loading: false
}
},
methods: {
// 选择封面图片
async chooseImage() {
try {
const res = await uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera']
})
if (res.tempFilePaths.length > 0) {
this.formData.coverImage = res.tempFilePaths[0]
}
} catch (error) {
console.error('选择图片失败:', error)
uni.showToast({
title: '选择图片失败',
icon: 'none'
})
}
},
// 切换兴趣标签
toggleTag(tag) {
const index = this.formData.tags.indexOf(tag)
if (index === -1) {
this.formData.tags.push(tag)
} else {
this.formData.tags.splice(index, 1)
}
},
// 切换隐私设置
togglePrivacy(e) {
this.formData.isPublic = e.detail.value
},
// 日期选择
onStartDateChange(e) {
this.formData.startDate = e
this.showStartDatePicker = false
},
onEndDateChange(e) {
this.formData.endDate = e
this.showEndDatePicker = false
},
// 表单验证
validateForm() {
if (!this.formData.title.trim()) {
uni.showToast({ title: '请输入旅行标题', icon: 'none' })
return false
}
if (!this.formData.destination.trim()) {
uni.showToast({ title: '请输入目的地', icon: 'none' })
return false
}
if (!this.formData.startDate) {
uni.showToast({ title: '请选择开始日期', icon: 'none' })
return false
}
if (!this.formData.endDate) {
uni.showToast({ title: '请选择结束日期', icon: 'none' })
return false
}
if (new Date(this.formData.startDate) > new Date(this.formData.endDate)) {
uni.showToast({ title: '结束日期不能早于开始日期', icon: 'none' })
return false
}
if (!this.formData.budget || Number(this.formData.budget) <= 0) {
uni.showToast({ title: '请输入合理的预算', icon: 'none' })
return false
}
if (!this.formData.description.trim()) {
uni.showToast({ title: '请输入行程描述', icon: 'none' })
return false
}
if (!this.formData.maxMembers || Number(this.formData.maxMembers) < 2) {
uni.showToast({ title: '人数限制至少2人', icon: 'none' })
return false
}
return true
},
// 提交表单
async submitForm() {
if (!this.validateForm()) return
this.loading = true
try {
const submitData = {
...this.formData,
budget: Number(this.formData.budget),
maxMembers: Number(this.formData.maxMembers)
}
await travelService.create(submitData)
uni.showToast({
title: '发布成功',
icon: 'success',
duration: 2000
})
// 发布成功后返回上一页
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (error) {
console.error('发布失败:', error)
uni.showToast({
title: '发布失败,请重试',
icon: 'none'
})
} finally {
this.loading = false
}
},
// 返回上一页
goBack() {
uni.navigateBack()
}
}
}
</script>
<style scoped>
.publish-page {
background-color: #f8f9fa;
min-height: 100vh;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
background-color: #fff;
border-bottom: 1rpx solid #eee;
position: sticky;
top: 0;
z-index: 100;
}
.back-btn {
padding: 10rpx;
}
.title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.submit-btn {
font-size: 28rpx;
color: #007aff;
font-weight: 500;
padding: 10rpx 20rpx;
}
.form-content {
height: calc(100vh - 120rpx);
padding: 20rpx 30rpx;
}
.form-section {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 30rpx;
display: block;
}
.upload-area {
height: 200rpx;
border: 2rpx dashed #ddd;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #fafafa;
}
.cover-image {
width: 100%;
height: 100%;
border-radius: 12rpx;
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.upload-text {
font-size: 24rpx;
color: #999;
margin-top: 10rpx;
}
.form-item {
margin-bottom: 30rpx;
}
.form-item:last-child {
margin-bottom: 0;
}
.label {
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-bottom: 15rpx;
display: block;
}
.input, .textarea {
background-color: #f8f9fa;
border-radius: 8rpx;
padding: 20rpx;
font-size: 28rpx;
border: 1rpx solid #eee;
}
.textarea {
height: 200rpx;
line-height: 1.5;
}
.char-count {
font-size: 24rpx;
color: #999;
text-align: right;
display: block;
margin-top: 10rpx;
}
.date-range {
display: flex;
align-items: center;
gap: 20rpx;
}
.date-input {
flex: 1;
background-color: #f8f9fa;
border-radius: 8rpx;
padding: 20rpx;
border: 1rpx solid #eee;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 28rpx;
color: #333;
}
.date-separator {
color: #999;
font-size: 28rpx;
}
.budget-input {
display: flex;
align-items: center;
background-color: #f8f9fa;
border-radius: 8rpx;
border: 1rpx solid #eee;
overflow: hidden;
}
.currency {
padding: 20rpx;
background-color: #f0f0f0;
color: #333;
font-size: 28rpx;
font-weight: 500;
}
.budget-input .input {
flex: 1;
border: none;
background: transparent;
}
.member-limit {
display: flex;
align-items: center;
gap: 10rpx;
}
.number-input {
width: 120rpx;
background-color: #f8f9fa;
border-radius: 8rpx;
padding: 20rpx;
border: 1rpx solid #eee;
text-align: center;
font-size: 28rpx;
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 15rpx;
margin-top: 15rpx;
}
.tag {
padding: 12rpx 24rpx;
background-color: #f8f9fa;
border-radius: 30rpx;
border: 1rpx solid #eee;
font-size: 24rpx;
color: #666;
}
.tag.active {
background-color: #007aff;
color: #fff;
border-color: #007aff;
}
.privacy-setting {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10rpx;
}
.privacy-tip {
font-size: 24rpx;
color: #999;
}
.loading-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
}
.loading-mask text {
color: #fff;
margin-top: 20rpx;
font-size: 28rpx;
}
</style>

View File

@@ -1 +1 @@
{"version":3,"file":"config.js","sources":["api/config.js"],"sourcesContent":["// API基础配置\r\nconst config = {\r\n // 开发环境\r\n development: {\r\n baseURL: 'http://localhost:3100/api',\r\n timeout: 10000\r\n },\r\n // 生产环境\r\n production: {\r\n baseURL: 'https://api.jiebanke.com/api',\r\n timeout: 15000\r\n }\r\n}\r\n\r\n// 获取当前环境配置\r\nconst getConfig = () => {\r\n const env = process.env.NODE_ENV || 'development'\r\n return config[env]\r\n}\r\n\r\n// API端点\r\nconst endpoints = {\r\n // 用户相关\r\n USER: {\r\n LOGIN: '/auth/login',\r\n REGISTER: '/auth/register',\r\n PROFILE: '/user/profile',\r\n UPDATE_PROFILE: '/user/profile',\r\n UPLOAD_AVATAR: '/user/avatar'\r\n },\r\n \r\n // 旅行计划\r\n TRAVEL: {\r\n LIST: '/travel/list',\r\n DETAIL: '/travel/detail',\r\n CREATE: '/travel/create',\r\n JOIN: '/travel/join',\r\n MY_PLANS: '/travel/my-plans',\r\n SEARCH: '/travel/search'\r\n },\r\n \r\n // 动物认养\r\n ANIMAL: {\r\n LIST: '/animal/list',\r\n DETAIL: '/animal/detail',\r\n ADOPT: '/animal/adopt',\r\n MY_ANIMALS: '/animal/my-animals',\r\n CATEGORIES: '/animal/categories'\r\n },\r\n \r\n // 送花服务\r\n FLOWER: {\r\n LIST: '/flower/list',\r\n DETAIL: '/flower/detail',\r\n ORDER: '/flower/order',\r\n MY_ORDERS: '/flower/my-orders',\r\n CATEGORIES: '/flower/categories'\r\n },\r\n \r\n // 订单管理\r\n ORDER: {\r\n LIST: '/order/list',\r\n DETAIL: '/order/detail',\r\n CANCEL: '/order/cancel',\r\n PAY: '/order/pay',\r\n CONFIRM: '/order/confirm'\r\n },\r\n \r\n // 支付相关\r\n PAYMENT: {\r\n CREATE: '/payment/create',\r\n QUERY: '/payment/query',\r\n REFUND: '/payment/refund'\r\n },\r\n \r\n // 系统相关\r\n SYSTEM: {\r\n CONFIG: '/system/config',\r\n NOTICE: '/system/notice',\r\n FEEDBACK: '/system/feedback'\r\n }\r\n}\r\n\r\nexport default {\r\n ...getConfig(),\r\n endpoints\r\n}"],"names":[],"mappings":";AACA,MAAM,SAAS;AAAA;AAAA,EAEb,aAAa;AAAA,IACX,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AAAA;AAAA,EAEA,YAAY;AAAA,IACV,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AACF;AAGA,MAAM,YAAY,MAAM;AACtB,QAAM,MAAM;AACZ,SAAO,OAAO,GAAG;AACnB;AAGA,MAAM,YAAY;AAAA;AAAA,EAEhB,MAAM;AAAA,IACJ,OAAO;AAAA,IACP,UAAU;AAAA,IACV,SAAS;AAAA,IACT,gBAAgB;AAAA,IAChB,eAAe;AAAA,EACjB;AAAA;AAAA,EAGA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,UAAU;AAAA,IACV,QAAQ;AAAA,EACV;AAAA;AAAA,EAGA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,YAAY;AAAA,EACd;AAAA;AAAA,EAGA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,WAAW;AAAA,IACX,YAAY;AAAA,EACd;AAAA;AAAA,EAGA,OAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,KAAK;AAAA,IACL,SAAS;AAAA,EACX;AAAA;AAAA,EAGA,SAAS;AAAA,IACP,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,QAAQ;AAAA,EACV;AAAA;AAAA,EAGA,QAAQ;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ;AACF;AAEA,MAAe,WAAA;AAAA,EACb,GAAG,UAAU;AAAA,EACb;AACF;;"}
{"version":3,"file":"config.js","sources":["api/config.js"],"sourcesContent":["// API基础配置\nconst config = {\n // 开发环境\n development: {\n baseURL: 'http://localhost:3100/api',\n timeout: 10000\n },\n // 生产环境\n production: {\n baseURL: 'https://api.jiebanke.com/api',\n timeout: 15000\n }\n}\n\n// 获取当前环境配置\nconst getConfig = () => {\n const env = process.env.NODE_ENV || 'development'\n return config[env]\n}\n\n// API端点\nconst endpoints = {\n // 用户相关\n USER: {\n LOGIN: '/auth/login',\n REGISTER: '/auth/register',\n PROFILE: '/user/profile',\n UPDATE_PROFILE: '/user/profile',\n UPLOAD_AVATAR: '/user/avatar'\n },\n \n // 旅行计划\n TRAVEL: {\n LIST: '/travel/list',\n DETAIL: '/travel/detail',\n CREATE: '/travel/create',\n UPDATE: '/travel/update',\n DELETE: '/travel/delete',\n JOIN: '/travel/join',\n QUIT: '/travel/quit',\n LIKE: '/travel/like',\n UNLIKE: '/travel/unlike',\n COMMENT: '/travel/comment',\n COMMENTS: '/travel/comments',\n MY_PLANS: '/travel/my-plans',\n SEARCH: '/travel/search'\n },\n \n // 动物认养\n ANIMAL: {\n LIST: '/animal/list',\n DETAIL: '/animal/detail',\n ADOPT: '/animal/adopt',\n MY_ANIMALS: '/animal/my-animals',\n CATEGORIES: '/animal/categories'\n },\n \n // 送花服务\n FLOWER: {\n LIST: '/flower/list',\n DETAIL: '/flower/detail',\n ORDER: '/flower/order',\n MY_ORDERS: '/flower/my-orders',\n CATEGORIES: '/flower/categories'\n },\n \n // 订单管理\n ORDER: {\n LIST: '/order/list',\n DETAIL: '/order/detail',\n CANCEL: '/order/cancel',\n PAY: '/order/pay',\n CONFIRM: '/order/confirm'\n },\n \n // 支付相关\n PAYMENT: {\n CREATE: '/payment/create',\n QUERY: '/payment/query',\n REFUND: '/payment/refund'\n },\n \n // 系统相关\n SYSTEM: {\n CONFIG: '/system/config',\n NOTICE: '/system/notice',\n FEEDBACK: '/system/feedback'\n },\n\n // 搜索相关\n SEARCH: {\n GLOBAL: '/search/global',\n SUGGESTIONS: '/search/suggestions',\n TRAVEL: '/search/travel',\n ANIMAL: '/search/animal',\n FLOWER: '/search/flower',\n USER: '/search/user'\n },\n\n // 推广相关\n PROMOTION: {\n DATA: '/promotion/data',\n RECORDS: '/promotion/records',\n ALL_RECORDS: '/promotion/all-records',\n QRCODE: '/promotion/qrcode',\n REWARD_DETAILS: '/promotion/reward-details',\n WITHDRAW: '/promotion/withdraw',\n WITHDRAW_RECORDS: '/promotion/withdraw-records'\n },\n\n // 认证相关\n AUTH: {\n PHONE_LOGIN: '/auth/phone-login',\n WECHAT_LOGIN: '/auth/wechat-login',\n PASSWORD_LOGIN: '/auth/password-login',\n SEND_SMS_CODE: '/auth/send-sms-code',\n CHECK_TOKEN: '/auth/check-token',\n REFRESH_TOKEN: '/auth/refresh-token',\n BIND_PHONE: '/auth/bind-phone',\n CHANGE_PASSWORD: '/auth/change-password',\n RESET_PASSWORD: '/auth/reset-password'\n }\n}\n\nexport default {\n ...getConfig(),\n endpoints\n}"],"names":[],"mappings":";AACA,MAAM,SAAS;AAAA;AAAA,EAEb,aAAa;AAAA,IACX,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AAAA;AAAA,EAEA,YAAY;AAAA,IACV,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AACF;AAGA,MAAM,YAAY,MAAM;AACtB,QAAM,MAAM;AACZ,SAAO,OAAO,GAAG;AACnB;AAGA,MAAM,YAAY;AAAA;AAAA,EAEhB,MAAM;AAAA,IACJ,OAAO;AAAA,IACP,UAAU;AAAA,IACV,SAAS;AAAA,IACT,gBAAgB;AAAA,IAChB,eAAe;AAAA,EACjB;AAAA;AAAA,EAGA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,IACV,UAAU;AAAA,IACV,QAAQ;AAAA,EACV;AAAA;AAAA,EAGA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,YAAY;AAAA,EACd;AAAA;AAAA,EAGA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,WAAW;AAAA,IACX,YAAY;AAAA,EACd;AAAA;AAAA,EAGA,OAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,KAAK;AAAA,IACL,SAAS;AAAA,EACX;AAAA;AAAA,EAGA,SAAS;AAAA,IACP,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,QAAQ;AAAA,EACV;AAAA;AAAA,EAGA,QAAQ;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,UAAU;AAAA,EACZ;AAAA;AAAA,EAGA,QAAQ;AAAA,IACN,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,EACR;AAAA;AAAA,EAGA,WAAW;AAAA,IACT,MAAM;AAAA,IACN,SAAS;AAAA,IACT,aAAa;AAAA,IACb,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,UAAU;AAAA,IACV,kBAAkB;AAAA,EACpB;AAAA;AAAA,EAGA,MAAM;AAAA,IACJ,aAAa;AAAA,IACb,cAAc;AAAA,IACd,gBAAgB;AAAA,IAChB,eAAe;AAAA,IACf,aAAa;AAAA,IACb,eAAe;AAAA,IACf,YAAY;AAAA,IACZ,iBAAiB;AAAA,IACjB,gBAAgB;AAAA,EAClB;AACF;AAEA,MAAe,WAAA;AAAA,EACb,GAAG,UAAU;AAAA,EACb;AACF;;"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -29,7 +29,14 @@ const endpoints = {
LIST: "/travel/list",
DETAIL: "/travel/detail",
CREATE: "/travel/create",
UPDATE: "/travel/update",
DELETE: "/travel/delete",
JOIN: "/travel/join",
QUIT: "/travel/quit",
LIKE: "/travel/like",
UNLIKE: "/travel/unlike",
COMMENT: "/travel/comment",
COMMENTS: "/travel/comments",
MY_PLANS: "/travel/my-plans",
SEARCH: "/travel/search"
},
@@ -68,6 +75,37 @@ const endpoints = {
CONFIG: "/system/config",
NOTICE: "/system/notice",
FEEDBACK: "/system/feedback"
},
// 搜索相关
SEARCH: {
GLOBAL: "/search/global",
SUGGESTIONS: "/search/suggestions",
TRAVEL: "/search/travel",
ANIMAL: "/search/animal",
FLOWER: "/search/flower",
USER: "/search/user"
},
// 推广相关
PROMOTION: {
DATA: "/promotion/data",
RECORDS: "/promotion/records",
ALL_RECORDS: "/promotion/all-records",
QRCODE: "/promotion/qrcode",
REWARD_DETAILS: "/promotion/reward-details",
WITHDRAW: "/promotion/withdraw",
WITHDRAW_RECORDS: "/promotion/withdraw-records"
},
// 认证相关
AUTH: {
PHONE_LOGIN: "/auth/phone-login",
WECHAT_LOGIN: "/auth/wechat-login",
PASSWORD_LOGIN: "/auth/password-login",
SEND_SMS_CODE: "/auth/send-sms-code",
CHECK_TOKEN: "/auth/check-token",
REFRESH_TOKEN: "/auth/refresh-token",
BIND_PHONE: "/auth/bind-phone",
CHANGE_PASSWORD: "/auth/change-password",
RESET_PASSWORD: "/auth/reset-password"
}
};
const config$1 = {

View File

@@ -1,7 +1,106 @@
"use strict";
require("../common/vendor.js");
const common_vendor = require("../common/vendor.js");
const api_request = require("./request.js");
require("./config.js");
const api_config = require("./config.js");
const { endpoints } = api_config.config;
const userService = {
// 登录
login: (data) => api_request.request.post(endpoints.USER.LOGIN, data),
// 注册
register: (data) => api_request.request.post(endpoints.USER.REGISTER, data),
// 获取用户信息
getProfile: () => api_request.request.get(endpoints.USER.PROFILE),
// 更新用户信息
updateProfile: (data) => api_request.request.put(endpoints.USER.UPDATE_PROFILE, data),
// 上传头像
uploadAvatar: (filePath) => api_request.request.upload(endpoints.USER.UPLOAD_AVATAR, filePath),
// 退出登录
logout: () => {
common_vendor.index.removeStorageSync("token");
common_vendor.index.removeStorageSync("refreshToken");
return Promise.resolve();
}
};
const travelService = {
// 获取旅行计划列表
getList: (params = {}) => api_request.request.get(endpoints.TRAVEL.LIST, params),
// 获取旅行计划详情
getDetail: (id) => api_request.request.get(`${endpoints.TRAVEL.DETAIL}/${id}`),
// 创建旅行计划
create: (data) => api_request.request.post(endpoints.TRAVEL.CREATE, data),
// 更新旅行计划
update: (id, data) => api_request.request.put(`${endpoints.TRAVEL.UPDATE}/${id}`, data),
// 删除旅行计划
deletePlan: (id) => api_request.request.delete(`${endpoints.TRAVEL.DELETE}/${id}`),
// 加入旅行计划
joinPlan: (travelId) => api_request.request.post(`${endpoints.TRAVEL.JOIN}/${travelId}`),
// 退出旅行计划
quitPlan: (travelId) => api_request.request.post(`${endpoints.TRAVEL.QUIT}/${travelId}`),
// 点赞旅行计划
likePlan: (travelId) => api_request.request.post(`${endpoints.TRAVEL.LIKE}/${travelId}`),
// 取消点赞旅行计划
unlikePlan: (travelId) => api_request.request.post(`${endpoints.TRAVEL.UNLIKE}/${travelId}`),
// 添加评论
addComment: (travelId, data) => api_request.request.post(`${endpoints.TRAVEL.COMMENT}/${travelId}`, data),
// 获取评论列表
getComments: (travelId, params = {}) => api_request.request.get(`${endpoints.TRAVEL.COMMENTS}/${travelId}`, params),
// 获取我的旅行计划
getMyPlans: (params = {}) => api_request.request.get(endpoints.TRAVEL.MY_PLANS, params),
// 搜索旅行计划
search: (keyword, params = {}) => api_request.request.get(endpoints.TRAVEL.SEARCH, { keyword, ...params })
};
const animalService = {
// 获取动物列表
getList: (params = {}) => api_request.request.get(endpoints.ANIMAL.LIST, params),
// 获取动物详情
getDetail: (id) => api_request.request.get(`${endpoints.ANIMAL.DETAIL}/${id}`),
// 认养动物
adopt: (animalId, data) => api_request.request.post(`${endpoints.ANIMAL.ADOPT}/${animalId}`, data),
// 获取我的动物
getMyAnimals: (params = {}) => api_request.request.get(endpoints.ANIMAL.MY_ANIMALS, params),
// 获取动物分类
getCategories: () => api_request.request.get(endpoints.ANIMAL.CATEGORIES)
};
const flowerService = {
// 获取花束列表
getList: (params = {}) => api_request.request.get(endpoints.FLOWER.LIST, params),
// 获取花束详情
getDetail: (id) => api_request.request.get(`${endpoints.FLOWER.DETAIL}/${id}`),
// 下单
order: (data) => api_request.request.post(endpoints.FLOWER.ORDER, data),
// 获取我的订单
getMyOrders: (params = {}) => api_request.request.get(endpoints.FLOWER.MY_ORDERS, params),
// 获取花束分类
getCategories: () => api_request.request.get(endpoints.FLOWER.CATEGORIES)
};
const orderService = {
// 获取订单列表
getList: (params = {}) => api_request.request.get(endpoints.ORDER.LIST, params),
// 获取订单详情
getDetail: (id) => api_request.request.get(`${endpoints.ORDER.DETAIL}/${id}`),
// 取消订单
cancel: (id) => api_request.request.post(`${endpoints.ORDER.CANCEL}/${id}`),
// 支付订单
pay: (id) => api_request.request.post(`${endpoints.ORDER.PAY}/${id}`),
// 确认收货
confirm: (id) => api_request.request.post(`${endpoints.ORDER.CONFIRM}/${id}`)
};
const paymentService = {
// 创建支付
create: (data) => api_request.request.post(endpoints.PAYMENT.CREATE, data),
// 查询支付状态
query: (paymentId) => api_request.request.get(`${endpoints.PAYMENT.QUERY}/${paymentId}`),
// 退款
refund: (paymentId, data) => api_request.request.post(`${endpoints.PAYMENT.REFUND}/${paymentId}`, data)
};
const systemService = {
// 获取系统配置
getConfig: () => api_request.request.get(endpoints.SYSTEM.CONFIG),
// 获取公告列表
getNotices: (params = {}) => api_request.request.get(endpoints.SYSTEM.NOTICE, params),
// 提交反馈
submitFeedback: (data) => api_request.request.post(endpoints.SYSTEM.FEEDBACK, data)
};
const homeService = {
// 获取首页数据
getHomeData: () => api_request.request.get("/home/data"),
@@ -14,5 +113,101 @@ const homeService = {
// 获取精选花束
getFeaturedFlowers: () => api_request.request.get("/home/featured-flowers")
};
const searchService = {
// 全局搜索
search: (params = {}) => api_request.request.get(endpoints.SEARCH.GLOBAL, params),
// 获取搜索建议
getSuggestions: (keyword) => api_request.request.get(endpoints.SEARCH.SUGGESTIONS, { keyword }),
// 旅行计划搜索
searchTravel: (params = {}) => api_request.request.get(endpoints.SEARCH.TRAVEL, params),
// 动物搜索
searchAnimal: (params = {}) => api_request.request.get(endpoints.SEARCH.ANIMAL, params),
// 花束搜索
searchFlower: (params = {}) => api_request.request.get(endpoints.SEARCH.FLOWER, params),
// 用户搜索
searchUser: (params = {}) => api_request.request.get(endpoints.SEARCH.USER, params)
};
const promotionService = {
// 获取推广数据
getPromotionData: () => api_request.request.get(endpoints.PROMOTION.DATA),
// 获取邀请记录
getRecentRecords: (params = {}) => api_request.request.get(endpoints.PROMOTION.RECORDS, params),
// 获取所有邀请记录
getAllRecords: (params = {}) => api_request.request.get(endpoints.PROMOTION.ALL_RECORDS, params),
// 生成邀请二维码
generateQRCode: () => api_request.request.get(endpoints.PROMOTION.QRCODE),
// 获取奖励明细
getRewardDetails: (params = {}) => api_request.request.get(endpoints.PROMOTION.REWARD_DETAILS, params),
// 提现申请
applyWithdraw: (data) => api_request.request.post(endpoints.PROMOTION.WITHDRAW, data),
// 获取提现记录
getWithdrawRecords: (params = {}) => api_request.request.get(endpoints.PROMOTION.WITHDRAW_RECORDS, params)
};
const authService = {
// 手机号登录
phoneLogin: (data) => api_request.request.post(endpoints.AUTH.PHONE_LOGIN, data),
// 微信登录
wechatLogin: (data) => api_request.request.post(endpoints.AUTH.WECHAT_LOGIN, data),
// 密码登录
passwordLogin: (data) => api_request.request.post(endpoints.AUTH.PASSWORD_LOGIN, data),
// 发送短信验证码
sendSmsCode: (phone) => api_request.request.post(endpoints.AUTH.SEND_SMS_CODE, { phone }),
// 验证token
checkToken: (token) => api_request.request.post(endpoints.AUTH.CHECK_TOKEN, { token }),
// 刷新token
refreshToken: (refreshToken) => api_request.request.post(endpoints.AUTH.REFRESH_TOKEN, { refreshToken }),
// 绑定手机号
bindPhone: (data) => api_request.request.post(endpoints.AUTH.BIND_PHONE, data),
// 修改密码
changePassword: (data) => api_request.request.post(endpoints.AUTH.CHANGE_PASSWORD, data),
// 重置密码
resetPassword: (data) => api_request.request.post(endpoints.AUTH.RESET_PASSWORD, data)
};
const apiUtils = {
// 生成分页参数
generatePagination: (page = 1, pageSize = 10) => ({
page,
pageSize,
skip: (page - 1) * pageSize
}),
// 处理上传进度
handleUploadProgress: (progressEvent) => {
const percent = Math.round(progressEvent.loaded * 100 / progressEvent.total);
return percent;
},
// 处理下载进度
handleDownloadProgress: (progressEvent) => {
const percent = Math.round(progressEvent.loaded * 100 / progressEvent.total);
return percent;
},
// 格式化错误信息
formatError: (error) => {
if (error.code) {
return error.message;
}
return "网络连接失败,请检查网络设置";
}
};
const travelService$1 = {
userService,
travelService,
animalService,
flowerService,
orderService,
paymentService,
systemService,
homeService,
authService,
searchService,
promotionService,
apiUtils
};
exports.animalService = animalService;
exports.authService = authService;
exports.flowerService = flowerService;
exports.homeService = homeService;
exports.promotionService = promotionService;
exports.searchService = searchService;
exports.travelService = travelService;
exports.travelService$1 = travelService$1;
//# sourceMappingURL=../../.sourcemap/mp-weixin/api/services.js.map