feat:重构模块
This commit is contained in:
50
src/components/Travel/Data/TravelBasicData.vue
Normal file
50
src/components/Travel/Data/TravelBasicData.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="info-item card-item">
|
||||
<div class="title-info">
|
||||
<svg-icon name="home-basic"/>
|
||||
<span class="sub-title">基础信息</span>
|
||||
</div>
|
||||
|
||||
<div class="info travel-basic-info">
|
||||
<div class="basic-item">
|
||||
<span class="basic-title">景点地区</span>
|
||||
<div class="basic-content">
|
||||
<el-tag size="large">
|
||||
<i class="fa-solid fa-location-dot"></i>
|
||||
{{ travelStore.currentTravelSpot.area.join('-') }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="basic-item">
|
||||
<span class="basic-title">景点地址</span>
|
||||
<div class="basic-content">
|
||||
<el-tag v-if="travelStore.currentTravelSpot.address"
|
||||
size="large">
|
||||
<i class="fa-solid fa-map-location-dot"></i>
|
||||
{{ travelStore.currentTravelSpot.address }}
|
||||
</el-tag>
|
||||
<el-tag v-else>无</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="basic-item">
|
||||
<span class="basic-title">景点价格</span>
|
||||
<div class="basic-content">
|
||||
<el-tag size="large">
|
||||
<i class="fa-solid fa-ticket"></i>
|
||||
{{ formatAmount(travelStore.currentTravelSpot.price) }} 元
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SvgIcon } from 'vue3-common'
|
||||
import { useTravelStore } from '@/store/travel.ts'
|
||||
import { formatAmount } from 'vue3-common/utils/numberUtil'
|
||||
|
||||
const travelStore = useTravelStore()
|
||||
</script>
|
||||
25
src/components/Travel/Data/TravelConsumeData.vue
Normal file
25
src/components/Travel/Data/TravelConsumeData.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<el-table :data="props.data" border>
|
||||
<el-table-column type="index" align="center" width="50" />
|
||||
<el-table-column prop="type" label="类型" align="center" min-width="20%"/>
|
||||
<el-table-column prop="name" label="名称" align="center" min-width="40%"/>
|
||||
<el-table-column prop="amount" label="金额" align="center" min-width="20%">
|
||||
<template #default="scope">
|
||||
{{ formatAmount(scope.row.amount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatAmount } from 'vue3-common/utils/numberUtil'
|
||||
import { defineProps, PropType } from 'vue'
|
||||
import { ITravelConsume } from '@/types/travel.ts'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
required: true,
|
||||
type: Array as PropType<ITravelConsume[]>
|
||||
}
|
||||
})
|
||||
</script>
|
||||
76
src/components/Travel/Data/TravelGuideData.vue
Normal file
76
src/components/Travel/Data/TravelGuideData.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="info-item card-item">
|
||||
<div class="title-info">
|
||||
<svg-icon name="home-guideInfo"></svg-icon>
|
||||
<span class="sub-title">攻略信息</span>
|
||||
</div>
|
||||
|
||||
<div class="info guide-info">
|
||||
<div class="guide-item">
|
||||
<span class="guide-title">交通指南</span>
|
||||
<div v-if="travelStore.currentTravelSpot.traffic" class="guide-content">
|
||||
{{ travelStore.currentTravelSpot.traffic }}
|
||||
</div>
|
||||
<div v-else class="guide-content">
|
||||
<el-tag>无</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guide-item">
|
||||
<span class="guide-title">游玩路线</span>
|
||||
<div v-if="travelStore.currentTravelSpot.router" class="guide-content">
|
||||
{{ travelStore.currentTravelSpot.router }}
|
||||
</div>
|
||||
<div v-else class="guide-content">
|
||||
<el-tag>无</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guide-item">
|
||||
<span class="guide-title">美食推荐</span>
|
||||
<div v-if="travelStore.currentTravelSpot.foods.length > 0"
|
||||
class="guide-content">
|
||||
<el-tag v-for="(item, index) in travelStore.currentTravelSpot.foods"
|
||||
:key="index" size="large">
|
||||
{{ item }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div v-else class="guide-content">
|
||||
<el-tag>无</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guide-item">
|
||||
<span class="guide-title">项目推荐</span>
|
||||
<div v-if="travelStore.currentTravelSpot.projects.length > 0"
|
||||
class="guide-content">
|
||||
<el-tag v-for="(item, index) in travelStore.currentTravelSpot.projects"
|
||||
:key="index" size="large">
|
||||
{{ item }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div v-else class="guide-content">
|
||||
<el-tag>无</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guide-item">
|
||||
<span class="guide-title">其他</span>
|
||||
<div class="guide-content">
|
||||
<el-tag v-if="travelStore.currentTravelSpot.remark"
|
||||
size="large">
|
||||
{{ travelStore.currentTravelSpot.remark }}
|
||||
</el-tag>
|
||||
<el-tag v-else>无</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SvgIcon } from 'vue3-common'
|
||||
import { useTravelStore } from '@/store/travel.ts'
|
||||
|
||||
const travelStore = useTravelStore()
|
||||
</script>
|
||||
47
src/components/Travel/Data/TravelRecordData.vue
Normal file
47
src/components/Travel/Data/TravelRecordData.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="info-item">
|
||||
<div class="title-info">
|
||||
<svg-icon name="home-travel-record"></svg-icon>
|
||||
<span class="sub-title">旅行记录</span>
|
||||
</div>
|
||||
|
||||
<div v-if="travelStore.currentTravelSpot.recordList.length > 0" class="info record-info">
|
||||
<el-timeline>
|
||||
<el-timeline-item v-for="(item, index) in travelStore.currentTravelSpot.recordList"
|
||||
:key="index" :timestamp="item.date" placement="top">
|
||||
<el-card class="record-card">
|
||||
<span class="record-content">旅行人:{{ item.persons.join('-') }}</span>
|
||||
<el-image v-for="(image, index) in item.imageList" :key="index"
|
||||
:src="serviceFileRootPath + image"
|
||||
:preview-src-list="[serviceFileRootPath + image]"
|
||||
fit="contain" alt="暂无图片" class="record-image"/>
|
||||
|
||||
<video v-for="(video, index) in item.videoList"
|
||||
:key="index" controls style="width: 60%;min-height: 200px;">
|
||||
<source :src="serviceFileRootPath + video" type="video/mp4">
|
||||
您的浏览器不支持 HTML5 视频
|
||||
</video>
|
||||
|
||||
<!-- <el-carousel v-if="item.imageList.length > 0"-->
|
||||
<!-- arrow="always">-->
|
||||
<!-- <el-carousel-item v-for="(item, index) in item.imageList" :key="index">-->
|
||||
<!-- -->
|
||||
<!-- </el-carousel-item>-->
|
||||
<!-- </el-carousel>-->
|
||||
|
||||
<!-- <el-empty v-else description="暂无记录"/>-->
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
<el-empty v-else description="暂无记录"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SvgIcon } from 'vue3-common'
|
||||
import { useTravelStore } from '@/store/travel.ts'
|
||||
import { serviceFileRootPath } from '@/utils'
|
||||
|
||||
const travelStore = useTravelStore()
|
||||
</script>
|
||||
106
src/components/Travel/Form/TravelRecordDialog.vue
Normal file
106
src/components/Travel/Form/TravelRecordDialog.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<el-dialog v-model="dialogData.visible" :title="dialogData.title"
|
||||
center @open="openDialogEvent" class="travel-record-dialog">
|
||||
<el-form ref="formRef" :model="dialogData.data"
|
||||
:rules="rules" label-width="80">
|
||||
<el-form-item label="景点名称" prop="name">
|
||||
<el-select
|
||||
v-model="travelStore.travelRecordDialog.data.name"
|
||||
@focus="travelStore.queryTravelSpotNameList()"
|
||||
:disabled="travelStore.travelRecordApiType === 'UPDATE'"
|
||||
placeholder="请选择景点名称">
|
||||
<el-option
|
||||
v-for="(item, index) in travelStore.travelSpotNameList"
|
||||
:key="index" :label="item" :value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="旅行日期" prop="date">
|
||||
<el-date-picker
|
||||
v-model="dialogData.data.date"
|
||||
type="date" placeholder="请选择日期"
|
||||
value-format="YYYY-MM-DD"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="旅行人员" prop="persons">
|
||||
<multi-input :data="dialogData.data.persons"
|
||||
:size="5"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="图片">
|
||||
<upload-image v-model="dialogData.data.imageList"
|
||||
:service-url="fileServiceUrl"
|
||||
:image-limit="3"
|
||||
:service-file-root-path="serviceFileRootPath"
|
||||
:image-path="travelStore.travelImagePath"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancelClick">取消</el-button>
|
||||
<el-button type="primary" @click="confirmClick">保存</el-button>
|
||||
<el-button v-if="travelStore.travelRecordApiType === 'UPDATE'"
|
||||
type="danger" @click="deleteClick">删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { UploadImage } from 'vue3-common'
|
||||
import { useTravelStore } from '@/store/travel.ts'
|
||||
import { ref, reactive, computed, nextTick } from 'vue'
|
||||
import { FormRules } from 'element-plus'
|
||||
import { travelRecordRules } from '@/utils/element/elRules.ts'
|
||||
import { MultiInput } from 'vue3-common'
|
||||
import { fileServiceUrl, serviceFileRootPath } from '@/utils'
|
||||
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
|
||||
|
||||
const rules = reactive<FormRules>(travelRecordRules)
|
||||
|
||||
const formRef = ref()
|
||||
const travelStore = useTravelStore()
|
||||
|
||||
const dialogData = computed(() => travelStore.travelRecordDialog)
|
||||
|
||||
/**
|
||||
* 打开对话框事件
|
||||
*/
|
||||
const openDialogEvent = async () => {
|
||||
await nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击确认按钮
|
||||
*/
|
||||
const confirmClick = () => {
|
||||
formRef.value.validate().then(() => {
|
||||
travelStore.handleTravelRecordApi(dialogData.value.data)
|
||||
})
|
||||
}
|
||||
|
||||
const cancelClick = () => {
|
||||
dialogData.value.visible = false
|
||||
}
|
||||
|
||||
const deleteClick = () => {
|
||||
commonElMessageBox('是否确认删除该记录?').then(() => {
|
||||
travelStore.travelRecordApiType = 'DELETE'
|
||||
travelStore.handleTravelRecordApi(dialogData.value.data)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.travel-record-dialog {
|
||||
width: 40%;
|
||||
|
||||
.el-dialog__body {
|
||||
height: 45vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
102
src/components/Travel/TravelCalendar.vue
Normal file
102
src/components/Travel/TravelCalendar.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="travel-daily">
|
||||
<v-calendar ref="calendarRef" :attributes='dailyInfo.attrs'
|
||||
@dayclick="dateClick" @did-move="changeMonthClick"
|
||||
:is-dark="appStore.isDark"
|
||||
class="travel-calendar card-item"/>
|
||||
|
||||
<travel-daily-item :date="dailyInfo.calendarDate"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TravelDailyItem from '@/components/Travel/TravelDailyItem.vue'
|
||||
import { reactive, watch, ref, onMounted } from 'vue'
|
||||
import { useTravelStore } from '@/store/travel.ts'
|
||||
import { formatDate } from 'vue3-common/utils/dateUtil'
|
||||
import { CalendarDay } from 'v-calendar/dist/types/src/utils/page'
|
||||
import { useAppStore } from '@/store/app.ts'
|
||||
|
||||
const calendarRef = ref()
|
||||
const travelStore = useTravelStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
onMounted(() => {
|
||||
const { month } = travelStore.queryRecord
|
||||
calendarRef.value.move(new Date(month))
|
||||
})
|
||||
|
||||
watch(() => travelStore.isRefresh, () => {
|
||||
updateInfo()
|
||||
})
|
||||
|
||||
const dailyInfo = reactive({
|
||||
calendarDate: '',
|
||||
attrs: [] as any[]
|
||||
})
|
||||
|
||||
const updateInfo = () => {
|
||||
getCalendarMoveDate()
|
||||
getCalendarAttrDates()
|
||||
}
|
||||
|
||||
const getCalendarMoveDate = () => {
|
||||
const recordLength = travelStore.travelRecordList.length
|
||||
if (recordLength > 0) {
|
||||
const today = formatDate(new Date())
|
||||
if (travelStore.travelRecordList.some((item) => item.date === today)) {
|
||||
dailyInfo.calendarDate = today
|
||||
} else {
|
||||
dailyInfo.calendarDate = formatDate(travelStore.travelRecordList[recordLength - 1].date)
|
||||
}
|
||||
} else {
|
||||
dailyInfo.calendarDate = ''
|
||||
}
|
||||
}
|
||||
|
||||
const getCalendarAttrDates = () => {
|
||||
dailyInfo.attrs = []
|
||||
dailyInfo.attrs.push({
|
||||
key: 'today',
|
||||
highlight: true,
|
||||
dates: new Date()
|
||||
})
|
||||
|
||||
const dateList = travelStore.travelRecordList.map((item) => item.date)
|
||||
dailyInfo.attrs.push({
|
||||
dot: 'red',
|
||||
dates: [...new Set(dateList)]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择日期事件
|
||||
* @param day 日期
|
||||
*/
|
||||
const dateClick = (day: CalendarDay) => {
|
||||
dailyInfo.calendarDate = formatDate(day.date)
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择月份事件
|
||||
*/
|
||||
const changeMonthClick = async (value) => {
|
||||
travelStore.queryRecord.month = value[0].id
|
||||
await travelStore.refreshInfo()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.travel-daily {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1vh;
|
||||
|
||||
.travel-calendar {
|
||||
width: 100%;
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
119
src/components/Travel/TravelCard.vue
Normal file
119
src/components/Travel/TravelCard.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div class="life-record-card card-item">
|
||||
<el-carousel v-if="props.travelItem?.recordList.length > 0"
|
||||
arrow="always" @change="changeTravelEvent">
|
||||
<el-carousel-item v-for="(item, index) in props.travelItem?.recordList" :key="index">
|
||||
<el-image :src="serviceFileRootPath + item.imageList[0]"
|
||||
fit="contain" alt="暂无图片">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<span>暂无图片</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
|
||||
<el-empty v-else description="暂无记录"/>
|
||||
|
||||
<div class="record-info">
|
||||
<div class="top-container">
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<span class="title">{{ props.travelItem.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-info">
|
||||
<el-button type="primary" :icon="Edit" circle :size="'small'"
|
||||
@click="editClick"/>
|
||||
<el-button type="primary" :icon="MoreFilled" circle :size="'small'"
|
||||
@click="viewClick()"/>
|
||||
<el-button type="danger" :icon="DeleteFilled" circle :size="'small'"
|
||||
@click="deleteClick()"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-container">
|
||||
<div class="info-list">
|
||||
<div class="info-item location-item">
|
||||
<i class="fa-solid fa-location-dot"></i>
|
||||
{{ props.travelItem.area.join('-') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="props.travelItem?.recordList.length > 0" class="info-list">
|
||||
<div class="info-item">
|
||||
<i class="fa-solid fa-flag"></i>
|
||||
{{ parsePerson(props.travelItem.recordList[cardInfo.currentIndex].persons) }}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<i class="fa-regular fa-calendar-check"></i>
|
||||
{{ formatDate(props.travelItem.recordList[cardInfo.currentIndex].date) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<span>暂无记录</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Edit, MoreFilled, DeleteFilled } from '@element-plus/icons-vue'
|
||||
import { useTravelStore } from '@/store/travel.ts'
|
||||
import { useAppStore } from '@/store/app.ts'
|
||||
import { defineProps, PropType, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { formatDate } from 'vue3-common/utils/dateUtil'
|
||||
import { ITravelSpot } from '@/types/travel.ts'
|
||||
import { commonElMessageBox } from 'vue3-common/utils/elUtil'
|
||||
import { serviceFileRootPath } from '@/utils'
|
||||
import { parsePerson } from '@/utils/home/travelUtil.ts'
|
||||
|
||||
const router = useRouter()
|
||||
const travelStore = useTravelStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const cardInfo = reactive({
|
||||
currentIndex: 0
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
travelItem: {
|
||||
required: true,
|
||||
type: Object as PropType<ITravelSpot>
|
||||
}
|
||||
})
|
||||
|
||||
const editClick = async () => {
|
||||
const id = props.travelItem?.id
|
||||
travelStore.travelSpotApiType = 'UPDATE'
|
||||
await travelStore.queryTravelSpot(id as number)
|
||||
|
||||
appStore.isShowMobileBack = true
|
||||
await router.push({ name: 'TravelSpotForm' })
|
||||
}
|
||||
|
||||
const viewClick = async () => {
|
||||
appStore.isShowMobileBack = true
|
||||
await router.push({ name: 'TravelDetailId', params: { id: props.travelItem?.id } })
|
||||
}
|
||||
|
||||
const deleteClick = () => {
|
||||
travelStore.travelSpotApiType = 'DELETE'
|
||||
commonElMessageBox(`请确认是否删除该旅行: ${props.travelItem?.name}?`).then(() => {
|
||||
travelStore.handleTravelSpotApi(props.travelItem)
|
||||
})
|
||||
}
|
||||
|
||||
const changeTravelEvent = (current: number) => {
|
||||
cardInfo.currentIndex = current
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/styles/home/life.record.module";
|
||||
</style>
|
||||
104
src/components/Travel/TravelDailyItem.vue
Normal file
104
src/components/Travel/TravelDailyItem.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<el-empty v-if="dailyItemInfo.itemList.length === 0" />
|
||||
|
||||
<div v-else class="travel-daily-item card-item">
|
||||
<daily-summary :date="dailyItemInfo.date"/>
|
||||
|
||||
<div class="daily-list">
|
||||
<daily-card v-for="(item, index) in dailyItemInfo.itemList"
|
||||
:key="index" @click="selectTravelRecordClick(item)"
|
||||
:title="item.name" :content="item.area.join('-')">
|
||||
<template #icon>
|
||||
<el-avatar>{{ item.area[0].charAt(0) }}</el-avatar>
|
||||
</template>
|
||||
|
||||
<template #extra>
|
||||
<span>{{ parsePerson(item.persons)}}</span>
|
||||
<span>{{ item.date }}</span>
|
||||
</template>
|
||||
</daily-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DailySummary from '@/components/DailySummary.vue'
|
||||
import DailyCard from '@/components/DailyCard.vue'
|
||||
import { defineProps, onMounted, reactive, watch } from 'vue'
|
||||
import { useTravelStore } from '@/store/travel.ts'
|
||||
import { ITravelRecord } from '@/types/travel.ts'
|
||||
import { formatChineseDate } from 'vue3-common/utils/dateUtil'
|
||||
import { useAppStore } from '@/store/app.ts'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { parsePerson } from '@/utils/home/travelUtil.ts'
|
||||
|
||||
const travelStore = useTravelStore()
|
||||
const appStore = useAppStore()
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
date: {
|
||||
required: true,
|
||||
type: String
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.date, () => {
|
||||
getTravelDailyByDate(props.date as string)
|
||||
})
|
||||
|
||||
watch(() => travelStore.isRefresh, () => {
|
||||
getTravelDailyByDate(props.date as string)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getTravelDailyByDate(props.date as string)
|
||||
})
|
||||
|
||||
const dailyItemInfo = reactive({
|
||||
date: '',
|
||||
itemList: [] as ITravelRecord[]
|
||||
})
|
||||
|
||||
/**
|
||||
* 根据日期获取当天的美食记录
|
||||
* @param date
|
||||
*/
|
||||
const getTravelDailyByDate = (date: string) => {
|
||||
if (date) {
|
||||
dailyItemInfo.date = formatChineseDate(date, 'MM月DD日 dddd')
|
||||
dailyItemInfo.itemList = []
|
||||
travelStore.travelRecordList.forEach((item) => {
|
||||
if (item.date === date) {
|
||||
dailyItemInfo.itemList.push(item)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
dailyItemInfo.itemList = []
|
||||
}
|
||||
}
|
||||
|
||||
const selectTravelRecordClick = async (travelRecord: ITravelRecord) => {
|
||||
travelStore.travelRecordApiType = 'UPDATE'
|
||||
|
||||
appStore.isShowMobileBack = true
|
||||
travelStore.currentTravelRecord = travelRecord
|
||||
await router.push({ name: 'TravelRecordForm' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.travel-daily-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1vh;
|
||||
padding: 1vh 0.5vw;
|
||||
|
||||
.daily-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1vh;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
57
src/components/Travel/TravelDailyList.vue
Normal file
57
src/components/Travel/TravelDailyList.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="travel-daily-list">
|
||||
<el-date-picker v-model="travelStore.queryRecord.year"
|
||||
type="year" :editable="false" style="width: 120px;"
|
||||
@change="travelStore.refreshInfo()"/>
|
||||
|
||||
<el-empty v-if="travelDailyInfo.dateList.length === 0"/>
|
||||
|
||||
<el-scrollbar>
|
||||
<travel-daily-item v-for="(item, index) in travelDailyInfo.dateList"
|
||||
:key="index" :date="item"/>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TravelDailyItem from '@/components/Travel/TravelDailyItem.vue'
|
||||
import { reactive, watch } from 'vue'
|
||||
import { useTravelStore } from '@/store/travel.ts'
|
||||
|
||||
const travelStore = useTravelStore()
|
||||
|
||||
watch(() => travelStore.isRefresh, () => {
|
||||
updateInfo()
|
||||
})
|
||||
|
||||
const travelDailyInfo = reactive({
|
||||
dateList: [] as string[]
|
||||
})
|
||||
|
||||
const updateInfo = () => {
|
||||
const dateList = travelStore.travelRecordList.map((item) => item.date)
|
||||
travelDailyInfo.dateList = [...new Set(dateList)] as string[]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.travel-daily-list {
|
||||
height: calc(100vh - 120px);
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1vh;
|
||||
|
||||
.el-date-editor {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.el-scrollbar {
|
||||
.el-scrollbar__view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
122
src/components/Travel/TravelDialog.vue
Normal file
122
src/components/Travel/TravelDialog.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<el-dialog v-model="dialogData.visible" :title="dialogData.title"
|
||||
center @open="openDialogEvent" width="50%" class="travel-dialog">
|
||||
<el-form ref="formRef" :model="dialogData.data" :rules=rules label-width="100px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="景点名称" prop="name">
|
||||
<el-input
|
||||
v-model="dialogData.data.name"
|
||||
placeholder="请输入景点名称" clearable
|
||||
show-word-limit maxlength="20"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="景点地区" prop="area">
|
||||
<el-cascader
|
||||
v-model="dialogData.data.area"
|
||||
placeholder="请选择景点地区" :options="pcTextArr">
|
||||
</el-cascader>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="景点地址" prop="address">
|
||||
<el-input
|
||||
v-model="dialogData.data.address"
|
||||
placeholder="请输入景点地址" clearable
|
||||
show-word-limit maxlength="50"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="门票价格" prop="price">
|
||||
<el-input
|
||||
v-model="dialogData.data.price"
|
||||
placeholder="请输入门票价格" clearable
|
||||
:formatter="(value: string) => `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')"
|
||||
:parser="(value: string) => value.replace(/\$\s?|(,*)/g, '')"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="推荐指数" prop="recommendRate">
|
||||
<el-rate v-model="dialogData.data.recommendRate"
|
||||
:colors="travelStore.rateColorList"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="dialogData.data.remark" type="textarea"
|
||||
placeholder="请输入备注" clearable
|
||||
:rows="5" show-word-limit maxlength="200">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="交通指南">
|
||||
<el-input v-model="dialogData.data.traffic" type="textarea"
|
||||
placeholder="请输入交通指南" clearable
|
||||
:rows="5" show-word-limit maxlength="200">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="旅游路线">
|
||||
<el-input v-model="dialogData.data.router" type="textarea"
|
||||
placeholder="请输入旅游路线" clearable
|
||||
:rows="5" show-word-limit maxlength="200">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="美食推荐">
|
||||
<multi-input :data="dialogData.data.foods" :size="5"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="项目推荐">
|
||||
<multi-input :data="dialogData.data.projects" :size="5"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancelClick">取消</el-button>
|
||||
<el-button type="primary" @click="confirmClick">保存</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useTravelStore } from '@/store/travel.ts'
|
||||
import { ref, computed, reactive, nextTick } from 'vue'
|
||||
import { MultiInput } from 'vue3-common'
|
||||
import { pcTextArr } from 'element-china-area-data'
|
||||
import { FormRules } from 'element-plus'
|
||||
import { travelRules } from '@/utils/element/elRules.ts'
|
||||
|
||||
const rules = reactive<FormRules>(travelRules)
|
||||
const formRef = ref()
|
||||
const travelStore = useTravelStore()
|
||||
|
||||
const dialogData = computed(() => travelStore.travelSpotDialog)
|
||||
|
||||
/**
|
||||
* 打开对话框事件
|
||||
*/
|
||||
const openDialogEvent = async () => {
|
||||
await nextTick(() => {
|
||||
formRef.value?.clearValidate()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击确认按钮
|
||||
*/
|
||||
const confirmClick = () => {
|
||||
formRef.value.validate().then(() => {
|
||||
travelStore.handleTravelSpotApi(travelStore.travelSpotDialog.data)
|
||||
})
|
||||
}
|
||||
|
||||
const cancelClick = () => {
|
||||
dialogData.value.visible = false
|
||||
}
|
||||
</script>
|
||||
68
src/components/Travel/TravelQuery.vue
Normal file
68
src/components/Travel/TravelQuery.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div v-if="isMobile()" class="mobile-common-query card-item">
|
||||
<div class="query-item">
|
||||
<span>城市:</span>
|
||||
<el-segmented v-model="travelStore.querySpot.category"
|
||||
:options="queryCategoryList"
|
||||
@change="queryTravelSegmented"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="common-query">
|
||||
<el-select v-model="travelStore.queryDateType"
|
||||
@change="travelStore.refreshInfo()"
|
||||
style="width: 120px;align-self: center;">
|
||||
<el-option v-for="(item, index) in dateTypeOptions" :key="index"
|
||||
:label="item.label" :value="item.value"/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTravelStore } from '@/store/travel.ts'
|
||||
import { isMobile } from 'vue3-common/utils/layoutUtil'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ICategory } from '@/types'
|
||||
|
||||
const travelStore = useTravelStore()
|
||||
|
||||
const queryCategoryList = ref([] as ICategory[])
|
||||
|
||||
const dateTypeOptions = [{
|
||||
label: '按月选择',
|
||||
value: 'month'
|
||||
},
|
||||
{
|
||||
label: '按年选择',
|
||||
value: 'year'
|
||||
}]
|
||||
|
||||
onMounted(async () => {
|
||||
await travelStore.queryTravelCategory()
|
||||
initCategoryList()
|
||||
})
|
||||
|
||||
const initCategoryList = () => {
|
||||
queryCategoryList.value = []
|
||||
queryCategoryList.value.push({
|
||||
label: '全部',
|
||||
value: ''
|
||||
})
|
||||
travelStore.travelCategoryList.forEach((item) => {
|
||||
queryCategoryList.value.push({
|
||||
label: item,
|
||||
value: item
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const queryTravelSegmented = async () => {
|
||||
travelStore.pageInfo = {
|
||||
currentPage: 1,
|
||||
pageSize: 3,
|
||||
totalSize: 1
|
||||
}
|
||||
travelStore.travelSpotList = []
|
||||
await travelStore.queryMobileTravelSpotList()
|
||||
}
|
||||
</script>
|
||||
39
src/components/Travel/TravelSpotList.vue
Normal file
39
src/components/Travel/TravelSpotList.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="travel-spot-list card-item">
|
||||
<el-empty v-if="travelStore.travelSpotList.length === 0" description="暂无旅行记录"/>
|
||||
|
||||
<div v-else class="list">
|
||||
<travel-card v-for="(item, index) in travelStore.travelSpotList"
|
||||
:key="index" :travel-item="item"/>
|
||||
</div>
|
||||
|
||||
<el-pagination
|
||||
v-model="travelStore.pageInfo.currentPage"
|
||||
:page-size="travelStore.pageInfo.pageSize"
|
||||
layout="total, prev, pager, next"
|
||||
:total="travelStore.pageInfo.totalSize"
|
||||
/>
|
||||
|
||||
<el-button v-if="isLargeScreen()"
|
||||
type="primary" :icon="Plus" circle
|
||||
@click="addTravelSpotClick" class="add-new" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import TravelCard from '@/components/Travel/TravelCard.vue'
|
||||
import { useTravelStore } from '@/store/travel.ts'
|
||||
import { isLargeScreen } from '@/utils'
|
||||
|
||||
const travelStore = useTravelStore()
|
||||
|
||||
const addTravelSpotClick = () => {
|
||||
travelStore.travelSpotApiType = 'ADD'
|
||||
travelStore.travelSpotDialog = {
|
||||
title: '新增景点',
|
||||
visible: true,
|
||||
data: travelStore.getEmptyTravelSpot()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
26
src/components/Travel/TravelStats.vue
Normal file
26
src/components/Travel/TravelStats.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="common-statistics">
|
||||
<stats-card
|
||||
title="景点总数" color="#CA6C65" unit="个" icon="iconfont icon-caipu"
|
||||
:value="travelStore.travelStats.spotCount" />
|
||||
|
||||
<stats-card
|
||||
title="旅行城市" color="#6FBB69" unit="个" icon="iconfont icon-leibie"
|
||||
:value="travelStore.travelStats.cityCount" />
|
||||
|
||||
<stats-card
|
||||
title="旅行次数" color="#E4BF62" unit="次" icon="iconfont icon-cishu"
|
||||
:value="travelStore.travelStats.recordCount" />
|
||||
|
||||
<stats-card
|
||||
title="平均次数" color="#5470C6" unit="次/月" icon="iconfont icon-pinlv"
|
||||
:value="1" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import StatsCard from '@/components/StatsCard.vue'
|
||||
import { useTravelStore } from '@/store/travel.ts'
|
||||
|
||||
const travelStore = useTravelStore()
|
||||
</script>
|
||||
Reference in New Issue
Block a user