临时收纳点照片上传

master
lzq 2025-08-25 17:18:35 +08:00
parent 444c7d3684
commit 7e20000c43
12 changed files with 274 additions and 36 deletions

View File

@ -150,6 +150,10 @@ export function beginOfMonth(date?: DateTime<true | false>) {
return date.startOf('month')
}
export function toDate(date: DateTime<true | false>) {
return date.toJSDate()
}
export default {
now,
parse,
@ -158,4 +162,5 @@ export default {
FMT,
endOfMonth,
beginOfMonth,
toDate,
}

View File

@ -0,0 +1,140 @@
<script lang="ts" setup>
import {
TableColumn,
TableColumnSelectable
} from '@idux/components/table'
import { TablePagination } from '@idux/components/table/src/types'
import { useAccessorAndControl } from '@idux/cdk/forms'
import { useFormItemRegister } from '@idux/components/form'
const props = defineProps<{
control?: string | number | (string | number)[] | object
disabled?: boolean
value?: string
placeholder?: string
searchFormPlaceholder?: string
columns: TableColumn<any>[]
getDatasource: (params: any) => Promise<G.PageResult<any>>
getKey: string | ((record: any) => any)
getLabel: string | ((record: any) => any)
}>()
// const emit = defineEmits([ 'update:value' ])
// useAccessorAndControl props control, disabled, value
const {accessor, control: controlRef} = useAccessorAndControl()
// FormItem control, FormItem
useFormItemRegister(controlRef)
// blur
const onBlur = () => {
accessor.markAsBlurred()
}
const inputStatus = ref<'valid' | 'invalid' | 'validating'>('validating')
controlRef.value?.watchValue((newVal) => {
console.log(222, controlRef.value?.valid, newVal)
if (controlRef.value?.valid.value) {
inputStatus.value = 'valid'
} else {
inputStatus.value = 'invalid'
}
})
const valueRef = reactive<{
key?: string
label?: string
}>({})
const visible = ref(false)
const selectedRowKeys = ref<string[]>([])
const selectableColumn = reactive<TableColumnSelectable<ComboboxTableTypes.TableData>>({
type: 'selectable',
align: 'center',
multiple: false,
showIndex: false,
trigger: 'click',
onChange: (selectedKeys, selectedRows) => {
if (selectedKeys.length === 0) {
valueRef.key = undefined
valueRef.label = undefined
} else {
valueRef.key = selectedKeys[0] as string
if (props.getLabel instanceof Function) {
valueRef.label = props.getLabel(selectedRows[0])
} else {
valueRef.label = selectedRows[0][props.getLabel as string]
}
}
accessor.setValue(valueRef.key)
// emit('update:value', valueRef.key)
},
})
const columns_: TableColumn<any>[] = [
selectableColumn,
...props.columns
]
const datasource = ref<ComboboxTableTypes.TableData[]>([])
const tableSpin = ref(false)
const keywords = ref('')
function obtainData() {
tableSpin.value = true
props.getDatasource({
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
keywords: keywords.value
}).then(res => {
pagination.pageIndex = res.current
pagination.pageSize = res.size
pagination.total = res.total
datasource.value = res.records
}).finally(() => {
tableSpin.value = false
})
}
const pagination = reactive<TablePagination>({
pageIndex: 1,
pageSize: 10,
total: 0,
size: 'sm',
showTotal: true,
onChange: obtainData
})
watch(visible, (newVal) => {
if (newVal) {
obtainData()
}
})
</script>
<template>
<IxPopover v-model:visible="visible" closable placement="bottom" trigger="click">
<IxInput v-model:value="valueRef.label" :placeholder="placeholder" :status="inputStatus" readonly @blur="onBlur"/>
<template #header>
<IxForm class="search-form" layout="inline" @submit.prevent="obtainData">
<IxFormItem>
<IxInput v-model:value="keywords" :placeholder="searchFormPlaceholder" clearable @clear="obtainData"/>
</IxFormItem>
<IxFormItem>
<IxButton mode="primary" type="submit">查询</IxButton>
</IxFormItem>
</IxForm>
</template>
<template #content>
<IxTable v-model:selectedRowKeys="selectedRowKeys" :columns="columns_" :dataSource="datasource" :get-key="getKey" :pagination="pagination" :spin="tableSpin"/>
</template>
</IxPopover>
</template>
<style lang="stylus" scoped>
.search-form {
width 100%
height 4rem
padding: var(--ix-padding-size-sm) var(--ix-padding-size-md);
border 1px solid #E5E7EB
}
</style>

View File

@ -0,0 +1,8 @@
export {}
declare global {
namespace ComboboxTableTypes {
interface TableData {
[key: string]: any;
}
}
}

View File

@ -8,6 +8,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
Charts: typeof import('./../components/echarts/Charts.vue')['default']
ComboboxTable: typeof import('./../components/combobox-table/ComboboxTable.vue')['default']
Dialog: typeof import('./../components/dialog/Dialog.vue')['default']
District: typeof import('./../components/district/District.vue')['default']
Funbar: typeof import('./../components/fun-bar/Funbar.vue')['default']
@ -18,6 +19,7 @@ declare module 'vue' {
IxCard: typeof import('@idux/components/card')['IxCard']
IxCol: typeof import('@idux/components/grid')['IxCol']
IxDatePicker: typeof import('@idux/components/date-picker')['IxDatePicker']
IxDateRangePicker: typeof import('@idux/components/date-picker')['IxDateRangePicker']
IxDivider: typeof import('@idux/components/divider')['IxDivider']
IxDropdown: typeof import('@idux/components/dropdown')['IxDropdown']
IxEmpty: typeof import('@idux/components/empty')['IxEmpty']

View File

@ -7,6 +7,7 @@ import Charts from '@/components/echarts/Charts.vue'
import DisposeRecodeDetail from '@/pages/dispose-recode/DisposeRecodeDetail.vue'
import DisposeRecodeApi from '@/pages/dispose-recode/dispose-recode-api.ts'
import Times from '@/common/utils/times.ts'
import times, { FMT } from '@/common/utils/times.ts'
const disposeRecodeDetail = ref<InstanceType<typeof DisposeRecodeDetail> | null>(null)
const chartOption = reactive<Echarts.Option>({
@ -104,6 +105,11 @@ const columns: TableColumn[] = [
title: '出场时间',
dataKey: 'outTime',
},
{
title: '状态',
dataKey: 'timeOutStatusTxt',
customCell: 'timeOutStatus',
},
{
title: '操作',
key: 'action',
@ -144,10 +150,21 @@ function searchHandler() {
})
}
function exportHandler() {
DisposeRecodeApi.exportData([ '2025-01-01', '2025-01-31' ])
function initExportDate() {
const now = new Date()
const beginOfMonth = times.beginOfMonth(times.parse(now))
return [ beginOfMonth.toJSDate(), now ]
}
const dateValue = ref<Date[]>(initExportDate())
function exportHandler() {
DisposeRecodeApi.exportData([ times.format(dateValue.value[0], FMT.date), times.format(dateValue.value[1], FMT.date) ])
}
function disabledDate(date: Date) {
return times.parse(date).diffNow().toMillis() > 0
}
const monthValue = ref<Date>(new Date())
function renderChart() {
@ -199,10 +216,19 @@ onMounted(() => {
<IxButton icon="search" mode="primary" type="submit"/>
</IxFormItem>
</IxForm>
<IxButton icon="download" mode="primary" @click="exportHandler"></IxButton>
<IxPopconfirm icon="calendar" title="选择导出时间" @ok="exportHandler">
<IxButton icon="download" mode="primary">导出</IxButton>
<template #content>
<IxDateRangePicker v-model:value="dateValue" :disabled-date="disabledDate"/>
</template>
</IxPopconfirm>
</div>
</template>
<IxTable :columns="columns" :dataSource="datasource" :pagination="pagination" autoHeight class="data-table" getKey="id">
<template #timeOutStatus="{record}">
<IxTag v-if="record.timeOutStatus === 'WeiChaoShi'" status="success"></IxTag>
<IxTag v-else status="error">已超时</IxTag>
</template>
<template #action="{record}">
<IxButton class="detail-btn" icon="eye" mode="text" @click="disposeRecodeDetail?.open(record)"></IxButton>
</template>

View File

@ -18,6 +18,8 @@ const data = reactive<DisposeRecodeTypes.DisposeRecodeData>({
inBodyPhoto: '',
outFrontPhoto: '',
outBodyPhoto: '',
timeOutStatus: '',
timeOutStatusTxt: ''
})
const container = ref<HTMLElement | undefined>(undefined)
@ -44,31 +46,45 @@ defineExpose({
:mask="false"
:on-before-close="closeHandler" cancel-text="关闭" class="dispose-recode-detail" draggable header="处置详情" maskClosable type="default" width="50rem">
<div class="dispose-recode-detail-info">
<p class="detail-item"><span>来源地</span><span>{{ data.origin }}</span></p>
<p class="detail-item"><span>清运公司</span><span>{{ data.clearingCompany }}</span></p>
<p class="detail-item"><span>车牌号</span><span>{{ data.licensePlate }}</span></p>
<p class="detail-item"><span>联系人</span><span>{{ data.contact }}</span></p>
<p class="detail-item"><span>联系电话</span><span>{{ data.contactPhone }}</span></p>
<p class="detail-item"><span>进场磅重</span><span>{{ data.inWeight }}</span></p>
<p class="detail-item"><span>出场磅重</span><span>{{ data.outWeight }}</span></p>
<p class="detail-item"><span>净重</span><span>{{ data.suttleWeight }}</span></p>
<p class="detail-item"><span>进场时间</span><span>{{ data.inTime }}</span></p>
<p class="detail-item"><span>出场时间</span><span>{{ data.outTime }}</span></p>
<p class="detail-item"><span>消纳场名称</span><span>{{ data.disposalSite }}</span></p>
<p class="detail-item"><span>来源地</span><span>{{ data.origin ?? '-' }}</span></p>
<p class="detail-item"><span>清运公司</span><span>{{ data.clearingCompany ?? '-' }}</span></p>
<p class="detail-item"><span>车牌号</span><span>{{ data.licensePlate ?? '-' }}</span></p>
<p class="detail-item"><span>联系人</span><span>{{ data.contact ?? '-' }}</span></p>
<p class="detail-item"><span>联系电话</span><span>{{ data.contactPhone ?? '-' }}</span></p>
<p class="detail-item"><span>进场磅重</span><span>{{ data.inWeight ?? '-' }}</span></p>
<p class="detail-item"><span>出场磅重</span><span>{{ data.outWeight ?? '-' }}</span></p>
<p class="detail-item"><span>净重</span><span>{{ data.suttleWeight ?? '-' }}</span></p>
<p class="detail-item"><span>进场时间</span><span>{{ data.inTime ?? '-' }}</span></p>
<p class="detail-item"><span>出场时间</span><span>{{ data.outTime ?? '-' }}</span></p>
<p class="detail-item"><span>消纳场名称</span><span>{{ data.disposalSite ?? '-' }}</span></p>
</div>
<div class="dispose-recode-detail-img-title">车辆照片</div>
<div class="dispose-recode-detail-img">
<p class="detail-item img-card"><span class="img-title">进场车头照</span><img :src="data.inFrontPhoto" alt="进场前"></p>
<p class="detail-item img-card"><span class="img-title">进场车身照</span><img :src="data.inBodyPhoto" alt="进场后"></p>
<p class="detail-item img-card"><span class="img-title">出出场车头照</span><img :src="data.outFrontPhoto" alt="出场前"></p>
<p class="detail-item img-card"><span class="img-title">出场车身照</span><img :src="data.outBodyPhoto" alt="出场后"></p>
<div class="detail-item img-card">
<div class="img-title">进场车头照</div>
<IxImage :src="data.inFrontPhoto" alt="进场前"/>
</div>
<div class="detail-item img-card">
<div class="img-title">进场车身照</div>
<IxImage :src="data.inBodyPhoto" alt="进场后"/>
</div>
<div class="detail-item img-card">
<div class="img-title">出出场车头照</div>
<IxImage :src="data.outFrontPhoto" alt="出场前"/>
</div>
<div class="detail-item img-card">
<div class="img-title">出场车身照</div>
<IxImage :src="data.outBodyPhoto" alt="出场后"/>
</div>
</div>
<div class="dispose-recode-detail-img-title">装车照片</div>
<div v-if="data.tspPhotos == null || data.tspPhotos.length === 0">
<IxEmpty/>
</div>
<div v-else class="dispose-recode-detail-img">
<p v-for="tspPhoto in data.tspPhotos" class="detail-item img-card"><span class="img-title"></span><img :src="tspPhoto" alt="装车照片"></p>
<p v-for="tspPhoto in data.tspPhotos" class="detail-item img-card"><span class="img-title"></span>
<IxImage :src="tspPhoto" alt="装车照片"/>
</p>
</div>
<template #footer="{ cancel:_, ok }">
@ -96,16 +112,21 @@ defineExpose({
padding .5rem
border-radius .5rem
.img-title {
text-align center
}
:deep(.ix-image) {
img {
width: 100%; /* 图片宽度适应列宽 */
height: auto; /* 保持图片原始比例 */
object-fit: cover; /* 图片会被裁剪以适应比例,保持填充 */
width: 100%;
height: auto;
object-fit: cover;
min-height unset
min-width unset;
}
}
}
.img-title {
text-align center
}
.detail-item {

View File

@ -18,6 +18,8 @@ declare global {
inBodyPhoto: string
outFrontPhoto: string
outBodyPhoto: string
timeOutStatus: string
timeOutStatusTxt: string
tspPhotos?: string[]
}

View File

@ -123,6 +123,7 @@ function submitPhoto() {
}"
:request-data="requestData"
dragable
accept="image/*"
multiple
@file-status-change="fileStatusChange">
<div class="drag-panel">

View File

@ -31,6 +31,7 @@ defineExpose({
<template>
<div :id="id" ref="containerElement" class="ix_modal-container">
<IxModal v-model:visible="show" :container="containerElement!"
:destroy-on-hide="true"
:header="header" :mask="false"
:on-before-close="closeHandler"
type="default" width="100%">

View File

@ -12,9 +12,9 @@ const createTsp = ref<InstanceType<typeof CreateTsp> | null>(null)
// const videoPanel = ref<InstanceType<typeof VideoPanel> | null>(null)
const photoPanel = ref<InstanceType<typeof PhotoPanel> | null>(null)
const dataSource: SelectData[] = [
{key: '', label: '全部状态'},
{key: 'ZhengChang', label: '正常运营'},
{key: 'FenZhengChang', label: '非正常运营'},
{key: '', label: '全部'},
{key: 'ZhengChang', label: '今日已上传'},
{key: 'FenZhengChang', label: '今日未上传'},
]
const pagination = reactive<TablePagination>({
@ -54,7 +54,7 @@ const columns: TableColumn[] = [
dataKey: 'propertyManagement'
},
{
title: '运营状态',
title: '状态',
dataKey: 'statusTxt',
customCell: 'status',
},
@ -126,7 +126,7 @@ onMounted(() => {
</IxCard>
<IxCard>
<div>
<div>正常运营数量</div>
<div>已上传数量</div>
<div>{{ statisticsResult.onlineCount }}</div>
<div><!--<i class="iconfont icon-arrow-up"></i>较上月增长 {{ statisticsResult.onlineCountGrowthRate }}%--></div>
</div>
@ -134,7 +134,7 @@ onMounted(() => {
</IxCard>
<IxCard>
<div>
<div>非正常运营数量</div>
<div>未上传数量</div>
<div>{{ statisticsResult.offlineCount }}</div>
<div><!--<i class="iconfont icon-arrow-up"></i>较上月增长 {{ statisticsResult.offlineCountGrowthRate }}%--></div>
</div>
@ -173,8 +173,8 @@ onMounted(() => {
<IxButton class="video-btn" icon="eye" mode="text" @click="playPhoto(record)"></IxButton>
</template>
<template #status="{record}">
<IxTag v-if="record.status === 'ZhengChang'" status="success">{{ record.statusTxt }}</IxTag>
<IxTag v-else status="error">{{ record.statusTxt }}</IxTag>
<IxTag v-if="record.status === 'ZhengChang'" status="success"></IxTag>
<IxTag v-else status="error">今日未上传</IxTag>
</template>
</IxTable>
</IxCard>

View File

@ -5,6 +5,9 @@ import {
} from '@idux/cdk'
import Toast from '@/components/toast'
import TspApi from '@/pages/tsp/tsp-api.ts'
import ComboboxTable from '@/components/combobox-table/ComboboxTable.vue'
import { TableColumn } from '@idux/components/table'
import UserApi from '@/pages/sys/user/user-api.ts'
const show = ref(false)
const formGroup = useFormGroup<TspTypes.AddParam>({
@ -12,12 +15,35 @@ const formGroup = useFormGroup<TspTypes.AddParam>({
streetName: [ undefined, Validators.required ],
microdistrict: [ undefined, Validators.required ],
propertyManagement: [ undefined, Validators.required ],
videoUrl: [ undefined, Validators.required ],
// videoUrl: [ undefined, Validators.required ],
managerId: [ undefined, Validators.required ],
})
const managerTableColumns: TableColumn<TspTypes.TspData>[] = [
{
title: '用户名称',
dataKey: 'nickname',
},
{
title: '账号',
dataKey: [ 'account', 'username' ],
},
]
function managerDataSource(params: {
pageIndex: number,
pageSize: number,
keywords?: string
}) {
return UserApi.paging({current: params.pageIndex, size: params.pageSize, nickname: params.keywords})
.then(res => res.data)
}
const gutter = [ 40, 0 ]
function submit() {
console.log('submit', formGroup.getValue())
if (formGroup.valid.value) {
TspApi.add({...formGroup.getValue(), status: 'ZhengChang'})
.then(_ => {
@ -54,8 +80,13 @@ defineExpose({
<IxFormItem :gutter="gutter" label="所属物业" messageTooltip>
<IxInput control="propertyManagement" placeholder="请输入物业名称"/>
</IxFormItem>
<IxFormItem :gutter="gutter" label="视频地址" messageTooltip>
<!--<IxFormItem :gutter="gutter" label="视频地址" messageTooltip>
<IxInput control="videoUrl" placeholder="请输入视频地址"/>
</IxFormItem>-->
<IxFormItem :gutter="gutter" label="负责人" messageTooltip>
<ComboboxTable :columns="managerTableColumns" :get-datasource="managerDataSource"
control="managerId"
get-key="id" get-label="nickname" placeholder="请选择负责人" search-form-placeholder="请输入负责人名称"/>
</IxFormItem>
</IxForm>
</IxModal>

View File

@ -18,6 +18,7 @@ declare global {
propertyManagement?: string
status?: 'ZhengChang' | 'FenZhengChang'
videoUrl?: string
managerId?: string
}
interface SearchParam {