index.vue 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188
  1. <template>
  2. <view class="travel-detail">
  3. <!-- 顶部封面区域 -->
  4. <view class="cover-section">
  5. <image class="cover-image" :src="getTripCoverImage()" mode="aspectFill"></image>
  6. <view class="cover-mask"></view>
  7. <view class="cover-content">
  8. <view class="back-btn" @tap="goBack">
  9. <text class="back-icon">←</text>
  10. </view>
  11. <view class="trip-title">{{tripData.name || '保定精彩之旅'}}</view>
  12. <view class="trip-subtitle">{{tripData.days || '3'}}天{{(tripData.days - 1) || '2'}}晚 · {{formatDate(tripData.startDate)}} 出发</view>
  13. </view>
  14. </view>
  15. <!-- 行程信息 -->
  16. <view class="trip-info">
  17. <view class="info-title">行程信息</view>
  18. <view class="info-content">
  19. <view class="info-item">
  20. <text class="info-label">行程天数</text>
  21. <text class="info-value">{{tripData.days || '3'}}天{{(tripData.days - 1) || '2'}}晚</text>
  22. </view>
  23. <view class="info-item">
  24. <text class="info-label">出发时间</text>
  25. <text class="info-value">{{formatDate(tripData.startDate)}}</text>
  26. </view>
  27. <view class="info-item" v-if="tripData.budget">
  28. <text class="info-label">预算金额</text>
  29. <text class="info-value">¥{{tripData.budget}}</text>
  30. </view>
  31. <view class="info-item">
  32. <text class="info-label">出行人数</text>
  33. <text class="info-value">{{tripData.peopleCount || 1}}人</text>
  34. </view>
  35. </view>
  36. </view>
  37. <!-- 景点列表 -->
  38. <view class="spots-section" v-if="tripData.spots && tripData.spots.length > 0">
  39. <view class="section-title">景点行程安排</view>
  40. <view class="spots-list">
  41. <block v-for="(spot, index) in tripData.spots" :key="index">
  42. <!-- 景点项 -->
  43. <view class="spot-item">
  44. <view class="spot-index">{{index + 1}}</view>
  45. <view class="spot-info">
  46. <text class="spot-name">{{spot.name}}</text>
  47. <text class="spot-address" v-if="spot.address">{{spot.address}}</text>
  48. </view>
  49. </view>
  50. <!-- 距离显示(与下一个景点的距离) -->
  51. <view class="distance-item" v-if="index < tripData.spots.length - 1">
  52. <view class="distance-line"></view>
  53. <view class="distance-value">
  54. <text class="distance-icon">↓</text>
  55. <text class="distance-text">{{getDistanceBetween(spot, tripData.spots[index + 1])}}公里</text>
  56. </view>
  57. </view>
  58. </block>
  59. </view>
  60. </view>
  61. <!-- 按钮区域 -->
  62. <view class="action-buttons">
  63. <button class="edit-btn" @tap="editTrip">修改行程</button>
  64. <button class="back-home-btn" @tap="goToHome">返回规划页面</button>
  65. </view>
  66. <!-- 分享二维码按钮 -->
  67. <view class="share-section">
  68. <button class="share-btn" @tap="shareTrip">
  69. <text class="share-icon">🔗</text>
  70. <text class="share-text">分享行程二维码</text>
  71. </button>
  72. </view>
  73. <!-- 删除行程按钮 -->
  74. <view class="delete-section">
  75. <button class="delete-btn" @tap="showDeleteConfirm">
  76. <text class="delete-icon">🗑️</text>
  77. <text class="delete-text">删除行程</text>
  78. </button>
  79. </view>
  80. <!-- 二维码弹窗 -->
  81. <view class="qrcode-popup" v-if="showQrCode" @tap.stop="handlePopupBackdrop">
  82. <view class="qrcode-container" @tap.stop>
  83. <view class="qrcode-header">
  84. <view class="qrcode-title">分享行程</view>
  85. <view class="qrcode-close" @tap.stop="closeQrCode">×</view>
  86. </view>
  87. <view class="qrcode-content">
  88. <view class="qrcode-image-wrap">
  89. <image class="qrcode-image" :src="qrCodeUrl" mode="aspectFit" />
  90. </view>
  91. <view class="qrcode-desc">扫描二维码查看行程</view>
  92. <view class="qrcode-tips">
  93. <text>行程:{{tripData.name}}</text>
  94. <text>天数:{{tripData.days}}天</text>
  95. <text>预算:{{tripData.budget}}元</text>
  96. </view>
  97. </view>
  98. <view class="qrcode-footer">
  99. <button class="qrcode-save-btn" @tap.stop="saveQrCode">保存到相册</button>
  100. </view>
  101. </view>
  102. </view>
  103. </view>
  104. </template>
  105. <script>
  106. import { API } from '@/util/api.js';
  107. export default {
  108. data() {
  109. return {
  110. isLoading: false,
  111. tripId: null,
  112. tripData: {},
  113. options: null,
  114. defaultImages: [
  115. '/static/baoding.jpg',
  116. '/static/custom_plan_icon.png',
  117. '/static/beijing.jpg',
  118. '/static/chengdu.jpg'
  119. ],
  120. showQrCode: false,
  121. qrCodeUrl: '',
  122. qrCodeExpireTime: 0
  123. }
  124. },
  125. onLoad(options) {
  126. console.log('行程详情页面加载,参数:', options);
  127. this.options = options;
  128. // 获取行程ID
  129. if (options && options.planId) {
  130. console.log('接收到行程ID:', options.planId);
  131. this.tripId = options.planId;
  132. this.loadTripData();
  133. } else {
  134. console.log('没有接收到行程ID,使用默认数据');
  135. // 如果没有ID,创建一个默认行程
  136. this.createDefaultTrip();
  137. }
  138. },
  139. // 监听app全局事件
  140. onShow() {
  141. // 监听加载指定行程的事件
  142. uni.$on('loadTripDetail', this.handleLoadTripDetail);
  143. },
  144. onHide() {
  145. // 取消监听事件
  146. uni.$off('loadTripDetail', this.handleLoadTripDetail);
  147. },
  148. methods: {
  149. // 处理通过事件加载行程
  150. handleLoadTripDetail(data) {
  151. if (data && data.tripId) {
  152. console.log('通过事件加载行程:', data.tripId);
  153. this.tripId = data.tripId;
  154. this.loadTripData();
  155. }
  156. },
  157. // 创建默认行程
  158. createDefaultTrip() {
  159. // 创建一个默认行程
  160. this.tripData = {
  161. id: 'default_trip_' + Date.now(),
  162. name: '保定精彩三日游',
  163. days: 3,
  164. nights: 2,
  165. startDate: this.formatDate(new Date()),
  166. budget: 1580
  167. };
  168. uni.showToast({
  169. title: '默认行程已生成',
  170. icon: 'success'
  171. });
  172. },
  173. // 加载行程数据
  174. loadTripData() {
  175. this.isLoading = true;
  176. // 首先尝试通过API加载
  177. if (this.tripId && !this.tripId.startsWith('trip_')) {
  178. // 如果ID不是本地临时ID(不以trip_开头),则使用API
  179. API.trip.detail(this.tripId)
  180. .then(res => {
  181. if (res.data) {
  182. this.tripData = res.data;
  183. uni.showToast({
  184. title: '行程加载成功',
  185. icon: 'success'
  186. });
  187. } else {
  188. console.log('API返回空数据,尝试从本地加载');
  189. this.loadFromLocalStorage();
  190. }
  191. })
  192. .catch(err => {
  193. console.error('通过API加载行程失败:', err);
  194. this.loadFromLocalStorage();
  195. })
  196. .finally(() => {
  197. this.isLoading = false;
  198. });
  199. } else {
  200. // 本地临时ID,从本地存储加载
  201. this.loadFromLocalStorage();
  202. }
  203. },
  204. // 从本地存储加载行程
  205. loadFromLocalStorage() {
  206. try {
  207. // 获取所有已保存的行程
  208. const savedTrips = uni.getStorageSync('savedTrips') || [];
  209. console.log('已保存行程:', savedTrips);
  210. // 查找当前行程ID对应的行程
  211. const tripData = savedTrips.find(trip => trip.id === this.tripId);
  212. if (tripData) {
  213. console.log('找到行程数据:', tripData);
  214. this.tripData = tripData;
  215. uni.showToast({
  216. title: '行程加载成功',
  217. icon: 'success'
  218. });
  219. } else {
  220. console.log('未找到行程数据,使用默认数据');
  221. // 使用默认数据
  222. this.createDefaultTrip();
  223. }
  224. } catch (e) {
  225. console.error('加载行程数据失败:', e);
  226. // 加载失败时使用默认数据
  227. this.createDefaultTrip();
  228. } finally {
  229. this.isLoading = false;
  230. }
  231. },
  232. // 修改行程
  233. editTrip() {
  234. if (!this.tripData || !this.tripData.id) {
  235. uni.showToast({
  236. title: '无法修改默认行程',
  237. icon: 'none'
  238. });
  239. return;
  240. }
  241. // 保存当前行程数据到本地存储中的临时数据
  242. try {
  243. // 如果有景点数据,保存到selectedLocations中
  244. if (this.tripData.spots && this.tripData.spots.length > 0) {
  245. uni.setStorageSync('selectedLocations', JSON.stringify(this.tripData.spots));
  246. // 跳转到行程编辑页面
  247. uni.navigateTo({
  248. url: `/pages/custom-trip/plan-detail?tripId=${this.tripData.id}`,
  249. success: () => {
  250. console.log('成功跳转到行程编辑页面,行程ID:', this.tripData.id);
  251. },
  252. fail: (err) => {
  253. console.error('跳转到行程编辑页面失败:', err);
  254. uni.showToast({
  255. title: '跳转失败',
  256. icon: 'none'
  257. });
  258. }
  259. });
  260. } else {
  261. uni.showToast({
  262. title: '行程没有景点数据',
  263. icon: 'none'
  264. });
  265. }
  266. } catch (e) {
  267. console.error('保存临时数据失败:', e);
  268. uni.showToast({
  269. title: '准备编辑数据失败',
  270. icon: 'none'
  271. });
  272. }
  273. },
  274. // 获取行程封面图片
  275. getTripCoverImage() {
  276. // 如果行程有spots并且第一个spot有图片,则使用该图片
  277. if (this.tripData.spots && this.tripData.spots.length > 0) {
  278. if (this.tripData.spots[0].coverImage) {
  279. return this.tripData.spots[0].coverImage;
  280. }
  281. }
  282. // 根据行程名称选择默认图片
  283. if (this.tripData.name && this.tripData.name.includes('保定')) {
  284. return '/static/baoding.jpg';
  285. } else if (this.tripData.name && this.tripData.name.includes('西安')) {
  286. return '/static/xian.jpg';
  287. } else if (this.tripData.name && this.tripData.name.includes('北京')) {
  288. return '/static/beijing.jpg';
  289. } else if (this.tripData.name && this.tripData.name.includes('上海')) {
  290. return '/static/shanghai.jpg';
  291. }
  292. // 根据行程ID生成一个固定的索引,确保每次显示相同的图片
  293. const hash = this.tripData.id ? this.tripData.id.split('_')[1] : Date.now();
  294. const index = hash % this.defaultImages.length;
  295. return this.defaultImages[index];
  296. },
  297. // 格式化日期
  298. formatDate(date) {
  299. if (!date) {
  300. const now = new Date();
  301. const year = now.getFullYear();
  302. const month = now.getMonth() + 1;
  303. const day = now.getDate();
  304. return `${year}.${month}.${day}`;
  305. }
  306. // 如果有日期对象,进行格式化
  307. if (date instanceof Date) {
  308. const year = date.getFullYear();
  309. const month = date.getMonth() + 1;
  310. const day = date.getDate();
  311. return `${year}.${month}.${day}`;
  312. }
  313. // 如果是字符串,直接返回
  314. return date;
  315. },
  316. // 返回上一页
  317. goBack() {
  318. uni.navigateBack({
  319. fail: () => {
  320. // 如果无法返回上一页,则回到首页
  321. this.goToHome();
  322. }
  323. });
  324. },
  325. // 回到首页
  326. goToHome() {
  327. uni.switchTab({
  328. url: '/pages/planning/index'
  329. });
  330. },
  331. // 计算两个景点之间的距离
  332. calculateDistance(spot1, spot2) {
  333. if (!spot1 || !spot2 || !spot1.latitude || !spot1.longitude || !spot2.latitude || !spot2.longitude) {
  334. return '未知';
  335. }
  336. // 将经纬度转换为数字类型
  337. const lat1 = Number(spot1.latitude);
  338. const lng1 = Number(spot1.longitude);
  339. const lat2 = Number(spot2.latitude);
  340. const lng2 = Number(spot2.longitude);
  341. // 检查经纬度是否有效
  342. if (isNaN(lat1) || isNaN(lng1) || isNaN(lat2) || isNaN(lng2)) {
  343. return '未知';
  344. }
  345. // 使用半正矢公式计算球面距离
  346. const R = 6371; // 地球半径,单位公里
  347. const dLat = this.toRadians(lat2 - lat1);
  348. const dLng = this.toRadians(lng2 - lng1);
  349. const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
  350. Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) *
  351. Math.sin(dLng/2) * Math.sin(dLng/2);
  352. const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
  353. const distance = R * c; // 距离,单位公里
  354. // 格式化距离,保留一位小数
  355. return distance.toFixed(1);
  356. },
  357. // 角度转弧度
  358. toRadians(degrees) {
  359. return degrees * Math.PI / 180;
  360. },
  361. // 获取两个景点之间的距离
  362. getDistanceBetween(spot1, spot2) {
  363. // 如果第二个景点有预先计算的距离信息,直接使用
  364. if (spot2.distanceFromPrevious !== undefined && spot2.distanceFromPrevious !== null) {
  365. return spot2.distanceFromPrevious;
  366. }
  367. // 否则实时计算距离
  368. return this.calculateDistance(spot1, spot2);
  369. },
  370. // 显示删除确认对话框
  371. showDeleteConfirm() {
  372. if (!this.tripData || !this.tripData.id) {
  373. uni.showToast({
  374. title: '无法删除默认行程',
  375. icon: 'none'
  376. });
  377. return;
  378. }
  379. uni.showModal({
  380. title: '确认删除',
  381. content: '确定要删除此行程吗?此操作不可恢复。',
  382. confirmColor: '#ff4d4f',
  383. success: (res) => {
  384. if (res.confirm) {
  385. this.deleteTrip();
  386. }
  387. }
  388. });
  389. },
  390. // 删除行程
  391. deleteTrip() {
  392. uni.showLoading({
  393. title: '正在删除...',
  394. mask: true
  395. });
  396. // 首先尝试通过API删除
  397. if (this.tripId && !this.tripId.startsWith('trip_')) {
  398. // 如果ID不是本地临时ID(不以trip_开头),则使用API
  399. API.trip.delete(this.tripId)
  400. .then(res => {
  401. console.log('API删除行程成功:', res);
  402. // 同时从本地存储中删除
  403. this.deleteFromLocalStorage();
  404. uni.showToast({
  405. title: '行程已删除',
  406. icon: 'success'
  407. });
  408. // 返回规划页面
  409. setTimeout(() => {
  410. this.goToHome();
  411. }, 1000);
  412. })
  413. .catch(err => {
  414. console.error('通过API删除行程失败:', err);
  415. // 尝试从本地存储中删除
  416. this.deleteFromLocalStorage();
  417. })
  418. .finally(() => {
  419. uni.hideLoading();
  420. });
  421. } else {
  422. // 本地临时ID,从本地存储删除
  423. this.deleteFromLocalStorage();
  424. uni.hideLoading();
  425. }
  426. },
  427. // 从本地存储中删除行程
  428. deleteFromLocalStorage() {
  429. try {
  430. // 获取所有已保存的行程
  431. const savedTrips = uni.getStorageSync('savedTrips') || [];
  432. // 过滤掉要删除的行程
  433. const updatedTrips = savedTrips.filter(trip => trip.id !== this.tripId);
  434. // 保存回本地存储
  435. uni.setStorageSync('savedTrips', updatedTrips);
  436. uni.showToast({
  437. title: '行程已删除',
  438. icon: 'success'
  439. });
  440. // 通知规划页面刷新数据
  441. uni.$emit('refreshTrips');
  442. // 返回规划页面
  443. setTimeout(() => {
  444. this.goToHome();
  445. }, 1000);
  446. } catch (e) {
  447. console.error('从本地存储删除行程失败:', e);
  448. uni.showToast({
  449. title: '删除失败',
  450. icon: 'none'
  451. });
  452. }
  453. },
  454. // 处理弹窗背景点击
  455. handlePopupBackdrop(e) {
  456. // 点击背景关闭弹窗
  457. this.closeQrCode();
  458. },
  459. // 分享行程方法
  460. shareTrip() {
  461. console.log("点击分享按钮");
  462. // 直接生成二维码分享
  463. this.generateQrCode();
  464. // 显示加载提示
  465. uni.showToast({
  466. title: '正在生成二维码',
  467. icon: 'loading',
  468. duration: 1000
  469. });
  470. },
  471. // 生成行程二维码
  472. generateQrCode() {
  473. console.log("点击生成二维码按钮");
  474. if (!this.tripData || !this.tripData.id) {
  475. uni.showToast({
  476. title: '无法生成默认行程的二维码',
  477. icon: 'none'
  478. });
  479. return;
  480. }
  481. // 如果已经生成过且未过期,直接显示
  482. if (this.qrCodeUrl && this.qrCodeExpireTime > Date.now()) {
  483. console.log('使用缓存的二维码');
  484. this.showQrCode = true;
  485. return;
  486. }
  487. // 生成一个简单的临时二维码以便稍后替换
  488. this.generateLocalQrCode();
  489. // 创建一个简化版的行程数据,只包含必要字段,减少传输数据量
  490. const simpleTripData = {
  491. id: this.tripData.id,
  492. name: this.tripData.name,
  493. days: this.tripData.days,
  494. budget: this.tripData.budget
  495. };
  496. // 打印发送到后端的数据
  497. console.log('发送到后端的数据:', JSON.stringify(simpleTripData));
  498. // 调用后端API生成二维码
  499. API.trip.generateQrCode(simpleTripData)
  500. .then(res => {
  501. // 打印后端返回的数据
  502. console.log('后端返回的二维码数据:', res);
  503. if (res.qrCodeUrl || res.data?.qrCodeUrl) {
  504. // 兼容不同的返回格式
  505. this.qrCodeUrl = res.qrCodeUrl || res.data?.qrCodeUrl;
  506. this.qrCodeExpireTime = res.expireTime || res.data?.expireTime || (Date.now() + 3600000);
  507. // 打印完整二维码URL和原始内容
  508. console.log('二维码URL:', this.qrCodeUrl);
  509. console.log('二维码内容:', res.qrContent || '未提供');
  510. // 更新二维码图片
  511. this.$nextTick(() => {
  512. // 图片已更新
  513. console.log('二维码图片已更新');
  514. // 显示成功提示
  515. uni.showToast({
  516. title: '二维码生成成功',
  517. icon: 'success',
  518. duration: 1500
  519. });
  520. });
  521. } else {
  522. console.error('生成二维码失败: 返回数据中无qrCodeUrl', res);
  523. // 保持显示本地生成的二维码
  524. uni.showToast({
  525. title: '使用本地二维码',
  526. icon: 'none'
  527. });
  528. }
  529. })
  530. .catch(err => {
  531. console.error('生成二维码失败:', err);
  532. // 保持显示本地生成的二维码
  533. uni.showToast({
  534. title: '使用本地二维码',
  535. icon: 'none',
  536. duration: 1500
  537. });
  538. });
  539. },
  540. // 生成本地二维码(不依赖后端)
  541. generateLocalQrCode() {
  542. try {
  543. // 显示弹窗
  544. this.showQrCode = true;
  545. // 设置为加载中的占位图,使用完整路径格式
  546. const qrContent = `pages/travel-detail/index?planId=${this.tripData.id}`;
  547. const loadingQrCode = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(qrContent)}`;
  548. this.qrCodeUrl = loadingQrCode;
  549. console.log('生成临时二维码:', loadingQrCode);
  550. console.log('二维码内容:', qrContent);
  551. // 这里可以进一步改进,使用本地QR码生成库
  552. } catch (e) {
  553. console.error('生成本地二维码失败:', e);
  554. }
  555. },
  556. // 关闭二维码弹窗
  557. closeQrCode() {
  558. this.showQrCode = false;
  559. },
  560. // 保存二维码到相册
  561. saveQrCode() {
  562. if (!this.qrCodeUrl) {
  563. uni.showToast({
  564. title: '二维码不存在',
  565. icon: 'none'
  566. });
  567. return;
  568. }
  569. // 检查是否有保存到相册的权限
  570. uni.getSetting({
  571. success: (res) => {
  572. if (!res.authSetting['scope.writePhotosAlbum']) {
  573. uni.authorize({
  574. scope: 'scope.writePhotosAlbum',
  575. success: () => {
  576. this.downloadQrCode();
  577. },
  578. fail: () => {
  579. uni.showModal({
  580. title: '提示',
  581. content: '请授权保存图片到相册的权限',
  582. confirmText: '去设置',
  583. success: (res) => {
  584. if (res.confirm) {
  585. uni.openSetting();
  586. }
  587. }
  588. });
  589. }
  590. });
  591. } else {
  592. this.downloadQrCode();
  593. }
  594. }
  595. });
  596. },
  597. // 下载二维码到本地
  598. downloadQrCode() {
  599. uni.showLoading({
  600. title: '保存中...'
  601. });
  602. // 检查qrCodeUrl是否是Base64格式
  603. if (this.qrCodeUrl && this.qrCodeUrl.indexOf('data:image/png;base64,') === 0) {
  604. console.log('检测到Base64格式图片,直接保存');
  605. const base64Data = this.qrCodeUrl.split(',')[1];
  606. // 小程序环境
  607. try {
  608. const fs = wx.getFileSystemManager();
  609. const filePath = `${wx.env.USER_DATA_PATH}/trip_qrcode_${Date.now()}.png`;
  610. fs.writeFile({
  611. filePath: filePath,
  612. data: base64Data,
  613. encoding: 'base64',
  614. success: () => {
  615. // 保存到相册
  616. wx.saveImageToPhotosAlbum({
  617. filePath: filePath,
  618. success: () => {
  619. uni.hideLoading();
  620. uni.showToast({
  621. title: '保存成功',
  622. icon: 'success'
  623. });
  624. },
  625. fail: (err) => {
  626. console.error('保存到相册失败:', err);
  627. uni.hideLoading();
  628. uni.showToast({
  629. title: '保存失败',
  630. icon: 'none'
  631. });
  632. }
  633. });
  634. },
  635. fail: (err) => {
  636. console.error('写入文件失败:', err);
  637. uni.hideLoading();
  638. uni.showToast({
  639. title: '保存失败',
  640. icon: 'none'
  641. });
  642. }
  643. });
  644. } catch (err) {
  645. console.error('保存Base64图片失败:', err);
  646. uni.hideLoading();
  647. uni.showToast({
  648. title: '当前平台不支持保存',
  649. icon: 'none'
  650. });
  651. }
  652. } else {
  653. // 如果是网络图片URL,使用downloadFile方法
  654. uni.downloadFile({
  655. url: this.qrCodeUrl,
  656. success: (res) => {
  657. if (res.statusCode === 200) {
  658. // 保存到相册
  659. uni.saveImageToPhotosAlbum({
  660. filePath: res.tempFilePath,
  661. success: () => {
  662. uni.showToast({
  663. title: '保存成功',
  664. icon: 'success'
  665. });
  666. },
  667. fail: (err) => {
  668. console.error('保存到相册失败:', err);
  669. uni.showToast({
  670. title: '保存失败',
  671. icon: 'none'
  672. });
  673. }
  674. });
  675. }
  676. },
  677. fail: (err) => {
  678. console.error('下载二维码失败:', err);
  679. uni.showToast({
  680. title: '保存失败',
  681. icon: 'none'
  682. });
  683. },
  684. complete: () => {
  685. uni.hideLoading();
  686. }
  687. });
  688. }
  689. },
  690. }
  691. }
  692. </script>
  693. <style>
  694. .travel-detail {
  695. padding-bottom: 40rpx;
  696. background-color: #f5f7fa;
  697. }
  698. /* 顶部封面区域 */
  699. .cover-section {
  700. position: relative;
  701. height: 400rpx;
  702. overflow: hidden;
  703. }
  704. .cover-image {
  705. width: 100%;
  706. height: 100%;
  707. }
  708. .cover-mask {
  709. position: absolute;
  710. top: 0;
  711. left: 0;
  712. width: 100%;
  713. height: 100%;
  714. background: linear-gradient(to bottom, rgba(0,0,0,0.1), rgba(0,0,0,0.6));
  715. }
  716. .cover-content {
  717. position: absolute;
  718. bottom: 40rpx;
  719. left: 0;
  720. width: 100%;
  721. padding: 0 40rpx;
  722. box-sizing: border-box;
  723. }
  724. .back-btn {
  725. position: absolute;
  726. top: -280rpx;
  727. left: 0;
  728. width: 80rpx;
  729. height: 80rpx;
  730. background-color: rgba(255,255,255,0.2);
  731. border-radius: 50%;
  732. display: flex;
  733. align-items: center;
  734. justify-content: center;
  735. }
  736. .back-icon {
  737. font-size: 48rpx;
  738. color: #ffffff;
  739. }
  740. .trip-title {
  741. font-size: 48rpx;
  742. font-weight: bold;
  743. color: #ffffff;
  744. margin-bottom: 16rpx;
  745. text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.3);
  746. }
  747. .trip-subtitle {
  748. font-size: 28rpx;
  749. color: rgba(255,255,255,0.9);
  750. }
  751. /* 行程信息 */
  752. .trip-info {
  753. margin: 30rpx;
  754. padding: 30rpx;
  755. background-color: #ffffff;
  756. border-radius: 20rpx;
  757. box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.05);
  758. }
  759. .info-title {
  760. font-size: 32rpx;
  761. font-weight: bold;
  762. margin-bottom: 20rpx;
  763. color: #333;
  764. }
  765. .info-content {
  766. display: flex;
  767. flex-direction: column;
  768. }
  769. .info-item {
  770. display: flex;
  771. justify-content: space-between;
  772. padding: 16rpx 0;
  773. border-bottom: 1px solid #f0f0f0;
  774. }
  775. .info-item:last-child {
  776. border-bottom: none;
  777. }
  778. .info-label {
  779. color: #666;
  780. font-size: 28rpx;
  781. }
  782. .info-value {
  783. color: #333;
  784. font-size: 28rpx;
  785. font-weight: 500;
  786. }
  787. /* 景点列表 */
  788. .spots-section {
  789. margin: 30rpx;
  790. padding: 30rpx;
  791. background-color: #ffffff;
  792. border-radius: 20rpx;
  793. box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.05);
  794. }
  795. .section-title {
  796. font-size: 32rpx;
  797. font-weight: bold;
  798. margin-bottom: 20rpx;
  799. color: #333;
  800. }
  801. .spots-list {
  802. display: flex;
  803. flex-direction: column;
  804. }
  805. .spot-item {
  806. display: flex;
  807. align-items: center;
  808. padding: 20rpx 0;
  809. }
  810. .spot-index {
  811. width: 50rpx;
  812. height: 50rpx;
  813. background-color: #3e98ff;
  814. color: white;
  815. border-radius: 50%;
  816. display: flex;
  817. justify-content: center;
  818. align-items: center;
  819. font-size: 26rpx;
  820. margin-right: 20rpx;
  821. }
  822. .spot-info {
  823. flex: 1;
  824. }
  825. .spot-name {
  826. font-size: 30rpx;
  827. color: #333;
  828. font-weight: 500;
  829. margin-bottom: 6rpx;
  830. }
  831. .spot-address {
  832. font-size: 24rpx;
  833. color: #999;
  834. }
  835. /* 距离项样式 */
  836. .distance-item {
  837. display: flex;
  838. flex-direction: column;
  839. align-items: center;
  840. padding: 10rpx 0;
  841. position: relative;
  842. }
  843. .distance-line {
  844. width: 2rpx;
  845. height: 40rpx;
  846. background-color: #ddd;
  847. }
  848. .distance-value {
  849. display: flex;
  850. align-items: center;
  851. background-color: #f5f7fa;
  852. padding: 6rpx 16rpx;
  853. border-radius: 100rpx;
  854. margin: 5rpx 0;
  855. }
  856. .distance-icon {
  857. color: #3e98ff;
  858. font-size: 24rpx;
  859. margin-right: 8rpx;
  860. }
  861. .distance-text {
  862. font-size: 24rpx;
  863. color: #666;
  864. }
  865. /* 按钮区域 */
  866. .action-buttons {
  867. padding: 30rpx 30rpx 0;
  868. display: flex;
  869. gap: 20rpx;
  870. justify-content: space-between;
  871. }
  872. .edit-btn {
  873. background-color: #3e98ff;
  874. color: #ffffff;
  875. border-radius: 50rpx;
  876. font-size: 32rpx;
  877. padding: 20rpx 0;
  878. flex: 1;
  879. }
  880. .back-home-btn {
  881. background-color: #f0f0f0;
  882. color: #333;
  883. border-radius: 50rpx;
  884. font-size: 32rpx;
  885. padding: 20rpx 0;
  886. flex: 1;
  887. }
  888. /* 分享二维码按钮样式 */
  889. .share-section {
  890. padding: 0 30rpx;
  891. margin-top: 20rpx;
  892. }
  893. .share-btn {
  894. background-color: #4caf50;
  895. color: #ffffff;
  896. border-radius: 50rpx;
  897. font-size: 32rpx;
  898. padding: 16rpx 0;
  899. width: 100%;
  900. display: flex;
  901. align-items: center;
  902. justify-content: center;
  903. box-shadow: 0 4rpx 10rpx rgba(76, 175, 80, 0.2);
  904. transition: all 0.3s ease;
  905. }
  906. .share-btn:active {
  907. transform: translateY(2rpx);
  908. }
  909. .share-icon {
  910. margin-right: 10rpx;
  911. font-size: 32rpx;
  912. }
  913. .share-text {
  914. font-size: 28rpx;
  915. }
  916. /* 删除行程按钮样式 */
  917. .delete-section {
  918. padding: 20rpx 30rpx 30rpx;
  919. }
  920. .delete-btn {
  921. background-color: #ffffff;
  922. color: #ff4d4f;
  923. border: 1px solid #ff4d4f;
  924. border-radius: 50rpx;
  925. font-size: 32rpx;
  926. padding: 16rpx 0;
  927. width: 100%;
  928. display: flex;
  929. align-items: center;
  930. justify-content: center;
  931. margin-top: 20rpx;
  932. box-shadow: 0 4rpx 10rpx rgba(255, 77, 79, 0.1);
  933. transition: all 0.3s ease;
  934. }
  935. .delete-btn:active {
  936. background-color: #fff1f0;
  937. transform: translateY(2rpx);
  938. }
  939. .delete-icon {
  940. margin-right: 10rpx;
  941. font-size: 32rpx;
  942. }
  943. .delete-text {
  944. font-size: 28rpx;
  945. }
  946. /* 二维码弹窗样式 */
  947. .qrcode-popup {
  948. position: fixed;
  949. top: 0;
  950. left: 0;
  951. right: 0;
  952. bottom: 0;
  953. width: 100%;
  954. height: 100%;
  955. background-color: rgba(0, 0, 0, 0.6);
  956. z-index: 9999;
  957. display: flex;
  958. align-items: center;
  959. justify-content: center;
  960. }
  961. .qrcode-container {
  962. width: 600rpx;
  963. background-color: #ffffff;
  964. border-radius: 20rpx;
  965. overflow: hidden;
  966. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.2);
  967. transform: scale(1);
  968. }
  969. .qrcode-header {
  970. padding: 30rpx;
  971. display: flex;
  972. justify-content: space-between;
  973. align-items: center;
  974. border-bottom: 1rpx solid #f0f0f0;
  975. }
  976. .qrcode-title {
  977. font-size: 32rpx;
  978. font-weight: bold;
  979. color: #333;
  980. }
  981. .qrcode-close {
  982. display: flex;
  983. align-items: center;
  984. justify-content: center;
  985. width: 60rpx;
  986. height: 60rpx;
  987. font-size: 48rpx;
  988. color: #999;
  989. line-height: 1;
  990. border-radius: 50%;
  991. }
  992. .qrcode-close:active {
  993. background-color: #f5f5f5;
  994. }
  995. .qrcode-content {
  996. padding: 40rpx;
  997. display: flex;
  998. flex-direction: column;
  999. align-items: center;
  1000. }
  1001. .qrcode-image-wrap {
  1002. width: 400rpx;
  1003. height: 400rpx;
  1004. display: flex;
  1005. justify-content: center;
  1006. align-items: center;
  1007. margin-bottom: 20rpx;
  1008. padding: 20rpx;
  1009. border: 1px dashed #eee;
  1010. border-radius: 8rpx;
  1011. background-color: #fff;
  1012. }
  1013. .qrcode-image {
  1014. width: 100%;
  1015. height: 100%;
  1016. }
  1017. .qrcode-desc {
  1018. font-size: 28rpx;
  1019. color: #666;
  1020. margin-top: 20rpx;
  1021. }
  1022. .qrcode-tips {
  1023. font-size: 24rpx;
  1024. color: #666;
  1025. margin-top: 20rpx;
  1026. display: flex;
  1027. flex-direction: column;
  1028. align-items: center;
  1029. }
  1030. .qrcode-tips text {
  1031. margin: 4rpx 0;
  1032. }
  1033. .qrcode-footer {
  1034. padding: 30rpx;
  1035. border-top: 1rpx solid #f0f0f0;
  1036. }
  1037. .qrcode-save-btn {
  1038. background-color: #3e98ff;
  1039. color: #ffffff;
  1040. border-radius: 50rpx;
  1041. font-size: 32rpx;
  1042. padding: 16rpx 0;
  1043. width: 100%;
  1044. display: flex;
  1045. align-items: center;
  1046. justify-content: center;
  1047. }
  1048. .qrcode-save-btn:active {
  1049. opacity: 0.9;
  1050. transform: translateY(2rpx);
  1051. }
  1052. </style>