894 lines
27 KiB
Vue
894 lines
27 KiB
Vue
<template>
|
||
<view class="travel-page">
|
||
<view class="page-hero">
|
||
<AppNavbar title="差旅报销" show-back/>
|
||
<view class="intro-card">
|
||
<view class="card-title">
|
||
<view class="title-bar"></view>
|
||
<text>差旅报销</text>
|
||
</view>
|
||
<text class="intro-text">填写出差时间、出发地和目的地,上传凭证后进入审核。</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card block">
|
||
<view class="section-title">新增报销</view>
|
||
<view class="form">
|
||
<view class="form-row">
|
||
<text>开始时间</text>
|
||
<view class="picker-value" @tap="openDatePicker('startDate')">{{ travelForm.startDate || '点击选择' }}</view>
|
||
</view>
|
||
<view class="form-row">
|
||
<text>结束时间</text>
|
||
<view class="picker-value" @tap="openDatePicker('endDate')">{{ travelForm.endDate || '点击选择' }}</view>
|
||
</view>
|
||
<view class="form-row">
|
||
<text>出发地</text>
|
||
<view class="picker-value" :class="{placeholder: !travelForm.fromProvince}" @tap="openRegionPicker('from')">
|
||
{{ fromRegionText }}
|
||
</view>
|
||
</view>
|
||
<view class="form-row">
|
||
<text>目的地</text>
|
||
<view class="picker-value" :class="{placeholder: !travelForm.toProvince}" @tap="openRegionPicker('to')">
|
||
{{ toRegionText }}
|
||
</view>
|
||
</view>
|
||
<view class="form-row">
|
||
<text>交通费</text>
|
||
<input type="number" v-model="travelForm.transportCosts" placeholder="元"/>
|
||
</view>
|
||
<view class="form-row">
|
||
<text>住宿费</text>
|
||
<input type="number" v-model="travelForm.liveCosts" placeholder="元"/>
|
||
</view>
|
||
<view class="form-row">
|
||
<text>其他费用</text>
|
||
<input type="number" v-model="travelForm.otherCosts" placeholder="元"/>
|
||
</view>
|
||
<view class="form-row textarea-row">
|
||
<text>备注</text>
|
||
<textarea
|
||
v-model="travelForm.remark"
|
||
class="remark-input"
|
||
maxlength="200"
|
||
auto-height
|
||
placeholder="请输入备注(选填)"
|
||
/>
|
||
</view>
|
||
<view class="upload" @tap="uploadProof">
|
||
<view>
|
||
<view class="upload-title">出差凭证</view>
|
||
<view class="upload-meta">{{ images.length ? '已上传,点击继续上传' : '点击上传照片' }}</view>
|
||
</view>
|
||
<view class="chip">必填</view>
|
||
</view>
|
||
<view v-if="images.length" class="proof-list">
|
||
<view class="proof-item" v-for="(img, idx) in images" :key="`${img}-${idx}`">
|
||
<image class="proof-image" :src="img" mode="aspectFill"/>
|
||
<view class="proof-remove" @tap.stop="removeProof(idx)">x</view>
|
||
</view>
|
||
</view>
|
||
<view class="primary-btn submit" @tap="submitTravel">提交报销</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card block">
|
||
<view class="section-title">历史报销</view>
|
||
<view v-if="loading" class="empty">加载中...</view>
|
||
<view v-else-if="travelHistory.length === 0" class="empty">暂无记录</view>
|
||
<view v-else>
|
||
<view class="history" v-for="item in travelHistory" :key="item.id">
|
||
<view class="history-head" @tap="toggleHistoryDetail(item.id)">
|
||
<view class="history-main">
|
||
<view class="history-title">{{ item.fromText }} → {{ item.toText }}</view>
|
||
<view class="history-meta">{{ item.start }} 至 {{ item.end }}</view>
|
||
</view>
|
||
<view class="history-side">
|
||
<view class="history-status" :class="{'pending': item.status === '审核中', 'reject': item.status === '已驳回'}">
|
||
{{ item.status }}
|
||
</view>
|
||
<u-icon name="arrow-right" size="24" color="#9ca3af" :class="['history-arrow', item.expanded ? 'open' : '']"/>
|
||
</view>
|
||
</view>
|
||
<view v-if="item.expanded" class="history-detail">
|
||
<view class="history-meta">交通 ¥{{ item.traffic }} · 住宿 ¥{{ item.hotel }}</view>
|
||
<view class="history-meta">其他 ¥{{ item.other }}</view>
|
||
<view class="history-meta">备注:{{ item.remark || '-' }}</view>
|
||
<view class="history-meta">凭证:</view>
|
||
<view v-if="item.proofs && item.proofs.length" class="history-proof-list">
|
||
<view class="history-proof-item" v-for="(proof, proofIdx) in item.proofs" :key="`${item.id}-proof-${proofIdx}`">
|
||
<image
|
||
class="history-proof-image"
|
||
:src="proof"
|
||
mode="aspectFill"
|
||
@tap="previewHistoryProof(item.proofs, proofIdx)"
|
||
/>
|
||
</view>
|
||
</view>
|
||
<view v-else class="history-meta">-</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<u-datetime-picker
|
||
:show="showStartPicker"
|
||
:value="startPickerValue"
|
||
mode="date"
|
||
@confirm="e => onDateConfirm('startDate', e)"
|
||
@cancel="closeDatePicker('startDate')"
|
||
@close="closeDatePicker('startDate')"
|
||
/>
|
||
<u-datetime-picker
|
||
:show="showEndPicker"
|
||
:value="endPickerValue"
|
||
mode="date"
|
||
@confirm="e => onDateConfirm('endDate', e)"
|
||
@cancel="closeDatePicker('endDate')"
|
||
@close="closeDatePicker('endDate')"
|
||
/>
|
||
|
||
<view v-if="showRegionPicker" class="form-popup-mask" @tap="cancelRegionPicker"></view>
|
||
<view v-if="showRegionPicker" class="region-picker-popup card" @tap.stop>
|
||
<view class="region-picker-head">
|
||
<view class="region-picker-title">选择{{ activeRegionField === 'from' ? '出发地' : '目的地' }}</view>
|
||
<view class="region-picker-actions">
|
||
<view class="region-picker-btn" @tap="cancelRegionPicker">取消</view>
|
||
<view class="region-picker-btn primary" @tap="confirmRegionPicker">确定</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="areaLoading" class="region-loading">加载中...</view>
|
||
<picker-view v-else class="picker-view" :value="areaIndex" @change="onAreaPickerChange">
|
||
<picker-view-column>
|
||
<view v-for="item in areaColumns.provinces" :key="item.id" class="picker-item">{{ item.name }}</view>
|
||
</picker-view-column>
|
||
<picker-view-column>
|
||
<view v-for="item in areaColumns.cities" :key="item.id" class="picker-item">{{ item.name }}</view>
|
||
</picker-view-column>
|
||
<picker-view-column>
|
||
<view v-for="item in areaColumns.regions" :key="item.id" class="picker-item">{{ item.name }}</view>
|
||
</picker-view-column>
|
||
</picker-view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import {reimburseAddReq, reimburseUserListReq} from "@/api/reimburse";
|
||
import {areaListByFidReq} from "@/api/common";
|
||
import {chooseImg} from "@/util";
|
||
import dayjs from "dayjs";
|
||
import AppNavbar from "@/components/AppNavbar.vue";
|
||
|
||
export default {
|
||
components: {AppNavbar},
|
||
data() {
|
||
return {
|
||
travelForm: this.buildForm(),
|
||
images: [],
|
||
travelHistory: [],
|
||
loading: false,
|
||
showStartPicker: false,
|
||
showEndPicker: false,
|
||
showRegionPicker: false,
|
||
activeRegionField: 'from',
|
||
areaLoading: false,
|
||
areaIndex: [0, 0, 0],
|
||
areaColumns: {
|
||
provinces: [],
|
||
cities: [],
|
||
regions: []
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
startPickerValue() {
|
||
return this.travelForm.startDate || Date.now()
|
||
},
|
||
endPickerValue() {
|
||
return this.travelForm.endDate || Date.now()
|
||
},
|
||
fromRegionText() {
|
||
return this.composeRegionText(this.travelForm.fromProvince, this.travelForm.fromCity, this.travelForm.fromRegion) || '请选择出发地'
|
||
},
|
||
toRegionText() {
|
||
return this.composeRegionText(this.travelForm.toProvince, this.travelForm.toCity, this.travelForm.toRegion) || '请选择目的地'
|
||
}
|
||
},
|
||
async onShow() {
|
||
await this.ensureDefaultProvince()
|
||
await this.fetchHistory()
|
||
},
|
||
methods: {
|
||
buildForm() {
|
||
return {
|
||
startDate: '',
|
||
endDate: '',
|
||
fromProvince: '',
|
||
fromCity: '',
|
||
fromRegion: '',
|
||
toProvince: '',
|
||
toCity: '',
|
||
toRegion: '',
|
||
transportCosts: '',
|
||
liveCosts: '',
|
||
otherCosts: '',
|
||
remark: '',
|
||
images: ''
|
||
}
|
||
},
|
||
composeRegionText(province, city, region) {
|
||
return [province, city, region].filter(Boolean).join(' ')
|
||
},
|
||
getFieldArea(field) {
|
||
if (field === 'from') {
|
||
return {
|
||
province: this.travelForm.fromProvince,
|
||
city: this.travelForm.fromCity,
|
||
region: this.travelForm.fromRegion
|
||
}
|
||
}
|
||
return {
|
||
province: this.travelForm.toProvince,
|
||
city: this.travelForm.toCity,
|
||
region: this.travelForm.toRegion
|
||
}
|
||
},
|
||
setFieldArea(field, province, city, region) {
|
||
if (field === 'from') {
|
||
this.travelForm.fromProvince = province || ''
|
||
this.travelForm.fromCity = city || ''
|
||
this.travelForm.fromRegion = region || ''
|
||
return
|
||
}
|
||
this.travelForm.toProvince = province || ''
|
||
this.travelForm.toCity = city || ''
|
||
this.travelForm.toRegion = region || ''
|
||
},
|
||
normalizeAreaItem(item) {
|
||
return {
|
||
id: item.areaId || item.code || item.value || item.id || null,
|
||
name: item.name || item.title || item.areaName || item.label || ''
|
||
}
|
||
},
|
||
async fetchAreaListByFid(fid) {
|
||
const {data} = await areaListByFidReq({fid})
|
||
const list = Array.isArray(data) ? data : (data && data.list ? data.list : [])
|
||
return list.map(this.normalizeAreaItem)
|
||
},
|
||
findGuangxiProvince(provinces = []) {
|
||
return provinces.find(item => String(item.name || '').includes('广西')) || null
|
||
},
|
||
async buildDefaultArea(provinces = [], preferredProvinceName = '') {
|
||
let province = null
|
||
if (preferredProvinceName) {
|
||
province = provinces.find(item => item.name === preferredProvinceName)
|
||
}
|
||
if (!province) {
|
||
province = this.findGuangxiProvince(provinces) || provinces[0] || null
|
||
}
|
||
if (!province) {
|
||
return {province: '', city: '', region: ''}
|
||
}
|
||
const cities = await this.fetchAreaListByFid(province.id)
|
||
const city = cities[0] || null
|
||
const regions = city ? await this.fetchAreaListByFid(city.id) : []
|
||
const region = regions[0] || null
|
||
return {
|
||
province: province.name || '',
|
||
city: city ? city.name : '',
|
||
region: region ? region.name : ''
|
||
}
|
||
},
|
||
async ensureDefaultProvince() {
|
||
if (
|
||
this.travelForm.fromProvince && this.travelForm.fromCity && this.travelForm.fromRegion &&
|
||
this.travelForm.toProvince && this.travelForm.toCity && this.travelForm.toRegion
|
||
) {
|
||
return
|
||
}
|
||
try {
|
||
const provinces = await this.fetchAreaListByFid(0)
|
||
const fromDefault = await this.buildDefaultArea(provinces, this.travelForm.fromProvince)
|
||
const toDefault = await this.buildDefaultArea(provinces, this.travelForm.toProvince)
|
||
if (!this.travelForm.fromProvince) this.travelForm.fromProvince = fromDefault.province
|
||
if (!this.travelForm.fromCity) this.travelForm.fromCity = fromDefault.city
|
||
if (!this.travelForm.fromRegion) this.travelForm.fromRegion = fromDefault.region
|
||
if (!this.travelForm.toProvince) this.travelForm.toProvince = toDefault.province
|
||
if (!this.travelForm.toCity) this.travelForm.toCity = toDefault.city
|
||
if (!this.travelForm.toRegion) this.travelForm.toRegion = toDefault.region
|
||
} catch (err) {
|
||
if (!this.travelForm.fromProvince) this.travelForm.fromProvince = '广西壮族自治区'
|
||
if (!this.travelForm.fromCity) this.travelForm.fromCity = ''
|
||
if (!this.travelForm.fromRegion) this.travelForm.fromRegion = ''
|
||
if (!this.travelForm.toProvince) this.travelForm.toProvince = '广西壮族自治区'
|
||
if (!this.travelForm.toCity) this.travelForm.toCity = ''
|
||
if (!this.travelForm.toRegion) this.travelForm.toRegion = ''
|
||
}
|
||
},
|
||
async initAreaColumns(field) {
|
||
this.areaLoading = true
|
||
try {
|
||
const provinces = await this.fetchAreaListByFid(0)
|
||
this.areaColumns.provinces = provinces
|
||
const current = this.getFieldArea(field)
|
||
const defaultProvince = this.findGuangxiProvince(provinces)
|
||
let provinceIndex = provinces.findIndex(item => item.name === current.province)
|
||
if (provinceIndex < 0 && current.province) {
|
||
provinceIndex = provinces.findIndex(item => current.province.includes(item.name) || item.name.includes(current.province))
|
||
}
|
||
if (provinceIndex < 0 && defaultProvince) {
|
||
provinceIndex = provinces.findIndex(item => item.id === defaultProvince.id)
|
||
}
|
||
if (provinceIndex < 0) provinceIndex = 0
|
||
const province = provinces[provinceIndex]
|
||
const cities = province ? await this.fetchAreaListByFid(province.id) : []
|
||
this.areaColumns.cities = cities
|
||
let cityIndex = cities.findIndex(item => item.name === current.city)
|
||
if (cityIndex < 0) cityIndex = 0
|
||
const city = cities[cityIndex]
|
||
const regions = city ? await this.fetchAreaListByFid(city.id) : []
|
||
this.areaColumns.regions = regions
|
||
let regionIndex = regions.findIndex(item => item.name === current.region)
|
||
if (regionIndex < 0) regionIndex = 0
|
||
this.areaIndex = [provinceIndex, cityIndex, regionIndex]
|
||
} catch (err) {
|
||
uni.showToast({
|
||
title: '地区加载失败',
|
||
icon: 'none'
|
||
})
|
||
} finally {
|
||
this.areaLoading = false
|
||
}
|
||
},
|
||
async updateCitiesByProvince(provinceIndex) {
|
||
const province = this.areaColumns.provinces[provinceIndex]
|
||
this.areaColumns.cities = province ? await this.fetchAreaListByFid(province.id) : []
|
||
this.areaColumns.regions = []
|
||
this.areaIndex = [provinceIndex, 0, 0]
|
||
await this.updateRegionsByCity(provinceIndex, 0)
|
||
},
|
||
async updateRegionsByCity(provinceIndex, cityIndex) {
|
||
const city = this.areaColumns.cities[cityIndex]
|
||
this.areaColumns.regions = city ? await this.fetchAreaListByFid(city.id) : []
|
||
this.areaIndex = [provinceIndex, cityIndex, 0]
|
||
},
|
||
async openRegionPicker(field) {
|
||
this.activeRegionField = field
|
||
this.showRegionPicker = true
|
||
await this.initAreaColumns(field)
|
||
},
|
||
cancelRegionPicker() {
|
||
this.showRegionPicker = false
|
||
},
|
||
confirmRegionPicker() {
|
||
const [pIndex, cIndex, rIndex] = this.areaIndex
|
||
const province = this.areaColumns.provinces[pIndex]
|
||
const city = this.areaColumns.cities[cIndex]
|
||
const region = this.areaColumns.regions[rIndex]
|
||
this.setFieldArea(this.activeRegionField, province ? province.name : '', city ? city.name : '', region ? region.name : '')
|
||
this.showRegionPicker = false
|
||
},
|
||
async onAreaPickerChange(e) {
|
||
const value = e.detail.value || [0, 0, 0]
|
||
const [pIndex, cIndex] = value
|
||
const [prevP, prevC] = this.areaIndex
|
||
if (pIndex !== prevP) {
|
||
await this.updateCitiesByProvince(pIndex)
|
||
return
|
||
}
|
||
if (cIndex !== prevC) {
|
||
await this.updateRegionsByCity(pIndex, cIndex)
|
||
return
|
||
}
|
||
this.areaIndex = value
|
||
},
|
||
async uploadProof() {
|
||
const remain = 9 - this.images.length
|
||
if (remain <= 0) return
|
||
const res = await chooseImg(remain)
|
||
this.images = [
|
||
...this.images,
|
||
...res.map(i => i.path)
|
||
]
|
||
},
|
||
removeProof(idx) {
|
||
this.images.splice(idx, 1)
|
||
},
|
||
async submitTravel() {
|
||
if (
|
||
!this.travelForm.startDate ||
|
||
!this.travelForm.endDate ||
|
||
!this.travelForm.fromProvince || !this.travelForm.fromCity || !this.travelForm.fromRegion ||
|
||
!this.travelForm.toProvince || !this.travelForm.toCity || !this.travelForm.toRegion ||
|
||
!this.images.length
|
||
) {
|
||
uni.showToast({
|
||
title: '请填写必填项并上传凭证',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
const start = dayjs(this.travelForm.startDate)
|
||
const end = dayjs(this.travelForm.endDate)
|
||
if (start.isValid() && end.isValid() && end.isBefore(start, 'day')) {
|
||
uni.showToast({
|
||
title: '结束时间不能早于开始时间',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
const fromText = this.composeRegionText(this.travelForm.fromProvince, this.travelForm.fromCity, this.travelForm.fromRegion)
|
||
const toText = this.composeRegionText(this.travelForm.toProvince, this.travelForm.toCity, this.travelForm.toRegion)
|
||
try {
|
||
await reimburseAddReq({
|
||
startDate: this.travelForm.startDate,
|
||
endDate: this.travelForm.endDate,
|
||
fromProvince: this.travelForm.fromProvince,
|
||
fromCity: this.travelForm.fromCity,
|
||
fromRegion: this.travelForm.fromRegion,
|
||
departureProvince: this.travelForm.fromProvince,
|
||
departureCity: this.travelForm.fromCity,
|
||
departureRegion: this.travelForm.fromRegion,
|
||
departure: fromText,
|
||
toProvince: this.travelForm.toProvince,
|
||
toCity: this.travelForm.toCity,
|
||
toRegion: this.travelForm.toRegion,
|
||
destinationProvince: this.travelForm.toProvince,
|
||
destinationCity: this.travelForm.toCity,
|
||
destinationRegion: this.travelForm.toRegion,
|
||
destination: toText,
|
||
city: toText,
|
||
transportCosts: this.travelForm.transportCosts,
|
||
liveCosts: this.travelForm.liveCosts,
|
||
otherCosts: this.travelForm.otherCosts,
|
||
remark: this.travelForm.remark,
|
||
images: JSON.stringify(this.images)
|
||
})
|
||
uni.showToast({
|
||
title: '报销已提交审核',
|
||
icon: 'success'
|
||
})
|
||
const keepFromProvince = this.travelForm.fromProvince
|
||
const keepFromCity = this.travelForm.fromCity
|
||
const keepFromRegion = this.travelForm.fromRegion
|
||
const keepToProvince = this.travelForm.toProvince
|
||
const keepToCity = this.travelForm.toCity
|
||
const keepToRegion = this.travelForm.toRegion
|
||
this.travelForm = this.buildForm()
|
||
this.travelForm.fromProvince = keepFromProvince
|
||
this.travelForm.fromCity = keepFromCity
|
||
this.travelForm.fromRegion = keepFromRegion
|
||
this.travelForm.toProvince = keepToProvince
|
||
this.travelForm.toCity = keepToCity
|
||
this.travelForm.toRegion = keepToRegion
|
||
this.images = []
|
||
await this.fetchHistory()
|
||
} catch (err) {
|
||
uni.showToast({
|
||
title: '提交失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
},
|
||
async fetchHistory() {
|
||
this.loading = true
|
||
try {
|
||
const {data} = await reimburseUserListReq()
|
||
const list = Array.isArray(data) ? data : (data && data.list ? data.list : [])
|
||
this.travelHistory = list.map(this.normalizeHistory)
|
||
} catch (err) {
|
||
uni.showToast({
|
||
title: '加载失败',
|
||
icon: 'none'
|
||
})
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
normalizeHistory(item) {
|
||
const statusValue = Number(item.status)
|
||
let statusLabel = '待审核'
|
||
if (statusValue === 0) statusLabel = '审核中'
|
||
if (statusValue === 1) statusLabel = '已通过'
|
||
if (statusValue === 2) statusLabel = '已驳回'
|
||
const fromProvince = item.fromProvince || item.departureProvince || item.startProvince || ''
|
||
const fromCity = item.fromCity || item.departureCity || item.startCity || ''
|
||
const fromRegion = item.fromRegion || item.departureRegion || item.startRegion || ''
|
||
const toProvince = item.toProvince || item.destinationProvince || ''
|
||
const toCity = item.toCity || item.destinationCity || ''
|
||
const toRegion = item.toRegion || item.destinationRegion || ''
|
||
const fromText = this.composeRegionText(fromProvince, fromCity, fromRegion) || item.departure || item.from || '-'
|
||
const toText = this.composeRegionText(toProvince, toCity, toRegion) || item.city || item.destination || '-'
|
||
const proofs = this.parseHistoryProofs(item.images || item.image || item.proof || item.proofs || item.receiptPhoto || item.receiptPhotos)
|
||
return {
|
||
id: item.id,
|
||
start: item.startDate || item.beginDate || '',
|
||
end: item.endDate || '',
|
||
fromText,
|
||
toText,
|
||
traffic: item.transportCosts || 0,
|
||
hotel: item.liveCosts || 0,
|
||
other: item.otherCosts || 0,
|
||
remark: item.remark || item.note || item.memo || '',
|
||
proofs,
|
||
status: statusLabel,
|
||
expanded: false
|
||
}
|
||
},
|
||
parseHistoryProofs(value) {
|
||
if (value === null || value === undefined || value === '') return []
|
||
if (Array.isArray(value)) {
|
||
return value
|
||
.map(item => {
|
||
if (!item) return ''
|
||
if (typeof item === 'string') return item
|
||
return item.url || item.path || item.image || item.img || ''
|
||
})
|
||
.filter(Boolean)
|
||
}
|
||
if (typeof value === 'object') {
|
||
const url = value.url || value.path || value.image || value.img
|
||
return url ? [url] : []
|
||
}
|
||
const text = String(value).trim()
|
||
if (!text) return []
|
||
if (text.startsWith('[') || text.startsWith('{')) {
|
||
try {
|
||
return this.parseHistoryProofs(JSON.parse(text))
|
||
} catch (err) {
|
||
}
|
||
}
|
||
if (text.includes(',')) {
|
||
return text.split(',').map(item => item.trim()).filter(Boolean)
|
||
}
|
||
return [text]
|
||
},
|
||
previewHistoryProof(list = [], index = 0) {
|
||
if (!Array.isArray(list) || list.length === 0) return
|
||
uni.previewImage({
|
||
urls: list,
|
||
current: list[index] || list[0]
|
||
})
|
||
},
|
||
toggleHistoryDetail(id) {
|
||
this.travelHistory = this.travelHistory.map(item => {
|
||
if (item.id !== id) return item
|
||
return {
|
||
...item,
|
||
expanded: !item.expanded
|
||
}
|
||
})
|
||
},
|
||
openDatePicker(field) {
|
||
if (field === 'startDate') this.showStartPicker = true
|
||
if (field === 'endDate') this.showEndPicker = true
|
||
},
|
||
closeDatePicker(field) {
|
||
if (field === 'startDate') this.showStartPicker = false
|
||
if (field === 'endDate') this.showEndPicker = false
|
||
},
|
||
onDateConfirm(field, e) {
|
||
const value = dayjs(e.value).format('YYYY-MM-DD')
|
||
this.travelForm[field] = value
|
||
this.closeDatePicker(field)
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.travel-page {
|
||
min-height: 100vh;
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.page-hero {
|
||
background: url('https://yaodongxiao.oss-cn-guangzhou.aliyuncs.com/20260126/462bfa04cf0eff8698884c38b9d17519.png') no-repeat
|
||
top center / 100% auto;
|
||
}
|
||
|
||
.page-navbar {
|
||
padding: 0 6rpx;
|
||
}
|
||
|
||
.intro-card {
|
||
margin: 18rpx 20rpx 0;
|
||
padding: 20rpx 18rpx;
|
||
border-radius: 20rpx;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.card-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.title-bar {
|
||
width: 6rpx;
|
||
height: 30rpx;
|
||
border-radius: 999rpx;
|
||
background: #2fc6a7;
|
||
}
|
||
|
||
.intro-text {
|
||
margin-top: 12rpx;
|
||
font-size: 26rpx;
|
||
color: #6b7280;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.block {
|
||
margin: 18rpx 20rpx 0;
|
||
}
|
||
|
||
.form {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.form-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 10rpx 0;
|
||
border-bottom: 1rpx solid #edf1f7;
|
||
}
|
||
|
||
.form-row input {
|
||
text-align: right;
|
||
color: var(--text-main);
|
||
}
|
||
|
||
.picker-value {
|
||
color: var(--text-main);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.picker-value.placeholder {
|
||
color: var(--text-sub);
|
||
}
|
||
|
||
.textarea-row {
|
||
align-items: flex-start;
|
||
padding-top: 14rpx;
|
||
}
|
||
|
||
.remark-input {
|
||
flex: 1;
|
||
min-height: 100rpx;
|
||
text-align: right;
|
||
font-size: 26rpx;
|
||
color: var(--text-main);
|
||
}
|
||
|
||
.upload {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 12rpx 0;
|
||
border-bottom: 1rpx solid #edf1f7;
|
||
}
|
||
|
||
.upload-title {
|
||
font-weight: 700;
|
||
}
|
||
|
||
.upload-meta {
|
||
color: var(--text-sub);
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.proof-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10rpx;
|
||
margin-top: 10rpx;
|
||
}
|
||
|
||
.proof-item {
|
||
position: relative;
|
||
width: 160rpx;
|
||
height: 160rpx;
|
||
border-radius: 12rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.proof-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
}
|
||
|
||
.proof-remove {
|
||
position: absolute;
|
||
top: 8rpx;
|
||
right: 8rpx;
|
||
width: 34rpx;
|
||
height: 34rpx;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
color: #ffffff;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 22rpx;
|
||
line-height: 1;
|
||
}
|
||
|
||
.submit {
|
||
margin-top: 12rpx;
|
||
padding: 14rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.history {
|
||
padding: 12rpx 0;
|
||
border-bottom: 1rpx solid #edf1f7;
|
||
}
|
||
|
||
.history:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.history-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.history-main {
|
||
flex: 1;
|
||
}
|
||
|
||
.history-side {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.history-title {
|
||
font-weight: 700;
|
||
}
|
||
|
||
.history-meta {
|
||
color: var(--text-sub);
|
||
font-size: 24rpx;
|
||
margin-top: 4rpx;
|
||
}
|
||
|
||
.history-status {
|
||
color: var(--brand-primary);
|
||
font-weight: 700;
|
||
margin: 8rpx 0;
|
||
}
|
||
|
||
.history-status.pending {
|
||
color: #ff8354;
|
||
}
|
||
|
||
.history-status.reject {
|
||
color: #ff3b30;
|
||
}
|
||
|
||
.history-arrow {
|
||
transform: rotate(0deg);
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.history-arrow.open {
|
||
transform: rotate(90deg);
|
||
}
|
||
|
||
.history-detail {
|
||
margin-top: 8rpx;
|
||
padding: 8rpx 10rpx;
|
||
border-radius: 12rpx;
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.history-proof-list {
|
||
margin-top: 8rpx;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.history-proof-item {
|
||
width: 140rpx;
|
||
height: 140rpx;
|
||
border-radius: 10rpx;
|
||
overflow: hidden;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.history-proof-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
}
|
||
|
||
.empty {
|
||
color: var(--text-sub);
|
||
text-align: center;
|
||
padding: 16rpx 0;
|
||
}
|
||
|
||
.form-popup-mask {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.45);
|
||
z-index: 30;
|
||
}
|
||
|
||
.region-picker-popup {
|
||
position: fixed;
|
||
left: 50%;
|
||
bottom: 40rpx;
|
||
transform: translateX(-50%);
|
||
width: calc(100vw - 40rpx);
|
||
max-height: 80vh;
|
||
z-index: 31;
|
||
border-radius: 18rpx;
|
||
overflow: hidden;
|
||
padding: 0;
|
||
}
|
||
|
||
.region-picker-head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 10rpx 12rpx;
|
||
background: #f6f8fc;
|
||
}
|
||
|
||
.region-picker-title {
|
||
font-size: 24rpx;
|
||
color: var(--text-main);
|
||
}
|
||
|
||
.region-picker-actions {
|
||
display: flex;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.region-picker-btn {
|
||
font-size: 24rpx;
|
||
color: var(--text-sub);
|
||
}
|
||
|
||
.region-picker-btn.primary {
|
||
color: var(--brand-primary);
|
||
}
|
||
|
||
.region-loading {
|
||
padding: 16rpx 0;
|
||
text-align: center;
|
||
color: var(--text-sub);
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.picker-view {
|
||
height: 360rpx;
|
||
}
|
||
|
||
.picker-item {
|
||
height: 60rpx;
|
||
line-height: 60rpx;
|
||
text-align: center;
|
||
font-size: 26rpx;
|
||
color: var(--text-main);
|
||
}
|
||
</style>
|