refactor travel

This commit is contained in:
Timi
2025-12-13 18:44:37 +08:00
parent 880e702288
commit 69659a1746
37 changed files with 4154 additions and 400 deletions

View File

@ -1,9 +1,13 @@
{
"component": true,
"usingComponents": {
"t-tag": "tdesign-miniprogram/tag/tag",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-empty": "tdesign-miniprogram/empty/empty",
"t-navbar": "tdesign-miniprogram/navbar/navbar",
"t-collapse": "tdesign-miniprogram/collapse/collapse",
"t-collapse-panel": "tdesign-miniprogram/collapse-panel/collapse-panel"
}
}
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group"
},
"styleIsolation": "shared",
"enablePullDownRefresh": true
}

View File

@ -1,25 +1,119 @@
/* pages/main/travel/travel.wxss */
// pages/main/travel/index.less
.travel {
.filter-menu {
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999;
position: fixed;
background: var(--theme-bg-overlay);
.collapse {
.content {
z-index: 1000;
position: fixed;
background: var(--theme-bg-menu);
box-shadow: 0 0 12px var(--theme-shadow-medium);
border-radius: 8rpx;
}
}
.panel {
.travel-list {
width: 100vw;
padding: 16rpx;
min-height: 100vh;
box-sizing: border-box;
padding-bottom: 120rpx;
.images {
column-gap: .25rem;
column-count: 3;
padding-bottom: 2rem;
.travel-card {
background: var(--theme-bg-card);
margin-bottom: 24rpx;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2px 12px var(--theme-shadow-light);
transition: all .3s;
.image {
width: 100%;
display: block;
overflow: hidden;
background: var(--theme-bg-card);
break-inside: avoid;
margin-bottom: .25rem;
&:active {
transform: scale(.98);
box-shadow: 0 2px 8px var(--theme-shadow-light);
}
.card-header {
padding: 24rpx;
border-bottom: 1px solid var(--theme-border-light);
}
.card-body {
padding: 24rpx;
.title {
color: var(--theme-text-primary);
font-size: 32rpx;
font-weight: bold;
margin-bottom: 16rpx;
line-height: 1.5;
}
.content {
color: var(--theme-text-secondary);
font-size: 28rpx;
line-height: 1.6;
margin-bottom: 24rpx;
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 2;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
}
.meta {
gap: 16rpx;
display: flex;
flex-wrap: wrap;
.meta-item {
gap: 8rpx;
display: flex;
align-items: center;
.icon {
color: var(--theme-text-secondary);
}
.text {
color: var(--theme-text-secondary);
font-size: 24rpx;
}
}
}
}
}
}
.finished {
color: var(--theme-text-secondary);
padding: 32rpx 0;
font-size: 24rpx;
text-align: center;
}
}
.fab {
right: 32rpx;
width: 112rpx;
bottom: 120rpx;
height: 112rpx;
display: flex;
z-index: 9999;
position: fixed;
background: var(--theme-wx);
align-items: center;
border-radius: 50%;
justify-content: center;
box-shadow: 0 8px 24px rgba(102, 126, 234, .4);
transition: all .3s ease;
&:active {
transform: scale(.9);
box-shadow: 0 4px 12px rgba(102, 126, 234, .3);
}
}

View File

@ -1,106 +1,174 @@
// pages/main/travel/travel.ts
// pages/main/travel/index.ts
import config from "../../../config/index";
import Time from "../../../utils/Time";
import { TravelApi } from "../../../api/TravelApi";
import { Travel, TravelPage, TravelStatus, TravelStatusLabel, TravelStatusIcon, TransportationTypeLabel } from "../../../types/Travel";
import { OrderType } from "../../../types/Model";
export type Luggage = {
gao: LuggageItem[];
yu: LuggageItem[];
}
export type LuggageItem = {
name: string;
isTaken: boolean;
}
type Guide = {
title: string;
images: string[];
}
interface ITravelData {
luggage?: Luggage;
guides: Guide[];
guidesDB: Guide[];
activeCollapse?: number;
interface TravelData {
/** 分页参数 */
page: TravelPage;
/** 旅行列表 */
list: Travel[];
/** 当前筛选状态 */
currentStatus: TravelStatus | "ALL";
/** 是否正在加载 */
isFetching: boolean;
/** 是否已加载完成 */
isFinished: boolean;
/** 是否显示筛选菜单 */
isShowFilterMenu: boolean;
/** 菜单位置 */
menuTop: number;
menuLeft: number;
/** 状态标签映射 */
statusLabels: typeof TravelStatusLabel;
/** 状态图标映射 */
statusIcons: typeof TravelStatusIcon;
/** 交通类型标签映射 */
transportLabels: typeof TransportationTypeLabel;
}
Page({
data: <ITravelData>{
luggage: undefined,
guides: [],
guidesDB: [],
activeCollapse: undefined
data: <TravelData>{
page: {
index: 0,
size: 10,
orderMap: {
travelAt: OrderType.DESC
}
},
list: [],
currentStatus: "ALL",
isFetching: false,
isFinished: false,
isShowFilterMenu: false,
menuTop: 0,
menuLeft: 0,
statusLabels: TravelStatusLabel,
statusIcons: TravelStatusIcon,
transportLabels: TransportationTypeLabel
},
onLoad() {
wx.request({
url: `${config.url}/journal/travel`,
method: "GET",
header: {
Key: wx.getStorageSync("key")
},
success: async (resp: any) => {
this.setData({
luggage: resp.data.data.luggage,
guides: resp.data.data.guides.map((item: any) => {
return {
title: item.title,
images: [] // 留空分批加载
}
}),
guidesDB: resp.data.data.guides
})
}
});
this.resetAndFetch();
},
onShow() {
wx.request({
url: `${config.url}/journal/travel`,
method: "GET",
header: {
Key: wx.getStorageSync("key")
},
success: async (resp: any) => {
this.setData({
luggage: resp.data.data.luggage
});
}
});
},
toLuggageList(e: WechatMiniprogram.BaseEvent) {
const { name } = e.currentTarget.dataset;
wx.setStorageSync("luggage", {
name,
luggage: this.data.luggage
});
wx.navigateTo({
"url": "/pages/main/travel/luggage/index"
})
},
onCollapseChange(e: any) {
const index = e.detail.value;
if (this.data.guides[index].images.length === 0) {
this.data.guides[index].images = this.data.guidesDB[index].images.map((item: any) => {
return `${config.url}/attachment/read/${item}`;
});
this.setData({
guides: this.data.guides
})
}
onHide() {
this.setData({
activeCollapse: index
})
isShowFilterMenu: false
});
},
preview(e: WechatMiniprogram.BaseEvent) {
const { index, imageIndex } = e.currentTarget.dataset;
const images = this.data.guides[index].images;
wx.previewMedia({
current: imageIndex,
sources: images.map((image: any) => {
return {
url: image,
type: "image"
onPullDownRefresh() {
this.resetAndFetch();
wx.stopPullDownRefresh();
},
onReachBottom() {
this.fetch();
},
/** 重置并获取数据 */
resetAndFetch() {
this.setData({
page: {
index: 0,
size: 10,
orderMap: {
travelAt: OrderType.DESC
},
equalsExample: this.data.currentStatus === "ALL" ? undefined : {
status: this.data.currentStatus as TravelStatus
}
})
})
},
list: [],
isFetching: false,
isFinished: false
});
this.fetch();
},
/** 获取旅行列表 */
async fetch() {
if (this.data.isFetching || this.data.isFinished) {
return;
}
this.setData({ isFetching: true });
try {
const pageResult = await TravelApi.getList(this.data.page);
const list = pageResult.list || [];
if (list.length === 0) {
this.setData({ isFinished: true });
return;
}
// 格式化数据
list.forEach(travel => {
if (travel.travelAt) {
travel.travelDate = Time.toDate(travel.travelAt);
travel.travelTime = Time.toTime(travel.travelAt);
}
});
this.setData({
page: {
...this.data.page,
index: this.data.page.index + 1
},
list: this.data.list.concat(list),
isFinished: list.length < this.data.page.size
});
} catch (error) {
console.error("获取旅行列表失败:", error);
} finally {
this.setData({ isFetching: false });
}
},
/** 切换筛选菜单 */
toggleFilterMenu() {
if (!this.data.isShowFilterMenu) {
// 打开菜单时计算位置
const query = wx.createSelectorQuery();
query.select(".filter-btn").boundingClientRect();
query.exec((res) => {
if (res[0]) {
const { top, left, height } = res[0];
this.setData({
isShowFilterMenu: true,
menuTop: top + height + 16,
menuLeft: left
});
}
});
} else {
// 关闭菜单
this.setData({
isShowFilterMenu: false
});
}
},
/** 阻止事件冒泡 */
stopPropagation() {
// 空函数,仅用于阻止事件冒泡
},
/** 筛选状态 */
filterByStatus(e: WechatMiniprogram.BaseEvent) {
const status = e.currentTarget.dataset.status as TravelStatus | "ALL";
this.setData({
currentStatus: status,
isShowFilterMenu: false
});
this.resetAndFetch();
},
/** 新建旅行 */
toCreate() {
wx.navigateTo({
url: "/pages/main/travel-editor/index"
});
},
/** 查看详情 */
toDetail(e: WechatMiniprogram.BaseEvent) {
const { id } = e.currentTarget.dataset;
wx.navigateTo({
url: `/pages/main/travel-detail/index?id=${id}`
});
},
});

View File

@ -1,30 +1,108 @@
<!--pages/main/travel/travel.wxml-->
<!--pages/main/travel/index.wxml-->
<view class="custom-navbar">
<t-navbar title="北海之旅" />
<t-navbar title="旅行计划">
<view slot="left" class="filter-btn" bind:tap="toggleFilterMenu">
<t-icon name="filter" size="24px" />
</view>
</t-navbar>
</view>
<view class="travel">
<t-cell title="小糕的旅行装备" arrow bindtap="toLuggageList" data-name="gao"></t-cell>
<t-cell title="夜雨的旅行装备" arrow bindtap="toLuggageList" data-name="yu"></t-cell>
<t-collapse class="collapse" bind:change="onCollapseChange" expandMutex expandIcon>
<t-collapse-panel
class="panel"
wx:for="{{guides}}"
header="{{item.title}}"
value="{{index}}"
wx:key="index"
>
<view wx:if="{{item.images}}" class="images">
<block wx:for="{{item.images}}" wx:for-item="image" wx:for-index="imageIndex" wx:key="imageIndex">
<image
class="image"
src="{{image}}"
mode="widthFix"
bindtap="preview"
data-index="{{index}}"
data-image-index="{{imageIndex}}"
></image>
</block>
<!-- 筛选菜单 -->
<view wx:if="{{isShowFilterMenu}}" class="filter-menu" catchtap="toggleFilterMenu">
<t-cell-group
class="content"
theme="card"
style="top: {{menuTop}}px; left: {{menuLeft}}px;"
catchtap="stopPropagation"
>
<t-cell
title="全部"
leftIcon="bulletpoint"
bind:tap="filterByStatus"
data-status="ALL"
rightIcon="{{currentStatus === 'ALL' ? 'check' : ''}}"
/>
<t-cell
title="计划中"
leftIcon="calendar"
bind:tap="filterByStatus"
data-status="PLANNING"
rightIcon="{{currentStatus === 'PLANNING' ? 'check' : ''}}"
/>
<t-cell
title="进行中"
leftIcon="play-circle"
bind:tap="filterByStatus"
data-status="ONGOING"
rightIcon="{{currentStatus === 'ONGOING' ? 'check' : ''}}"
/>
<t-cell
title="已完成"
leftIcon="check-circle"
bind:tap="filterByStatus"
data-status="COMPLETED"
rightIcon="{{currentStatus === 'COMPLETED' ? 'check' : ''}}"
/>
</t-cell-group>
</view>
<!-- 旅行列表 -->
<view class="travel-list">
<!-- 空状态 -->
<t-empty
wx:if="{{!isFetching && list.length === 0}}"
icon="travel"
description="暂无旅行计划"
/>
<!-- 列表内容 -->
<view
wx:for="{{list}}"
wx:for-item="travel"
wx:for-index="travelIndex"
wx:key="id"
class="travel-card"
bind:tap="toDetail"
data-id="{{travel.id}}"
>
<view class="card-header">
<t-tag
theme="{{travel.status === 'PLANNING' ? 'default' : travel.status === 'ONGOING' ? 'warning' : 'success'}}"
variant="light"
icon="{{statusIcons[travel.status]}}"
>
{{statusLabels[travel.status]}}
</t-tag>
</view>
<view class="card-body">
<view class="title">{{travel.title || '未命名旅行'}}</view>
<view wx:if="{{travel.content}}" class="content">{{travel.content}}</view>
<view class="meta">
<view class="meta-item">
<t-icon name="time" size="16px" class="icon" />
<text class="text">{{travel.travelDate}} {{travel.travelTime}}</text>
</view>
<view wx:if="{{travel.days}}" class="meta-item">
<t-icon name="calendar" size="16px" class="icon" />
<text class="text">{{travel.days}} 天</text>
</view>
<view wx:if="{{travel.transportationType}}" class="meta-item">
<t-icon name="{{travel.transportationType === 'PLANE' ? 'flight-takeoff' : travel.transportationType === 'TRAIN' ? 'map-route' : travel.transportationType === 'SELF_DRIVING' ? 'control-platform' : 'location'}}" size="16px" class="icon" />
<text class="text">{{transportLabels[travel.transportationType]}}</text>
</view>
</view>
</t-collapse-panel>
</t-collapse>
</view>
</view>
</view>
<!-- 加载完成提示 -->
<view wx:if="{{isFinished && 0 < list.length}}" class="finished">没有更多了</view>
</view>
<!-- 新建按钮 -->
<view class="fab" bind:tap="toCreate">
<t-icon name="add" size="24px" color="#fff" />
</view>

View File

@ -1,10 +0,0 @@
{
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon",
"t-input": "tdesign-miniprogram/input/input",
"t-navbar": "tdesign-miniprogram/navbar/navbar",
"t-button": "tdesign-miniprogram/button/button",
"t-checkbox": "tdesign-miniprogram/checkbox/checkbox",
"t-checkbox-group": "tdesign-miniprogram/checkbox-group/checkbox-group"
}
}

View File

@ -1,88 +0,0 @@
.luggage {
.tips {
color: var(--theme-text-secondary);
margin: .5rem 0;
font-size: 12px;
text-align: center;
}
.items {
gap: 8px;
width: calc(100% - 64rpx);
margin: 12rpx auto;
display: grid;
grid-template-columns: repeat(3, 1fr);
.item {
--td-checkbox-vertical-padding: 12rpx 24rpx;
flex: 1;
margin: 0;
border: 3rpx solid var(--theme-text-disabled);
position: relative;
overflow: hidden;
box-sizing: border-box;
word-break: break-all;
border-radius: 12rpx;
&:first-child {
margin-left: 0;
}
&.active {
border-color: var(--td-brand-color, #0052d9);
&::after {
content: "";
top: 0;
left: 0;
width: 0;
height: 0;
display: block;
position: absolute;
border-width: 24px 24px 24px 0;
border-style: solid;
border-color: var(--td-brand-color);
border-bottom-color: transparent;
border-right-color: transparent;
}
}
.icon {
top: 1px;
left: 1px;
color: var(--td-bg-color-container, #fff);
z-index: 1;
position: absolute;
font-size: 12px;
}
.checkbox {
height: calc(100% - 24rpx);
}
}
}
}
.add-container {
left: 0;
right: 0;
color: var(--theme-text-primary);
bottom: 0;
display: flex;
padding: 20rpx;
position: fixed;
border-top: 1px solid var(--theme-border-light);
background: var(--theme-bg-secondary);
box-sizing: border-box;
align-items: center;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
backdrop-filter: blur(10px);
.input {
--td-input-vertical-padding: 8rpx;
margin-right: .5rem;
border-radius: 5px;
}
}

View File

@ -1,86 +0,0 @@
// pages/main/travel/luggage/index.ts
import { LuggageItem } from "..";
import config from "../../../../config/index"
interface ILuggageData {
name: string;
value: LuggageItem[];
keyboardHeight: number;
addValue: string;
}
Page({
data: <ILuggageData>{
name: "",
value: [],
keyboardHeight: 0,
addValue: ""
},
onLoad() {
wx.onKeyboardHeightChange(res => {
this.setData({
keyboardHeight: res.height
})
})
},
onShow() {
const store = wx.getStorageSync("luggage");
const value = store.luggage[store.name];
this.setData({
value,
name: store.name === "gao" ? "小糕" : "夜雨"
});
},
doBack() {
const store = wx.getStorageSync("luggage");
store.luggage[store.name] = this.data.value;
wx.request({
url: `${config.url}/journal/travel/luggage/update`,
method: "POST",
header: {
Key: wx.getStorageSync("key")
},
data: store.luggage,
success: () => {
wx.navigateBack();
}
});
},
onTapItem(e: WechatMiniprogram.BaseEvent) {
const index = e.currentTarget.dataset.index;
this.data.value[index].isTaken = !this.data.value[index].isTaken;
this.setData({ value: this.data.value });
},
showMenu(e: WechatMiniprogram.BaseEvent) {
const index = e.currentTarget.dataset.index;
wx.showActionSheet({
itemList: ["删除"],
itemColor: "red",
success: () => {
this.data.value.splice(index, 1);
this.setData({ value: this.data.value })
}
});
},
onInputFocus() {
this.setData({
keyboardHeight: this.data.keyboardHeight
})
},
onInputBlur() {
this.setData({
keyboardHeight: 0
})
},
add() {
this.data.value.push({
name: this.data.addValue,
isTaken: false
})
this.setData({
value: this.data.value,
addValue: ""
})
}
})

View File

@ -1,39 +0,0 @@
<!--pages/main/travel/luggage/index.wxml-->
<wxs module="_"> module.exports.contain = function(arr, key) { return arr.indexOf(key) > -1 } </wxs>
<view class="custom-navbar">
<t-navbar title="{{name}}的旅行装备" bind:go-back="doBack" delta="0" left-arrow />
</view>
<view class="luggage">
<view class="tips">tips: 勾选表示已携带,返回自动保存</view>
<t-checkbox-group class="items">
<view
class="item {{item.isTaken ? 'active' : ''}}"
wx:for="{{value}}"
wx:key="index"
data-index="{{index}}"
bindtap="onTapItem"
bindlongpress="showMenu"
>
<t-icon wx:if="{{item.isTaken}}" name="check" class="icon" ariaHidden="{{true}}" />
<t-checkbox
class="checkbox"
value="{{item.isTaken}}"
label="{{item.name}}"
icon="none"
borderless
/>
</view>
</t-checkbox-group>
</view>
<view class="add-container" style="transform: translateY(-{{keyboardHeight}}px)">
<t-input
class="input"
placeholder="请输入新的物品"
cursor-spacing="20"
adjust-position="{{false}}"
model:value="{{addValue}}"
borderless
/>
<t-button size="small" theme="primary" bindtap="add" disabled="{{!addValue}}">添加</t-button>
</view>