123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779 |
- <template>
- <view class="plan-detail">
- <!-- 顶部导航栏 -->
- <view class="navbar">
- <view class="back-btn" @tap="goBack">
- <text class="back-icon">←</text>
- <text class="back-text">返回</text>
- </view>
- <view class="title">{{editMode ? '修改行程' : '定制行程'}}</view>
- </view>
-
- <view class="container">
- <view class="header">
- <view class="title">自定义行程</view>
- <view class="subtitle">{{selectedLocations.length}}个景点 · {{editMode ? '请修改行程信息' : '请完善行程信息'}}</view>
- </view>
-
- <!-- 表单区域 -->
- <view class="form-section">
- <!-- 行程天数 -->
- <view class="form-item">
- <view class="form-label">行程天数</view>
- <view class="days-selector">
- <view class="days-btn minus" @tap="decreaseDays" :class="{'disabled': tripDays <= 1}">-</view>
- <view class="days-value">{{tripDays}}天</view>
- <view class="days-btn plus" @tap="increaseDays">+</view>
- </view>
- <view class="form-hint">建议天数: {{recommendedDays}}天</view>
- </view>
-
- <!-- 预算金额 -->
- <view class="form-item">
- <view class="form-label">预算金额</view>
- <view class="budget-input">
- <text class="currency">¥</text>
- <input type="number" v-model="budget" placeholder="请输入预算金额" />
- </view>
- <view class="form-hint">人均预计花费</view>
- </view>
-
- <!-- 出发日期 -->
- <view class="form-item">
- <view class="form-label">出发日期</view>
- <view class="date-picker">
- <picker mode="date" :value="startDate" @change="onDateChange">
- <view class="picker-value">{{formatDate(startDate)}}</view>
- </picker>
- <text class="picker-icon">▼</text>
- </view>
- </view>
-
- <!-- 出行人数 -->
- <view class="form-item">
- <view class="form-label">出行人数</view>
- <view class="people-selector">
- <view class="people-btn minus" @tap="decreasePeople" :class="{'disabled': peopleCount <= 1}">-</view>
- <view class="people-value">{{peopleCount}}人</view>
- <view class="people-btn plus" @tap="increasePeople">+</view>
- </view>
- </view>
- </view>
-
- <!-- 已选景点列表 -->
- <view class="spots-section">
- <view class="section-title">已选景点</view>
- <view class="spots-list">
- <view class="spot-item" v-for="(spot, index) in selectedLocations" :key="index">
- <view class="spot-index">{{index + 1}}</view>
- <view class="spot-info">
- <view class="spot-name">{{spot.name || '未命名景点'}}</view>
- <view class="spot-address">{{spot.address || '无地址信息'}}</view>
- </view>
- <view class="remove-btn" @tap="removeSpot(index)">×</view>
- </view>
- </view>
-
- <!-- 空状态提示 -->
- <view class="empty-state" v-if="selectedLocations.length === 0">
- <view class="empty-icon">🗺️</view>
- <view class="empty-text">未选择任何景点,请返回地图选择</view>
- </view>
- </view>
-
- <!-- 底部操作按钮 -->
- <view class="action-buttons">
- <view class="back-btn" @tap="goBack">返回{{editMode ? '' : '修改'}}</view>
- <view class="submit-btn" @tap="createTrip" :class="{'disabled': !canSubmit}">{{editMode ? '保存修改' : '生成行程'}}</view>
- </view>
- </view>
- </view>
- </template>
- <script>
- import { API } from '@/util/api.js';
-
- export default {
- data() {
- return {
- selectedLocations: [],
- tripDays: 1,
- budget: '',
- startDate: new Date().toISOString().split('T')[0], // 今天的日期,格式:YYYY-MM-DD
- peopleCount: 2,
- options: {},
- editMode: false,
- tripId: null
- }
- },
- computed: {
- // 计算推荐天数
- recommendedDays() {
- return Math.max(1, Math.ceil(this.selectedLocations.length / 3));
- },
-
- // 检查是否可以提交
- canSubmit() {
- return this.selectedLocations.length > 0 &&
- this.tripDays >= 1 &&
- this.budget &&
- this.startDate;
- }
- },
- onLoad(options) {
- console.log('规划详情页参数:', options);
- this.options = options || {};
- this.editMode = options && options.tripId ? true : false;
- this.tripId = options && options.tripId ? options.tripId : null;
-
- // 从URL参数或本地存储获取已选景点数据
- try {
- const locationsData = uni.getStorageSync('selectedLocations');
- if (locationsData) {
- this.selectedLocations = JSON.parse(locationsData);
- console.log('已加载选择的景点数据:', this.selectedLocations);
-
- // 如果是编辑模式,尝试加载现有行程数据
- if (this.editMode && this.tripId) {
- this.loadExistingTripData();
- } else {
- // 根据景点数量设置推荐天数
- this.tripDays = this.recommendedDays;
- }
- } else {
- console.log('未找到已选景点数据');
- // 如果没有数据,返回地图页面
- uni.showToast({
- title: '请先选择景点',
- icon: 'none'
- });
- setTimeout(() => {
- uni.navigateBack();
- }, 1500);
- }
- } catch (e) {
- console.error('读取已选景点数据失败:', e);
- }
- },
- methods: {
- // 加载现有行程数据
- loadExistingTripData() {
- try {
- // 获取所有已保存的行程
- const savedTrips = uni.getStorageSync('savedTrips') || [];
- // 查找当前行程ID对应的行程
- const tripData = savedTrips.find(trip => trip.id === this.tripId);
-
- if (tripData) {
- console.log('找到现有行程数据:', tripData);
- // 设置表单数据
- this.tripDays = tripData.days || 1;
- this.budget = tripData.budget ? tripData.budget.toString() : '';
- this.startDate = tripData.startDate;
- this.peopleCount = tripData.peopleCount || 2;
-
- // 更新页面标题
- uni.setNavigationBarTitle({
- title: '修改行程'
- });
- } else {
- console.log('未找到现有行程数据');
- this.tripDays = this.recommendedDays;
- }
- } catch (e) {
- console.error('加载现有行程数据失败:', e);
- this.tripDays = this.recommendedDays;
- }
- },
-
- // 增加天数
- increaseDays() {
- this.tripDays++;
- },
-
- // 减少天数
- decreaseDays() {
- if (this.tripDays > 1) {
- this.tripDays--;
- }
- },
-
- // 增加人数
- increasePeople() {
- this.peopleCount++;
- },
-
- // 减少人数
- decreasePeople() {
- if (this.peopleCount > 1) {
- this.peopleCount--;
- }
- },
-
- // 格式化日期显示
- formatDate(dateString) {
- if (!dateString) return '请选择日期';
- const date = new Date(dateString);
- return `${date.getFullYear()}.${date.getMonth() + 1}.${date.getDate()}`;
- },
-
- // 日期选择变化处理
- onDateChange(e) {
- this.startDate = e.detail.value;
- },
-
- // 移除景点
- removeSpot(index) {
- uni.showModal({
- title: '移除景点',
- content: `确定要移除"${this.selectedLocations[index].name}"吗?`,
- success: (res) => {
- if (res.confirm) {
- this.selectedLocations.splice(index, 1);
- // 更新本地存储
- this.updateLocalStorage();
- }
- }
- });
- },
-
- // 更新本地存储
- updateLocalStorage() {
- try {
- uni.setStorageSync('selectedLocations', JSON.stringify(this.selectedLocations));
- console.log('已更新本地存储的景点数据');
- } catch (e) {
- console.error('更新本地存储失败:', e);
- }
- },
-
- // 创建行程
- createTrip() {
- if (!this.canSubmit) {
- uni.showToast({
- title: '请完善行程信息',
- icon: 'none'
- });
- return;
- }
-
- // 显示加载提示
- uni.showLoading({
- title: this.editMode ? '保存更改中...' : '行程生成中...'
- });
-
- // 为景点添加默认封面图片
- const spotsWithCoverImage = this.selectedLocations.map((location, index) => {
- let coverImage = '/static/baoding.jpg';
-
- // 根据景点名称设置不同的封面图
- if (location.name && location.name.includes('古莲花池')) {
- coverImage = '/static/baoding.jpg';
- } else if (location.name && location.name.includes('直隶')) {
- coverImage = '/static/xian.jpg';
- } else if (location.name && location.name.includes('野三坡')) {
- coverImage = '/static/beijing.jpg';
- } else {
- // 根据景点ID或索引设置不同的封面图
- const images = [
- '/static/baoding.jpg',
- '/static/beijing.jpg',
- '/static/shanghai.jpg',
- '/static/chengdu.jpg'
- ];
- const imageIndex = index % images.length;
- coverImage = images[imageIndex];
- }
-
- // 计算与上一个景点的距离
- let distanceFromPrevious = null;
- if (index > 0) {
- const prevLocation = this.selectedLocations[index - 1];
- distanceFromPrevious = this.calculateDistance(
- prevLocation.latitude,
- prevLocation.longitude,
- location.latitude,
- location.longitude
- );
- }
-
- // 确保所有数值类型的字段都是数字
- const latitude = Number(location.latitude);
- const longitude = Number(location.longitude);
-
- console.log(`景点${index+1} - ${location.name} 经纬度:`, latitude, longitude);
-
- return {
- id: location.id || `spot_${Date.now()}_${index}`, // 确保有唯一ID
- name: location.name || '未命名景点',
- address: location.address || '无地址信息',
- latitude: latitude,
- longitude: longitude,
- order: index + 1,
- coverImage: coverImage, // 添加封面图片
- distanceFromPrevious: distanceFromPrevious // 添加距离信息
- };
- });
-
- // 准备行程数据
- const tripData = {
- name: '保定自定义行程',
- days: this.tripDays,
- budget: this.budget ? Number(this.budget) : null,
- startDate: this.startDate,
- peopleCount: this.peopleCount,
- spots: spotsWithCoverImage,
- createdAt: new Date().toISOString(),
- updatedAt: new Date().toISOString()
- };
-
- // 如果是编辑模式,添加ID
- if (this.editMode && this.tripId) {
- // 检查ID是否是数字字符串
- if (this.tripId.startsWith('trip_')) {
- // 前端生成的临时ID,需要移除
- tripData.id = null;
- } else {
- tripData.id = this.tripId;
- }
- } else {
- // 创建模式,不设置ID,让后端生成
- tripData.id = null;
- }
-
- console.log('准备保存行程数据:', JSON.stringify(tripData));
-
- // 检查API是否可用
- if (this.$api && this.$api.trip) {
- // 使用API模块发起请求
- const apiMethod = this.editMode ? this.$api.trip.update : this.$api.trip.create;
-
- apiMethod(tripData)
- .then(res => {
- console.log('API保存行程成功:', res);
-
- // 同时保存到本地存储,确保显示正常
- // 如果API返回了完整数据,使用API返回的数据
- const finalTripData = (res && res.data) ? res.data : tripData;
- this.saveToLocalStorage(finalTripData, true);
-
- // 服务器保存成功,清除本地存储
- uni.removeStorageSync('selectedLocations');
-
- // 显示成功提示
- uni.showToast({
- title: this.editMode ? '行程更新成功!' : '行程创建成功!',
- icon: 'success',
- duration: 2000
- });
-
- // 跳转回规划页面
- setTimeout(() => {
- uni.switchTab({
- url: '/pages/planning/index',
- success: () => {
- // 通知规划页面刷新数据
- uni.$emit('refreshTrips');
- console.log('成功返回规划页面并请求刷新');
- }
- });
- }, 1000);
- })
- .catch(err => {
- console.error('API请求失败,回退到本地存储:', err);
- // 回退到本地存储方案
- this.saveToLocalStorage(tripData);
- })
- .finally(() => {
- uni.hideLoading();
- });
- } else {
- // API不可用,直接使用本地存储
- console.log('API不可用,使用本地存储保存行程');
- this.saveToLocalStorage(tripData);
- }
- },
-
- // 保存到本地存储(作为后备方案)
- saveToLocalStorage(tripData, skipToast = false) {
- try {
- // 获取已有的行程
- let savedTrips = uni.getStorageSync('savedTrips') || [];
-
- // 编辑模式 - 更新现有行程
- if (this.editMode && this.tripId) {
- const index = savedTrips.findIndex(trip => trip.id === this.tripId);
- if (index !== -1) {
- // 保留原创建时间
- const originalCreatedAt = savedTrips[index].createdAt;
- tripData.createdAt = originalCreatedAt;
-
- // 更新行程
- savedTrips[index] = tripData;
-
- if (!skipToast) {
- uni.showToast({
- title: '行程更新成功!',
- icon: 'success',
- duration: 2000
- });
- }
- }
- } else {
- // 创建模式 - 添加新行程
- savedTrips.push(tripData);
-
- if (!skipToast) {
- uni.showToast({
- title: '行程创建成功!',
- icon: 'success',
- duration: 2000
- });
- }
- }
-
- console.log('保存到本地的行程数据:', savedTrips);
-
- // 保存回本地存储
- uni.setStorageSync('savedTrips', savedTrips);
-
- // 清除已选景点的临时存储
- uni.removeStorageSync('selectedLocations');
-
- // 如果是API回调中的保存,不需要再跳转
- if (!skipToast) {
- // 跳转回规划页面
- setTimeout(() => {
- uni.switchTab({
- url: '/pages/planning/index',
- success: () => {
- // 通知规划页面刷新数据
- uni.$emit('refreshTrips');
- console.log('成功返回规划页面并请求刷新');
- },
- fail: (err) => {
- console.error('返回规划页面失败:', err);
- }
- });
- }, 1000);
- }
- } catch (e) {
- console.error('保存行程失败:', e);
- uni.showToast({
- title: '保存行程失败',
- icon: 'none'
- });
- }
- },
-
- // 返回上一页
- goBack() {
- uni.navigateBack();
- },
-
- // 计算两点之间的距离(公里)
- calculateDistance(lat1, lng1, lat2, lng2) {
- // 将经纬度转换为数字类型
- lat1 = Number(lat1);
- lng1 = Number(lng1);
- lat2 = Number(lat2);
- lng2 = Number(lng2);
-
- // 检查经纬度是否有效
- if (isNaN(lat1) || isNaN(lng1) || isNaN(lat2) || isNaN(lng2)) {
- return null;
- }
-
- // 使用半正矢公式计算球面距离
- const R = 6371; // 地球半径,单位公里
- const dLat = this.toRadians(lat2 - lat1);
- const dLng = this.toRadians(lng2 - lng1);
-
- const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
- Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) *
- Math.sin(dLng/2) * Math.sin(dLng/2);
-
- const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
- const distance = R * c; // 距离,单位公里
-
- // 保留一位小数
- return parseFloat(distance.toFixed(1));
- },
-
- // 角度转弧度
- toRadians(degrees) {
- return degrees * Math.PI / 180;
- }
- }
- }
- </script>
- <style>
- .plan-detail {
- min-height: 100vh;
- background-color: #f8f8f8;
- }
-
- .navbar {
- display: flex;
- align-items: center;
- height: 90rpx;
- padding: 0 30rpx;
- background-color: #ffffff;
- border-bottom: 1rpx solid #eaeaea;
- position: relative;
- }
-
- .back-btn {
- display: flex;
- align-items: center;
- position: absolute;
- left: 30rpx;
- }
-
- .back-icon {
- font-size: 40rpx;
- }
-
- .back-text {
- font-size: 28rpx;
- margin-left: 6rpx;
- }
-
- .navbar .title {
- flex: 1;
- text-align: center;
- font-size: 32rpx;
- font-weight: 500;
- }
-
- .container {
- padding: 30rpx;
- }
-
- .header {
- margin-bottom: 30rpx;
- }
-
- .header .title {
- font-size: 36rpx;
- font-weight: 600;
- margin-bottom: 10rpx;
- }
-
- .header .subtitle {
- font-size: 28rpx;
- color: #666;
- }
-
- .form-section {
- background-color: #ffffff;
- border-radius: 12rpx;
- padding: 20rpx;
- margin-bottom: 30rpx;
- }
-
- .form-item {
- padding: 20rpx 10rpx;
- border-bottom: 1rpx solid #f0f0f0;
- }
-
- .form-item:last-child {
- border-bottom: none;
- }
-
- .form-label {
- font-size: 30rpx;
- margin-bottom: 20rpx;
- font-weight: 500;
- }
-
- .form-hint {
- font-size: 24rpx;
- color: #999;
- margin-top: 10rpx;
- }
-
- .days-selector, .people-selector {
- display: flex;
- align-items: center;
- }
-
- .days-btn, .people-btn {
- width: 60rpx;
- height: 60rpx;
- border-radius: 50%;
- background-color: #f0f0f0;
- color: #333;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 32rpx;
- }
-
- .days-value, .people-value {
- margin: 0 30rpx;
- font-size: 32rpx;
- width: 100rpx;
- text-align: center;
- }
-
- .disabled {
- opacity: 0.5;
- pointer-events: none;
- }
-
- .budget-input {
- display: flex;
- align-items: center;
- border: 1rpx solid #eaeaea;
- border-radius: 8rpx;
- padding: 0 20rpx;
- height: 80rpx;
- }
-
- .currency {
- font-size: 32rpx;
- color: #666;
- margin-right: 10rpx;
- }
-
- .budget-input input {
- flex: 1;
- height: 80rpx;
- font-size: 32rpx;
- }
-
- .date-picker {
- display: flex;
- align-items: center;
- justify-content: space-between;
- border: 1rpx solid #eaeaea;
- border-radius: 8rpx;
- padding: 0 20rpx;
- height: 80rpx;
- }
-
- .picker-value {
- font-size: 32rpx;
- }
-
- .picker-icon {
- font-size: 24rpx;
- color: #999;
- }
-
- .spots-section {
- background-color: #ffffff;
- border-radius: 12rpx;
- padding: 20rpx;
- margin-bottom: 30rpx;
- }
-
- .section-title {
- font-size: 30rpx;
- font-weight: 500;
- margin-bottom: 20rpx;
- padding: 0 10rpx;
- }
-
- .spots-list {
- max-height: 500rpx;
- overflow-y: auto;
- }
-
- .spot-item {
- display: flex;
- align-items: center;
- padding: 20rpx 10rpx;
- border-bottom: 1rpx solid #f0f0f0;
- }
-
- .spot-item:last-child {
- border-bottom: none;
- }
-
- .spot-index {
- width: 40rpx;
- height: 40rpx;
- border-radius: 50%;
- background-color: #3e98ff;
- color: #ffffff;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 24rpx;
- margin-right: 20rpx;
- }
-
- .spot-info {
- flex: 1;
- }
-
- .spot-name {
- font-size: 28rpx;
- margin-bottom: 6rpx;
- }
-
- .spot-address {
- font-size: 24rpx;
- color: #999;
- }
-
- .remove-btn {
- width: 50rpx;
- height: 50rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 36rpx;
- color: #999;
- }
-
- .empty-state {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 60rpx 0;
- }
-
- .empty-icon {
- font-size: 80rpx;
- margin-bottom: 20rpx;
- }
-
- .empty-text {
- font-size: 28rpx;
- color: #999;
- }
-
- .action-buttons {
- display: flex;
- gap: 20rpx;
- }
-
- .back-btn, .submit-btn {
- flex: 1;
- height: 90rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 45rpx;
- font-size: 32rpx;
- }
-
- .back-btn {
- background-color: #f0f0f0;
- color: #333;
- }
-
- .submit-btn {
- background-color: #3e98ff;
- color: #ffffff;
- }
-
- .submit-btn.disabled {
- background-color: #cccccc;
- }
- </style>
|