提交
This commit is contained in:
1 parent
499896b29c
commit
8c96de2c0c
64 files changed
+35711
-30445
No files matched your search
@@ -0,0 +1,537 @@
|
||||
<template>
|
||||
<view class="bx-page bx-page--form">
|
||||
<view class="topbar" :style="safeAreaStyle">
|
||||
<view class="topbar-left">
|
||||
<text class="topbar-back" @click="goBack">←</text>
|
||||
<text class="topbar-title">{{ pageTitle }}</text>
|
||||
</view>
|
||||
<view class="topbar-right" />
|
||||
</view>
|
||||
|
||||
<view class="page-padded">
|
||||
<view class="form-card">
|
||||
<view class="form-header">
|
||||
<text class="form-header-title">加油信息</text>
|
||||
<text class="form-badge">{{ formBadge }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="form.registerNo" class="field">
|
||||
<text class="field-label">登记单号</text>
|
||||
<input class="form-input" :value="form.registerNo" disabled />
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="field-label">加油类别<text class="req">*</text></text>
|
||||
<view class="fuel-seg">
|
||||
<view
|
||||
v-for="opt in categoryOptions"
|
||||
:key="opt.value"
|
||||
class="fuel-seg__item"
|
||||
:class="{
|
||||
'fuel-seg__item--active': form.fuelCategory === opt.value,
|
||||
'fuel-seg__item--disabled': readonly
|
||||
}"
|
||||
@click="onPickCategory(opt.value)"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="field-label">日期<text class="req">*</text></text>
|
||||
<picker
|
||||
mode="date"
|
||||
:value="form.registerDate"
|
||||
:disabled="readonly"
|
||||
@change="onDateChange"
|
||||
>
|
||||
<view class="picker-trigger" :class="{ error: fieldErr.registerDate }">
|
||||
<text class="value" :class="{ ph: !form.registerDate }">
|
||||
{{ form.registerDate || '请选择日期' }}
|
||||
</text>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="field-label">登记人</text>
|
||||
<input class="form-input" :value="registrantDisplay" disabled />
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="field-label">车牌号<text class="req">*</text></text>
|
||||
<view
|
||||
class="picker-trigger"
|
||||
:class="{ error: fieldErr.licensePlate }"
|
||||
@click="openVehiclePicker"
|
||||
>
|
||||
<text class="value" :class="{ ph: !form.licensePlate }">
|
||||
{{ form.licensePlate || '请选择车牌(自有/三方)' }}
|
||||
</text>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="field-label">期初公里数/KM</text>
|
||||
<input
|
||||
class="form-input"
|
||||
type="digit"
|
||||
:value="form.odometerKm"
|
||||
:disabled="readonly"
|
||||
placeholder="选填"
|
||||
@input="onOdometerInput"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="field-label">加油升数/L<text class="req">*</text></text>
|
||||
<input
|
||||
class="form-input"
|
||||
type="digit"
|
||||
:class="{ error: fieldErr.fuelLiters }"
|
||||
:value="form.fuelLiters"
|
||||
:disabled="readonly"
|
||||
placeholder="请输入加油升数"
|
||||
@input="onLitersInput"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="field-label">备注</text>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
:value="form.remark"
|
||||
:disabled="readonly"
|
||||
placeholder="选填"
|
||||
:maxlength="500"
|
||||
@input="onRemarkInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-card">
|
||||
<view class="form-header">
|
||||
<text class="form-header-title">附件</text>
|
||||
</view>
|
||||
<button
|
||||
v-if="!readonly"
|
||||
class="btn btn-primary btn-sm"
|
||||
:disabled="uploading"
|
||||
@click="chooseAttachments"
|
||||
>
|
||||
{{ uploading ? '上传中…' : '上传图片' }}
|
||||
</button>
|
||||
<view v-if="imageUrls.length" class="att-grid">
|
||||
<view v-for="(url, idx) in imageUrls" :key="url + idx" class="att-img" @click="previewAt(idx)">
|
||||
<image :src="resolveUrl(url)" mode="aspectFill" />
|
||||
<view v-if="!readonly" class="del" @click.stop="removeImage(idx)">×</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="detail-section-empty" style="margin-top: 8px">
|
||||
{{ readonly ? '暂无附件' : '暂无附件(可选,支持相册/拍照)' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!readonly" class="bottom-action-bar">
|
||||
<button class="btn btn-primary btn-block" :disabled="submitting || uploading" @click="onSubmit">
|
||||
{{ submitting ? '保存中…' : '保存' }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 车牌选择:后台模糊检索(对齐管理端 PlVehicleSelect) -->
|
||||
<view v-if="showVehiclePicker" class="bx-sheet-mask" @click="closeVehiclePicker">
|
||||
<view class="bx-sheet bx-sheet--tall" @click.stop>
|
||||
<view class="bx-sheet__title">选择车牌</view>
|
||||
<view class="bx-sheet__search">
|
||||
<input
|
||||
class="bx-sheet__search-input"
|
||||
:value="vehicleKeyword"
|
||||
placeholder="输入车牌号搜索"
|
||||
confirm-type="search"
|
||||
@input="onVehicleKeywordInput"
|
||||
@confirm="searchVehiclesNow"
|
||||
/>
|
||||
<view
|
||||
v-if="vehicleKeyword.trim()"
|
||||
class="bx-sheet__search-clear"
|
||||
@click="clearVehicleKeyword"
|
||||
>
|
||||
×
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view scroll-y class="bx-sheet__body">
|
||||
<view v-if="vehicleLoading" class="bx-sheet__empty">搜索中…</view>
|
||||
<block v-else>
|
||||
<view v-if="!vehicles.length" class="bx-sheet__empty">
|
||||
{{ vehicleKeyword.trim() ? '无匹配车牌' : '暂无车辆' }}
|
||||
</view>
|
||||
<view
|
||||
v-for="item in vehicles"
|
||||
:key="vehicleRowKey(item)"
|
||||
class="bx-sheet__item"
|
||||
:class="{ 'is-selected': vehiclePlate(item) === form.licensePlate }"
|
||||
@click="confirmVehiclePick(item)"
|
||||
>
|
||||
<view>
|
||||
<text>{{ vehiclePlate(item) }}</text>
|
||||
<view v-if="vehicleTypeTag(item)" class="bx-sheet__item-sub">
|
||||
{{ vehicleTypeTag(item) }}
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="vehiclePlate(item) === form.licensePlate">✓</text>
|
||||
</view>
|
||||
</block>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { PAGE_PATHS } from '@/config/api'
|
||||
import { MINIO_BASE_URL } from '@/config/env'
|
||||
import { fuelRegisterApi, fetchVehicleOptions } from '@/api/pl'
|
||||
import { chooseAndUploadImages, previewImages } from '@/utils/upload'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getSafeAreaStyle } from '@/utils/safeArea'
|
||||
|
||||
const safeAreaStyle = getSafeAreaStyle()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const MAX_ATTACHMENTS = 5
|
||||
const categoryOptions = [
|
||||
{ label: '百链', value: 'bailian' },
|
||||
{ label: '加油站', value: 'gas_station' }
|
||||
]
|
||||
|
||||
const editId = ref('')
|
||||
const readonly = ref(false)
|
||||
const submitting = ref(false)
|
||||
const uploading = ref(false)
|
||||
const imageUrls = ref([])
|
||||
const vehicles = ref([])
|
||||
const showVehiclePicker = ref(false)
|
||||
const vehicleKeyword = ref('')
|
||||
const vehicleLoading = ref(false)
|
||||
let vehicleSearchTimer = null
|
||||
let vehicleSearchSeq = 0
|
||||
|
||||
const form = reactive({
|
||||
registerNo: '',
|
||||
fuelCategory: 'bailian',
|
||||
registerDate: '',
|
||||
registrantId: '',
|
||||
licensePlate: '',
|
||||
odometerKm: '',
|
||||
fuelLiters: '',
|
||||
remark: ''
|
||||
})
|
||||
const fieldErr = reactive({
|
||||
registerDate: false,
|
||||
licensePlate: false,
|
||||
fuelLiters: false
|
||||
})
|
||||
|
||||
const isEdit = computed(() => !!editId.value)
|
||||
const pageTitle = computed(() => {
|
||||
if (readonly.value) return '加油登记详情'
|
||||
return isEdit.value ? '编辑加油登记' : '新增加油登记'
|
||||
})
|
||||
const formBadge = computed(() => {
|
||||
if (readonly.value) return '只读'
|
||||
return isEdit.value ? '编辑' : '新建'
|
||||
})
|
||||
|
||||
const registrantDisplay = computed(() => {
|
||||
const info = userStore.userInfo || {}
|
||||
const name = info.realName || info.nickName || info.username || ''
|
||||
return name || '当前登录人'
|
||||
})
|
||||
|
||||
function vehiclePlate(v) {
|
||||
return String(v?.licensePlate || v?.license_plate || '').trim()
|
||||
}
|
||||
|
||||
function vehicleTypeTag(v) {
|
||||
const type = v?.vehicleType || v?.vehicle_type
|
||||
if (type === 'self') return '自有'
|
||||
if (type === 'carrier') return '三方'
|
||||
return ''
|
||||
}
|
||||
|
||||
function vehicleRowKey(v) {
|
||||
return String(v?.id || vehiclePlate(v) || '')
|
||||
}
|
||||
|
||||
function todayStr() {
|
||||
const d = new Date()
|
||||
const m = `${d.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${d.getDate()}`.padStart(2, '0')
|
||||
return `${d.getFullYear()}-${m}-${day}`
|
||||
}
|
||||
|
||||
function resolveUrl(url) {
|
||||
const trimmed = String(url || '').trim()
|
||||
if (!trimmed) return ''
|
||||
if (/^https?:\/\//i.test(trimmed)) return trimmed
|
||||
const base = String(MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
return base ? `${base}/${trimmed.replace(/^\//, '')}` : trimmed
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack({ fail: () => uni.redirectTo({ url: PAGE_PATHS.plFuelRegister }) })
|
||||
}
|
||||
|
||||
function onPickCategory(value) {
|
||||
if (readonly.value) return
|
||||
form.fuelCategory = value
|
||||
}
|
||||
|
||||
function onDateChange(e) {
|
||||
form.registerDate = e.detail.value
|
||||
fieldErr.registerDate = false
|
||||
}
|
||||
|
||||
function onOdometerInput(e) {
|
||||
form.odometerKm = e.detail.value
|
||||
}
|
||||
|
||||
function onLitersInput(e) {
|
||||
form.fuelLiters = e.detail.value
|
||||
fieldErr.fuelLiters = false
|
||||
}
|
||||
|
||||
function onRemarkInput(e) {
|
||||
form.remark = e.detail.value
|
||||
}
|
||||
|
||||
async function loadVehicles(keyword = '') {
|
||||
const seq = ++vehicleSearchSeq
|
||||
vehicleLoading.value = true
|
||||
try {
|
||||
const list = await fetchVehicleOptions(keyword)
|
||||
if (seq !== vehicleSearchSeq) return
|
||||
vehicles.value = Array.isArray(list) ? list : []
|
||||
} catch (e) {
|
||||
if (seq !== vehicleSearchSeq) return
|
||||
vehicles.value = []
|
||||
} finally {
|
||||
if (seq === vehicleSearchSeq) vehicleLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openVehiclePicker() {
|
||||
if (readonly.value) return
|
||||
showVehiclePicker.value = true
|
||||
vehicleKeyword.value = ''
|
||||
loadVehicles('')
|
||||
}
|
||||
|
||||
function closeVehiclePicker() {
|
||||
showVehiclePicker.value = false
|
||||
if (vehicleSearchTimer) {
|
||||
clearTimeout(vehicleSearchTimer)
|
||||
vehicleSearchTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function onVehicleKeywordInput(e) {
|
||||
vehicleKeyword.value = e.detail.value || ''
|
||||
if (vehicleSearchTimer) clearTimeout(vehicleSearchTimer)
|
||||
vehicleSearchTimer = setTimeout(() => {
|
||||
loadVehicles(vehicleKeyword.value)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function searchVehiclesNow() {
|
||||
if (vehicleSearchTimer) {
|
||||
clearTimeout(vehicleSearchTimer)
|
||||
vehicleSearchTimer = null
|
||||
}
|
||||
loadVehicles(vehicleKeyword.value)
|
||||
}
|
||||
|
||||
function clearVehicleKeyword() {
|
||||
vehicleKeyword.value = ''
|
||||
if (vehicleSearchTimer) {
|
||||
clearTimeout(vehicleSearchTimer)
|
||||
vehicleSearchTimer = null
|
||||
}
|
||||
loadVehicles('')
|
||||
}
|
||||
|
||||
function confirmVehiclePick(item) {
|
||||
const plate = vehiclePlate(item)
|
||||
if (!plate) return
|
||||
form.licensePlate = plate
|
||||
fieldErr.licensePlate = false
|
||||
closeVehiclePicker()
|
||||
}
|
||||
|
||||
async function chooseAttachments() {
|
||||
uploading.value = true
|
||||
try {
|
||||
const urls = await chooseAndUploadImages({
|
||||
folder: 'pl/vehicle-fuel/register',
|
||||
maxCount: MAX_ATTACHMENTS,
|
||||
currentUrls: imageUrls.value
|
||||
})
|
||||
if (urls.length) {
|
||||
imageUrls.value = imageUrls.value.concat(urls)
|
||||
}
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function removeImage(idx) {
|
||||
imageUrls.value.splice(idx, 1)
|
||||
}
|
||||
|
||||
function previewAt(idx) {
|
||||
previewImages(
|
||||
imageUrls.value.map((u) => resolveUrl(u)),
|
||||
resolveUrl(imageUrls.value[idx])
|
||||
)
|
||||
}
|
||||
|
||||
function validate() {
|
||||
fieldErr.registerDate = false
|
||||
fieldErr.licensePlate = false
|
||||
fieldErr.fuelLiters = false
|
||||
if (!form.fuelCategory) {
|
||||
uni.showToast({ title: '请选择加油类别', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (!form.registerDate) {
|
||||
fieldErr.registerDate = true
|
||||
uni.showToast({ title: '请选择日期', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (!form.licensePlate) {
|
||||
fieldErr.licensePlate = true
|
||||
uni.showToast({ title: '请选择车牌号', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
const litersRaw = String(form.fuelLiters ?? '').trim()
|
||||
if (!litersRaw) {
|
||||
fieldErr.fuelLiters = true
|
||||
uni.showToast({ title: '请输入加油升数', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
const liters = Number(litersRaw)
|
||||
if (Number.isNaN(liters) || liters <= 0) {
|
||||
fieldErr.fuelLiters = true
|
||||
uni.showToast({ title: '加油升数须大于 0', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function buildPayload() {
|
||||
const odoRaw = String(form.odometerKm ?? '').trim()
|
||||
return {
|
||||
fuelCategory: form.fuelCategory,
|
||||
registerDate: form.registerDate,
|
||||
registrantId: form.registrantId || undefined,
|
||||
licensePlate: form.licensePlate,
|
||||
odometerKm: odoRaw === '' ? null : Number(odoRaw),
|
||||
fuelLiters: Number(form.fuelLiters),
|
||||
attachmentUrls: imageUrls.value.join(',') || undefined,
|
||||
remark: form.remark || undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEditData(id) {
|
||||
try {
|
||||
const data = await fuelRegisterApi.get(id)
|
||||
form.registerNo = data.registerNo || ''
|
||||
form.fuelCategory = data.fuelCategory || 'bailian'
|
||||
form.registerDate = data.registerDate || ''
|
||||
form.registrantId = data.registrantId || ''
|
||||
form.licensePlate = data.licensePlate || ''
|
||||
form.odometerKm = data.odometerKm != null ? String(data.odometerKm) : ''
|
||||
form.fuelLiters = data.fuelLiters != null ? String(data.fuelLiters) : ''
|
||||
form.remark = data.remark || ''
|
||||
imageUrls.value = String(data.attachmentUrls || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
readonly.value = data.status === 'cancelled'
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (!validate()) return
|
||||
if (uploading.value) {
|
||||
uni.showToast({ title: '附件上传中,请稍候', icon: 'none' })
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
const payload = buildPayload()
|
||||
if (editId.value) {
|
||||
await fuelRegisterApi.update(editId.value, payload)
|
||||
uni.showToast({ title: '更新成功', icon: 'success' })
|
||||
} else {
|
||||
await fuelRegisterApi.create(payload)
|
||||
uni.showToast({ title: '创建成功', icon: 'success' })
|
||||
}
|
||||
setTimeout(() => goBack(), 500)
|
||||
} catch (e) {
|
||||
// toasted
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(async (query) => {
|
||||
const info = userStore.userInfo || {}
|
||||
form.registrantId = String(info.id || info.userId || '')
|
||||
form.registerDate = todayStr()
|
||||
const id = query?.id ? String(query.id) : ''
|
||||
if (id) {
|
||||
editId.value = id
|
||||
await loadEditData(id)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/styles/bx-theme.scss';
|
||||
|
||||
.fuel-seg {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.fuel-seg__item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text-sec);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.fuel-seg__item--active {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary-bg);
|
||||
color: var(--primary-dark);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fuel-seg__item--disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<view class="bx-page">
|
||||
<view class="topbar" :style="safeAreaStyle">
|
||||
<view class="topbar-left">
|
||||
<text class="topbar-back" @click="goBack">←</text>
|
||||
<text class="topbar-title">加油登记</text>
|
||||
</view>
|
||||
<view class="topbar-right" />
|
||||
</view>
|
||||
|
||||
<view class="fab-add" @click="goForm()">
|
||||
<view class="fab-add__bar fab-add__bar--h" />
|
||||
<view class="fab-add__bar fab-add__bar--v" />
|
||||
</view>
|
||||
|
||||
<view class="search-wrap">
|
||||
<view class="search-box">
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="keyword"
|
||||
placeholder="搜索登记单号/车牌"
|
||||
confirm-type="search"
|
||||
@confirm="onRefresh"
|
||||
/>
|
||||
<view v-if="keyword.trim()" class="search-box__clear" @click="clearKeyword">×</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="search-wrap" style="padding-top: 0">
|
||||
<scroll-view scroll-x class="search-filter-scroll" :show-scrollbar="false">
|
||||
<view class="search-filter">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value === '' ? '__all' : tab.value"
|
||||
class="filter-tag"
|
||||
:class="{ active: activeTab === tab.value }"
|
||||
@click="onTabChange(tab.value)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
class="page-padded list-scroll"
|
||||
scroll-y
|
||||
:style="{ height: listHeight }"
|
||||
refresher-enabled
|
||||
:refresher-triggered="refreshing"
|
||||
@refresherrefresh="onPullRefresh"
|
||||
@scrolltolower="onLoadMore"
|
||||
>
|
||||
<view v-if="!list.length && !loading" class="empty-state empty-state--fill">
|
||||
<text>暂无加油登记</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="expense-card"
|
||||
@click="goForm(item.id)"
|
||||
>
|
||||
<view class="ec-top">
|
||||
<view class="ec-type">
|
||||
<text class="advance-list-card__title">{{ item.licensePlate || '—' }}</text>
|
||||
</view>
|
||||
<text class="status-tag" :class="statusTagClass(item.status)">
|
||||
{{ statusLabel(item.status) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="ec-amount-label">加油升数</view>
|
||||
<view class="ec-amount">{{ formatLiters(item.fuelLiters) }} L</view>
|
||||
<view class="list-card-kv">
|
||||
<view class="list-card-kv__row">
|
||||
<text class="list-card-kv__k">登记单号</text>
|
||||
<text class="list-card-kv__v">{{ item.registerNo || '—' }}</text>
|
||||
</view>
|
||||
<view class="list-card-kv__row">
|
||||
<text class="list-card-kv__k">加油类别</text>
|
||||
<text class="list-card-kv__v">{{ categoryLabel(item.fuelCategory) }}</text>
|
||||
</view>
|
||||
<view class="list-card-kv__row">
|
||||
<text class="list-card-kv__k">日期</text>
|
||||
<text class="list-card-kv__v">{{ item.registerDate || '—' }}</text>
|
||||
</view>
|
||||
<view class="list-card-kv__row">
|
||||
<text class="list-card-kv__k">登记人</text>
|
||||
<text class="list-card-kv__v">{{ item.registrantName || '—' }}</text>
|
||||
</view>
|
||||
<view class="list-card-kv__row">
|
||||
<text class="list-card-kv__k">期初公里数</text>
|
||||
<text class="list-card-kv__v">{{ item.odometerKm != null ? item.odometerKm : '—' }}</text>
|
||||
</view>
|
||||
<view v-if="item.remark" class="list-card-kv__row">
|
||||
<text class="list-card-kv__k">备注</text>
|
||||
<text class="list-card-kv__v">{{ item.remark }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ec-bottom" @click.stop>
|
||||
<view class="op-actions">
|
||||
<template v-if="item.status === 'normal'">
|
||||
<button class="btn btn-sm btn-primary" @click="goForm(item.id)">编辑</button>
|
||||
<button class="btn btn-sm btn-danger" @click="handleCancel(item)">作废</button>
|
||||
</template>
|
||||
<button v-else class="btn btn-sm btn-primary" @click="goForm(item.id)">查看</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="list.length" class="list-footer-tip">
|
||||
{{ finished ? '没有更多了' : loading ? '加载中...' : '上拉加载更多' }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { PAGE_PATHS } from '@/config/api'
|
||||
import { fuelRegisterApi } from '@/api/pl'
|
||||
import { getSafeAreaStyle } from '@/utils/safeArea'
|
||||
|
||||
const safeAreaStyle = getSafeAreaStyle()
|
||||
|
||||
const tabs = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '正常', value: 'normal' },
|
||||
{ label: '已作废', value: 'cancelled' }
|
||||
]
|
||||
|
||||
const keyword = ref('')
|
||||
const activeTab = ref('')
|
||||
const list = ref([])
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = 10
|
||||
const listHeight = ref('60vh')
|
||||
|
||||
function computeListHeight() {
|
||||
try {
|
||||
const sys = uni.getSystemInfoSync()
|
||||
const h = (sys.windowHeight || 600) - 210
|
||||
listHeight.value = `${Math.max(h, 320)}px`
|
||||
} catch (e) {
|
||||
listHeight.value = '60vh'
|
||||
}
|
||||
}
|
||||
|
||||
function categoryLabel(v) {
|
||||
if (v === 'bailian') return '百链'
|
||||
if (v === 'gas_station') return '加油站'
|
||||
return v || '—'
|
||||
}
|
||||
|
||||
function statusLabel(v) {
|
||||
if (v === 'cancelled') return '已作废'
|
||||
return '正常'
|
||||
}
|
||||
|
||||
function statusTagClass(v) {
|
||||
return v === 'cancelled' ? 's-withdrawn' : 's-approved'
|
||||
}
|
||||
|
||||
function formatLiters(v) {
|
||||
const n = Number(v)
|
||||
return Number.isNaN(n) ? '—' : n.toFixed(2)
|
||||
}
|
||||
|
||||
async function fetchPage() {
|
||||
if (loading.value && !refreshing.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: currentPage.value,
|
||||
pageSize
|
||||
}
|
||||
if (activeTab.value) params.status = activeTab.value
|
||||
if (keyword.value.trim()) params.q = keyword.value.trim()
|
||||
const { items, total } = await fuelRegisterApi.list(params)
|
||||
if (currentPage.value === 1) {
|
||||
list.value = items
|
||||
} else {
|
||||
list.value = list.value.concat(items)
|
||||
}
|
||||
currentPage.value += 1
|
||||
finished.value = items.length < pageSize || list.value.length >= total
|
||||
} catch (e) {
|
||||
finished.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
refreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onRefresh() {
|
||||
currentPage.value = 1
|
||||
finished.value = false
|
||||
loading.value = false
|
||||
list.value = []
|
||||
fetchPage()
|
||||
}
|
||||
|
||||
function onPullRefresh() {
|
||||
refreshing.value = true
|
||||
currentPage.value = 1
|
||||
finished.value = false
|
||||
fetchPage()
|
||||
}
|
||||
|
||||
function onLoadMore() {
|
||||
if (finished.value || loading.value) return
|
||||
fetchPage()
|
||||
}
|
||||
|
||||
function onTabChange(value) {
|
||||
activeTab.value = value
|
||||
onRefresh()
|
||||
}
|
||||
|
||||
function clearKeyword() {
|
||||
keyword.value = ''
|
||||
onRefresh()
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack({ fail: () => uni.reLaunch({ url: PAGE_PATHS.workbench }) })
|
||||
}
|
||||
|
||||
function goForm(id) {
|
||||
const q = id ? `?id=${id}` : ''
|
||||
uni.navigateTo({ url: `${PAGE_PATHS.plFuelRegisterForm}${q}` })
|
||||
}
|
||||
|
||||
function handleCancel(item) {
|
||||
uni.showModal({
|
||||
title: '作废确认',
|
||||
content: '确定作废该加油登记吗?',
|
||||
confirmText: '作废',
|
||||
confirmColor: '#ef4444',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
try {
|
||||
await fuelRegisterApi.cancel(item.id)
|
||||
uni.showToast({ title: '已作废', icon: 'success' })
|
||||
onRefresh()
|
||||
} catch (e) {
|
||||
// toasted
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
computeListHeight()
|
||||
onRefresh()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/styles/bx-theme.scss';
|
||||
|
||||
.list-scroll {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<view class="bx-page pl-site-plan-page pl-site-plan-detail">
|
||||
<view class="topbar" :style="safeAreaStyle">
|
||||
<view class="topbar-left">
|
||||
<text class="topbar-back" @click="goBack">←</text>
|
||||
<text class="topbar-title">现场回传</text>
|
||||
</view>
|
||||
<view class="topbar-right" />
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="page-loading">加载中...</view>
|
||||
<view v-else-if="errorMessage" class="empty-hint">{{ errorMessage }}</view>
|
||||
<view v-else-if="page?.planVoided" class="empty-hint">运输计划已作废</view>
|
||||
<view v-else-if="page?.planArchived" class="empty-hint">运输计划已归档完成</view>
|
||||
<view v-else-if="page && !(page.stops || []).length" class="empty-hint"
|
||||
>暂无需要现场回传的段</view
|
||||
>
|
||||
<template v-else-if="page">
|
||||
<view class="section-card">
|
||||
<view class="kv-row">
|
||||
<text class="kv-k">计划编号</text>
|
||||
<text class="kv-v">{{ page.planNo || '—' }}</text>
|
||||
</view>
|
||||
<view class="kv-row">
|
||||
<text class="kv-k">计划日期</text>
|
||||
<text class="kv-v">{{ page.planDate || '—' }}</text>
|
||||
</view>
|
||||
<view class="kv-row">
|
||||
<text class="kv-k">车牌</text>
|
||||
<text class="kv-v">{{ page.licensePlate || page.vehicleNo || '—' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-for="stop in stopStates"
|
||||
:key="`${stop.stopType}-${stop.stopId}`"
|
||||
class="section-card"
|
||||
>
|
||||
<view class="stop-title"
|
||||
>{{ stop.label }} · {{ stop.customerName || '—'
|
||||
}}<text v-if="stop.businessType" class="meta">({{ stop.businessType }})</text></view
|
||||
>
|
||||
|
||||
<view class="kv-row">
|
||||
<text class="kv-k">地址</text>
|
||||
<view class="addr-row kv-v">
|
||||
<text>{{ stop.address || '—' }}</text>
|
||||
<text v-if="canNavigate(stop)" class="nav-btn" @click="openNavigation(stop)"
|
||||
>导航</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
<view class="kv-row">
|
||||
<text class="kv-k">联系方式</text>
|
||||
<text class="kv-v">{{ stop.contact || '—' }}</text>
|
||||
</view>
|
||||
<view class="kv-row">
|
||||
<text class="kv-k">注意事项</text>
|
||||
<text class="kv-v">{{ stop.notes || '—' }}</text>
|
||||
</view>
|
||||
|
||||
<view v-for="(item, idx) in stop.products || []" :key="item.stopProductId || idx" class="product-card">
|
||||
<view class="product-name"
|
||||
>{{ idx + 1 }}/{{ (stop.products || []).length }} {{ item.productName || '产品' }}</view
|
||||
>
|
||||
<view class="meta">计划:{{ formatPlanQty(item) }}</view>
|
||||
<view class="meta">别名:{{ item.aliasName || '—' }}</view>
|
||||
<view class="meta">品牌:{{ item.brandName || '—' }}</view>
|
||||
<view class="meta">规格型号:{{ item.model || '—' }}</view>
|
||||
<view class="meta">包装规格:{{ item.packageSpec || '—' }}</view>
|
||||
</view>
|
||||
|
||||
<view class="actual-box">
|
||||
<view class="actual-label">实际数量(吨)</view>
|
||||
<view class="actual-row">
|
||||
<input
|
||||
class="actual-input"
|
||||
type="digit"
|
||||
v-model="stop.quantityText"
|
||||
:disabled="readonly"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
<text>吨</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="attach-label">附件上传</view>
|
||||
<view class="attach-row">
|
||||
<image
|
||||
v-for="(url, i) in stop.attachmentUrls"
|
||||
:key="`${url}-${i}`"
|
||||
class="attach-thumb"
|
||||
:src="resolveFileUrl(url)"
|
||||
mode="aspectFill"
|
||||
@click="previewStopImages(stop, i)"
|
||||
@longpress="removeAttachment(stop, i)"
|
||||
/>
|
||||
<view
|
||||
v-if="!readonly && stop.attachmentUrls.length < 9"
|
||||
class="attach-add"
|
||||
@click="uploadForStop(stop)"
|
||||
>+</view
|
||||
>
|
||||
</view>
|
||||
<view v-if="!canUploadAttachments(stop) && !readonly" class="meta"
|
||||
>请先填写数量后再上传附件</view
|
||||
>
|
||||
|
||||
<textarea
|
||||
class="remark-input"
|
||||
v-model="stop.remark"
|
||||
:disabled="readonly"
|
||||
maxlength="500"
|
||||
placeholder="填写备注(可选)"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-if="page && !errorMessage && !loading" class="page-footer">
|
||||
<view class="footer-btn footer-btn--ghost" @click="goList">现场记录</view>
|
||||
<view
|
||||
v-if="(page.stops || []).length && !readonly"
|
||||
class="footer-btn footer-btn--primary"
|
||||
@click="handleSave"
|
||||
>{{ saving ? '保存中...' : '保存回传' }}</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { API_BASE, PAGE_PATHS, TOKEN_KEY } from '@/config/api'
|
||||
import { MINIO_BASE_URL } from '@/config/env'
|
||||
import { sitePlanApi } from '@/api/pl'
|
||||
import { getSafeAreaStyle } from '@/utils/safeArea'
|
||||
import { previewImages } from '@/utils/upload'
|
||||
|
||||
import '@/styles/bx-theme.scss'
|
||||
import '@/styles/pl-site-plan.scss'
|
||||
|
||||
const safeAreaStyle = getSafeAreaStyle()
|
||||
const planId = ref('')
|
||||
const page = ref(null)
|
||||
const stopStates = ref([])
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
const readonly = computed(
|
||||
() => !!(page.value?.planVoided || page.value?.planArchived)
|
||||
)
|
||||
|
||||
function formatPlanQty(item) {
|
||||
const qty = item.quantity
|
||||
const unit = item.unit || ''
|
||||
if (qty == null) return '—'
|
||||
return unit ? `${qty} ${unit}` : String(qty)
|
||||
}
|
||||
|
||||
function canUploadAttachments(stop) {
|
||||
return Number(stop.quantityText || 0) > 0
|
||||
}
|
||||
|
||||
function canNavigate(stop) {
|
||||
return stop.lng != null && stop.lat != null
|
||||
}
|
||||
|
||||
function openNavigation(stop) {
|
||||
const lat = Number(stop.lat)
|
||||
const lng = Number(stop.lng)
|
||||
if (Number.isNaN(lat) || Number.isNaN(lng)) return
|
||||
uni.openLocation({
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
name: stop.label || '目的地',
|
||||
address: stop.address || '',
|
||||
fail: () => uni.showToast({ title: '无法打开导航', icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
function resolveFileUrl(url) {
|
||||
if (!url) return ''
|
||||
if (/^https?:\/\//i.test(url)) return url
|
||||
const base = String(MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
return base ? `${base}/${String(url).replace(/^\//, '')}` : url
|
||||
}
|
||||
|
||||
function previewStopImages(stop, index) {
|
||||
const urls = (stop.attachmentUrls || []).map(resolveFileUrl)
|
||||
previewImages(urls, urls[index])
|
||||
}
|
||||
|
||||
function removeAttachment(stop, index) {
|
||||
if (readonly.value) return
|
||||
uni.showModal({
|
||||
title: '删除附件',
|
||||
content: '确定删除该附件吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) stop.attachmentUrls.splice(index, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function uploadForStop(stop) {
|
||||
if (readonly.value) return
|
||||
if (!canUploadAttachments(stop)) {
|
||||
uni.showToast({ title: '请先填写数量后再上传附件', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const remain = 9 - (stop.attachmentUrls?.length || 0)
|
||||
if (remain <= 0) return
|
||||
|
||||
const choose = await new Promise((resolve) => {
|
||||
uni.chooseImage({
|
||||
count: remain,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: resolve,
|
||||
fail: () => resolve(null)
|
||||
})
|
||||
})
|
||||
if (!choose?.tempFilePaths?.length) return
|
||||
|
||||
const token = uni.getStorageSync(TOKEN_KEY) || ''
|
||||
const uploadUrl = `${API_BASE}/transport/site-plan/${planId.value}/attachments`
|
||||
uni.showLoading({ title: '上传中...' })
|
||||
try {
|
||||
for (const filePath of choose.tempFilePaths) {
|
||||
const uploadRes = await new Promise((ok, fail) => {
|
||||
uni.uploadFile({
|
||||
url: uploadUrl,
|
||||
filePath,
|
||||
name: 'file',
|
||||
header: { Authorization: token ? `Bearer ${token}` : '' },
|
||||
formData: { folder: 'pl/transport/site-feedback' },
|
||||
success: ok,
|
||||
fail
|
||||
})
|
||||
})
|
||||
let body = {}
|
||||
try {
|
||||
body = JSON.parse(uploadRes.data || '{}')
|
||||
} catch (e) {
|
||||
body = {}
|
||||
}
|
||||
const data = body.data || {}
|
||||
const url = data.url || data.fileUrl || data.file_url
|
||||
if (body.code !== 200 || !url) {
|
||||
uni.showToast({ title: body.message || '上传失败', icon: 'none' })
|
||||
break
|
||||
}
|
||||
stop.attachmentUrls.push(String(url))
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
if (!planId.value) {
|
||||
errorMessage.value = '缺少计划 ID'
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const data = await sitePlanApi.get(planId.value)
|
||||
page.value = data
|
||||
stopStates.value = (data.stops || []).map((s) => ({
|
||||
...s,
|
||||
quantityText: s.quantity != null ? String(s.quantity) : '',
|
||||
remark: s.remark || '',
|
||||
attachmentUrls: [...(s.attachmentUrls || [])]
|
||||
}))
|
||||
} catch (e) {
|
||||
errorMessage.value = e?.message || '加载失败'
|
||||
page.value = null
|
||||
stopStates.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (readonly.value || saving.value) return
|
||||
if (!stopStates.value.length) return
|
||||
saving.value = true
|
||||
try {
|
||||
for (const stop of stopStates.value) {
|
||||
const quantity = Number(stop.quantityText || 0)
|
||||
if (quantity <= 0 && !stop.attachmentUrls.length && !(stop.remark || '').trim()) {
|
||||
continue
|
||||
}
|
||||
await sitePlanApi.saveFeedback(planId.value, {
|
||||
stopType: stop.stopType,
|
||||
stopId: stop.stopId,
|
||||
quantity: quantity > 0 ? quantity : undefined,
|
||||
attachmentUrls: stop.attachmentUrls,
|
||||
remark: (stop.remark || '').trim() || undefined
|
||||
})
|
||||
}
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
await loadDetail()
|
||||
} catch (e) {
|
||||
// dbRequest already toasted
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goList() {
|
||||
uni.navigateBack({
|
||||
fail: () => uni.redirectTo({ url: PAGE_PATHS.plTransportPlan })
|
||||
})
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
goList()
|
||||
}
|
||||
|
||||
onLoad((q) => {
|
||||
planId.value = (q && q.planId) || ''
|
||||
loadDetail()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<view class="bx-page pl-site-plan-page">
|
||||
<view class="topbar" :style="safeAreaStyle">
|
||||
<view class="topbar-left">
|
||||
<text class="topbar-back" @click="goBack">←</text>
|
||||
<text class="topbar-title">运输计划单</text>
|
||||
</view>
|
||||
<view class="topbar-right" />
|
||||
</view>
|
||||
|
||||
<view class="filters">
|
||||
<uni-calendar
|
||||
:insert="true"
|
||||
:date="selectedDate"
|
||||
:showMonth="false"
|
||||
@change="onCalendarChange"
|
||||
/>
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="keyword"
|
||||
placeholder="车牌号/产品名称"
|
||||
confirm-type="search"
|
||||
@confirm="loadList"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="list-body">
|
||||
<view v-if="loading" class="page-loading">加载中...</view>
|
||||
<view v-else-if="!records.length" class="empty-hint">暂无现场记录</view>
|
||||
<view
|
||||
v-for="(row, index) in records"
|
||||
:key="row.planId || index"
|
||||
class="record-card"
|
||||
@click="openRecord(row)"
|
||||
>
|
||||
<text class="index-tag">{{ index + 1 }}</text>
|
||||
<view class="record-top">
|
||||
<text class="card-title">{{ cardTitle(row) }}</text>
|
||||
<text class="tag" :class="tagClass(row)">{{ row.statusLabel || '进行中' }}</text>
|
||||
</view>
|
||||
<view class="meta">
|
||||
车牌:<text class="plate-hl">{{ row.licensePlate || '—' }}</text>
|
||||
</view>
|
||||
<template v-if="(row.stops || []).length">
|
||||
<view
|
||||
v-for="(stop, sIndex) in row.stops"
|
||||
:key="`${stop.stopType}-${stop.seq ?? sIndex}`"
|
||||
class="stop-block"
|
||||
>
|
||||
<view class="stop-block__title"
|
||||
>{{ stop.label }} · {{ stop.customerName || '—' }}</view
|
||||
>
|
||||
<view class="meta">时间:{{ formatDay(stop.stopTime) }}</view>
|
||||
<view class="meta">地址:{{ stop.address || '—' }}</view>
|
||||
<template v-if="!(stop.products || []).length">
|
||||
<view class="meta">产品:—</view>
|
||||
</template>
|
||||
<template v-for="(prod, pIndex) in stop.products || []" :key="pIndex">
|
||||
<view class="meta">产品:{{ prod.productName || '—' }}</view>
|
||||
<view class="meta">规格型号:{{ prod.model || '—' }}</view>
|
||||
<view class="meta">包装规格:{{ prod.packageSpec || '—' }}</view>
|
||||
<view class="meta"
|
||||
>数量:{{ formatQty(prod.quantity) }} · 单位:{{ prod.unit || '—' }}</view
|
||||
>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
<view class="meta">运输计划号:{{ row.planNo || '—' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { PAGE_PATHS } from '@/config/api'
|
||||
import { sitePlanApi } from '@/api/pl'
|
||||
import { getSafeAreaStyle } from '@/utils/safeArea'
|
||||
|
||||
import '@/styles/bx-theme.scss'
|
||||
import '@/styles/pl-site-plan.scss'
|
||||
|
||||
const safeAreaStyle = getSafeAreaStyle()
|
||||
|
||||
function todayStr() {
|
||||
const d = new Date()
|
||||
const m = `${d.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${d.getDate()}`.padStart(2, '0')
|
||||
return `${d.getFullYear()}-${m}-${day}`
|
||||
}
|
||||
|
||||
const selectedDate = ref(todayStr())
|
||||
const keyword = ref('')
|
||||
const records = ref([])
|
||||
const loading = ref(false)
|
||||
let keywordTimer = null
|
||||
|
||||
function formatDay(value) {
|
||||
if (!value) return '—'
|
||||
const normalized = String(value).replace('T', ' ')
|
||||
return normalized.length >= 10 ? normalized.slice(0, 10) : normalized
|
||||
}
|
||||
|
||||
function formatQty(value) {
|
||||
if (value == null || value === '') return '—'
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function cardTitleDate(row) {
|
||||
const stopTimes = (row.stops || [])
|
||||
.map((stop) => formatDay(stop.stopTime))
|
||||
.filter((value) => value !== '—')
|
||||
if (stopTimes.length) return stopTimes[0]
|
||||
const fromPickup = formatDay(row.pickupTime)
|
||||
if (fromPickup !== '—') return fromPickup
|
||||
const fromDelivery = formatDay(row.deliveryTime)
|
||||
if (fromDelivery !== '—') return fromDelivery
|
||||
return selectedDate.value
|
||||
}
|
||||
|
||||
function cardTitle(row) {
|
||||
const names = (row.stops || [])
|
||||
.flatMap((stop) => (stop.products || []).map((p) => (p.productName || '').trim()))
|
||||
.filter(Boolean)
|
||||
const uniqueNames = [...new Set(names)]
|
||||
const productLabel = uniqueNames.length ? uniqueNames.join('、') : '—'
|
||||
return `${cardTitleDate(row)} ${productLabel}`
|
||||
}
|
||||
|
||||
function tagClass(row) {
|
||||
if (row.planVoided || row.statusLabel === '已作废') return 'tag--danger'
|
||||
if (row.planArchived || row.statusLabel === '已归档') return 'tag--muted'
|
||||
return 'tag--success'
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await sitePlanApi.history({
|
||||
date: selectedDate.value,
|
||||
q: keyword.value.trim() || undefined
|
||||
})
|
||||
records.value = res.records || []
|
||||
} catch (e) {
|
||||
records.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onCalendarChange(e) {
|
||||
const v = e?.fulldate || e?.date || (typeof e === 'string' ? e : '')
|
||||
if (typeof v === 'string' && /^\d{4}-\d{2}-\d{2}/.test(v)) {
|
||||
selectedDate.value = v.slice(0, 10)
|
||||
loadList()
|
||||
}
|
||||
}
|
||||
|
||||
function openRecord(row) {
|
||||
const id = row.planId || row.plan_id
|
||||
if (!id) {
|
||||
uni.showToast({ title: '计划无效', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.navigateTo({ url: `${PAGE_PATHS.plTransportPlanDetail}?planId=${id}` })
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack({ fail: () => uni.reLaunch({ url: PAGE_PATHS.workbench }) })
|
||||
}
|
||||
|
||||
watch(keyword, () => {
|
||||
clearTimeout(keywordTimer)
|
||||
keywordTimer = setTimeout(loadList, 300)
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
loadList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 保险:覆盖 uni-calendar 默认蓝选中态(组件内已改为 #0a9f6d) */
|
||||
:deep(.uni-calendar-item--checked),
|
||||
:deep(.uni-calendar-item--isDay),
|
||||
:deep(.uni-calendar-item--multiple) {
|
||||
background-color: #0a9f6d !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
:deep(.uni-calendar-item--isDay-text) {
|
||||
color: #0a9f6d !important;
|
||||
}
|
||||
|
||||
:deep(.uni-calendar-item--checked.uni-calendar-item--isDay-text),
|
||||
:deep(.uni-calendar-item--isDay.uni-calendar-item--isDay-text) {
|
||||
color: #fff !important;
|
||||
}
|
||||
</style>
|
||||
@@ -81,9 +81,12 @@
|
||||
height: var(--sq-header-height, calc(44px + 44px));
|
||||
padding: var(--sq-safe-top, 44px) 12px 0 8px;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #eee;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
/* 对齐首页 mp-header:绿渐变 + 下缘左右圆角;高度保持业务顶栏原值 */
|
||||
background: var(--mp-header-gradient, linear-gradient(145deg, #087f57 0%, #0a9f6d 72%, #d6a343 165%));
|
||||
border-bottom-left-radius: var(--mp-radius-xl, 44rpx);
|
||||
border-bottom-right-radius: var(--mp-radius-xl, 44rpx);
|
||||
box-shadow: 0 16rpx 40rpx rgba(10, 159, 109, 0.12);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bx-page .topbar-left {
|
||||
@@ -96,7 +99,7 @@
|
||||
|
||||
.bx-page .topbar-back {
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
color: #fff;
|
||||
padding: 4px 8px;
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
@@ -110,7 +113,7 @@
|
||||
text-align: left;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
color: #fff;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -544,7 +547,8 @@
|
||||
}
|
||||
|
||||
.bx-page .form-input.error,
|
||||
.bx-page .form-textarea.error {
|
||||
.bx-page .form-textarea.error,
|
||||
.bx-page .picker-trigger.error {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
@@ -1159,6 +1163,37 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bx-page .bx-sheet__search {
|
||||
position: relative;
|
||||
padding: 10px 16px 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.bx-page .bx-sheet__search-input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 36px 0 12px;
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: var(--rs);
|
||||
font-size: 14px;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bx-page .bx-sheet__search-clear {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.bx-page .bx-sheet__body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -11,9 +11,12 @@
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #eee;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
/* 对齐首页 mp-header:绿渐变 + 下缘左右圆角;高度保持业务顶栏原值 */
|
||||
background: var(--mp-header-gradient, linear-gradient(145deg, #087f57 0%, #0a9f6d 72%, #d6a343 165%));
|
||||
border-bottom-left-radius: var(--mp-radius-xl, 44rpx);
|
||||
border-bottom-right-radius: var(--mp-radius-xl, 44rpx);
|
||||
box-shadow: 0 16rpx 40rpx rgba(10, 159, 109, 0.12);
|
||||
color: #fff;
|
||||
height: var(--sq-header-height, calc(44px + 44px));
|
||||
padding-top: var(--sq-safe-top, 44px);
|
||||
box-sizing: border-box;
|
||||
@@ -54,6 +57,12 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 仅顶栏内为白字;弹层等复用 __title 时仍保持深色 */
|
||||
.db-header .db-header__back,
|
||||
.db-header .db-header__title {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.db-search-wrap {
|
||||
padding: 8px 12px 0;
|
||||
background: #fff;
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
/* 运输计划单 · 现场记录/回传正文(三墙绿;顶栏样式见 bx-theme) */
|
||||
.pl-site-plan-page {
|
||||
--pl-site-primary: #059669;
|
||||
--pl-site-brand: #0a9f6d;
|
||||
padding-bottom: 24px;
|
||||
background: #f8fafc;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pl-site-plan-page .filters {
|
||||
margin: 12px 16px;
|
||||
padding: 4px 4px 10px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pl-site-plan-page .search-input {
|
||||
display: block;
|
||||
width: calc(100% - 8px);
|
||||
height: 36px;
|
||||
margin: 8px 4px 0;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #0f172a;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pl-site-plan-page .list-body {
|
||||
padding: 0 16px 24px;
|
||||
}
|
||||
|
||||
.pl-site-plan-page .record-card {
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
padding: 16px 14px 14px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pl-site-plan-page .index-tag {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
min-width: 24px;
|
||||
height: 22px;
|
||||
padding: 0 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px 0 10px 0;
|
||||
background: var(--pl-site-primary);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.pl-site-plan-page .record-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.pl-site-plan-page .card-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.pl-site-plan-page .tag {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.pl-site-plan-page .tag--success {
|
||||
background: #ecfdf5;
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.pl-site-plan-page .tag--danger {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.pl-site-plan-page .tag--muted {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.pl-site-plan-page .meta {
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
line-height: 1.5;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.pl-site-plan-page .plate-hl {
|
||||
color: var(--pl-site-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pl-site-plan-page .stop-block {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
}
|
||||
|
||||
.pl-site-plan-page .stop-block__title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.pl-site-plan-page .page-loading,
|
||||
.pl-site-plan-page .empty-hint {
|
||||
text-align: center;
|
||||
color: #94a3b8;
|
||||
padding: 48px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* detail */
|
||||
.pl-site-plan-detail {
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .section-card {
|
||||
margin: 12px 16px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .kv-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .kv-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .kv-k {
|
||||
color: #64748b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .kv-v {
|
||||
color: #0f172a;
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .stop-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .addr-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .nav-btn {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
background: var(--pl-site-primary);
|
||||
border-radius: 999px;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .product-card {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .product-name {
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .actual-box {
|
||||
margin-top: 12px;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .actual-label {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #047857;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .actual-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .actual-input {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #a7f3d0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .remark-input {
|
||||
width: 100%;
|
||||
min-height: 64px;
|
||||
margin-top: 10px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .attach-label {
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .attach-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .attach-thumb {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 8px;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .attach-add {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed #a7f3d0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--pl-site-primary);
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .page-footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
|
||||
background: #fff;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .footer-btn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
border-radius: 999px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .footer-btn--ghost {
|
||||
background: #ecfdf5;
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.pl-site-plan-detail .footer-btn--primary {
|
||||
background: var(--pl-site-primary);
|
||||
color: #fff;
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
// 统一页顶栏(对齐地磅「仪表盘」白底顶栏)
|
||||
// 统一页顶栏(对齐首页 mp-header:绿渐变 + 下缘左右圆角;高度保持业务顶栏)
|
||||
// 结构:safe-top + nav 行;左侧返回←/标题;右侧预留胶囊;工作台可加用户区
|
||||
|
||||
.sq-nav-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #eee;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
background: var(--mp-header-gradient, linear-gradient(145deg, #087f57 0%, #0a9f6d 72%, #d6a343 165%));
|
||||
border-bottom-left-radius: var(--mp-radius-xl, 44rpx);
|
||||
border-bottom-right-radius: var(--mp-radius-xl, 44rpx);
|
||||
box-shadow: 0 16rpx 40rpx rgba(10, 159, 109, 0.12);
|
||||
color: #fff;
|
||||
height: var(--sq-header-height, calc(44px + 44px));
|
||||
padding-top: var(--sq-safe-top, 44px);
|
||||
box-sizing: border-box;
|
||||
@@ -60,7 +62,7 @@
|
||||
|
||||
.sq-nav-header__back {
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
color: #fff;
|
||||
padding: 4px 8px;
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
@@ -69,7 +71,7 @@
|
||||
.sq-nav-header__title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
color: #fff;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -79,7 +81,7 @@
|
||||
|
||||
.sq-nav-header__meta {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
}
|
||||
@@ -109,7 +111,7 @@
|
||||
.sq-nav-header__user-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
color: #fff;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -121,13 +123,13 @@
|
||||
line-height: 1.3;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #d1d5db;
|
||||
color: #374151;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.65);
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.sq-nav-header__logout:active {
|
||||
background: #f3f4f6;
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
|
||||
.sq-nav-header__logout--disabled {
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
$uni-color-error: #e43d33;
|
||||
$uni-opacity-disabled: 0.3;
|
||||
$uni-text-color-disable:#c0c0c0;
|
||||
$uni-primary: #2979ff !default;
|
||||
$uni-primary: #0a9f6d !default;
|
||||
.uni-calendar-item__weeks-box {
|
||||
flex: 1;
|
||||
/* #ifndef APP-NVUE */
|
||||
|
||||
Reference in new issue
Block a user