This commit is contained in:
zlwu committed 2026-08-22 09:08:05 +08:00
1 parent 833544def3
commit 16be6fa22d
204 files changed
+455089 -46491

No files matched your search

+27 -24
View File
@@ -185,6 +185,14 @@
<el-icon><CircleClose /></el-icon>标为无效
</el-button>
<ListExportDropdown
:export-fn="exportClue"
:get-filter-params="getClueExportFilterParams"
:has-selection="true"
:selected-ids="ids.map(String)"
:visible-columns="clueExportColumns"
filename="线索列表.xlsx"
/>
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
@@ -504,6 +512,7 @@
import AssignForm from './assignForm.vue'
import InvalidForm from './invalidForm.vue'
import DetailDrawer from './detailDrawer.vue'
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
@@ -579,12 +588,29 @@
]
})
const clueExportColumns = computed(() => {
const cols = [...visibleColumns.value]
if (!cols.includes('company')) cols.unshift('company')
return cols
})
function getClueExportFilterParams() {
const params = buildQueryParams()
delete params.pageNum
delete params.pageSize
if (activeView.value === 'mine') params.viewMode = 'mine'
else if (activeView.value === 'toFollow') params.viewType = 'toFollow'
else if (activeView.value === 'today') params.viewType = 'todayNew'
else if (activeView.value === 'converted') params.viewType = 'converted'
return params
}
const importDialogVisible = ref(false)
const importFileList = ref<UploadFile[]>([])
const importLoading = ref(false)
const importUpdateSupport = ref(false)
const templateLoading = ref(false)
const exportLoading = ref(false)
const canConvertSelected = computed(() => {
if (selectedRows.value.length !== 1) return false
@@ -852,29 +878,6 @@
}
}
async function handleExport() {
exportLoading.value = true
try {
const params = buildQueryParams()
const res = await exportClue(params)
const blob =
res instanceof Blob
? res
: new Blob([res as BlobPart], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `线索列表_${new Date().getTime()}.xlsx`
link.click()
URL.revokeObjectURL(url)
} catch {
ElMessage.error('导出失败')
} finally {
exportLoading.value = false
}
}
watchDebounced(
() => ({ ...searchForm, dateRange: dateRange.value }),
@@ -130,9 +130,14 @@
>
<el-icon><Upload /></el-icon>上传扫描件
</el-button>
<el-button class="btn-ghost" :loading="exportLoading" @click="handleExport">
<el-icon><Download /></el-icon>导出
</el-button>
<ListExportDropdown
:export-fn="exportContract"
:get-filter-params="getContractExportFilterParams"
:has-selection="true"
:selected-ids="ids.map(String)"
:visible-columns="contractExportColumns"
filename="合同列表.xlsx"
/>
<ListToolbarEndActions
:loading="loading"
@@ -562,6 +567,7 @@ import {
openCrmAttachmentPreviewInNewTab
} from '@/utils/crm-attachment'
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
@@ -578,7 +584,6 @@ const isManageMode = computed(() => props.mode === 'manage')
const pageTitle = computed(() => (isManageMode.value ? '合同管理' : '合同审批'))
const userStore = useUserStore();
const exportLoading = ref(false);
/** 当前登录用户 ID(后端 current-user 返回 id,兼容历史 userId 字段) */
const resolveCurrentUserId = (): string => {
@@ -1108,29 +1113,26 @@ const handleView = (row?: ContractVO) => {
contractDetailRef.value?.open(targetRow.id);
};
/** 导出按钮操作 */
const handleExport = async () => {
exportLoading.value = true;
try {
const res = await exportContract({ ...queryParams.value });
const blob =
res instanceof Blob
? res
: new Blob([res as any], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `contract_${new Date().getTime()}.xlsx`;
link.click();
URL.revokeObjectURL(url);
} catch (error) {
console.error('导出失败:', error);
} finally {
exportLoading.value = false;
const contractExportColumns = computed(() => {
const cols = [...visibleColumns.value]
if (!cols.includes('contractNo')) cols.unshift('contractNo')
if (!cols.includes('contractName')) {
const idx = cols.indexOf('contractNo')
cols.splice(idx + 1, 0, 'contractName')
}
};
return cols
})
function getContractExportFilterParams() {
const params: Record<string, any> = { ...queryParams.value }
if (dateRange.value && dateRange.value.length === 2) {
params.signDateBegin = dateRange.value[0]
params.signDateEnd = dateRange.value[1]
}
delete params.pageNum
delete params.pageSize
return params
}
/** 提交审批(单条) */
const handleSubmit = (row?: ContractVO) => {
@@ -70,13 +70,14 @@
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button
class="btn-ghost"
:loading="exportLoading"
@click="handleExport"
>
<el-icon><Download /></el-icon>导出
</el-button>
<ListExportDropdown
:export-fn="exportApproval"
:get-filter-params="getApprovalExportFilterParams"
:has-selection="true"
:selected-ids="selectedRows.map((r) => String(r.id))"
:visible-columns="approvalExportColumns"
filename="合同审批记录.xlsx"
/>
<ListToolbarEndActions
:loading="loading"
@@ -266,6 +267,7 @@
</template>
<script setup lang="ts" name="ContractApproval">
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { ref, reactive, computed, onMounted, watch } from 'vue'
@@ -319,6 +321,24 @@
resetToDefault
} = usePersistedListColumns('crm_contract_approval_visible_columns', listColumnDefs)
const approvalExportColumns = computed(() => {
const cols = [...visibleColumns.value]
if (!cols.includes('contractNo')) cols.unshift('contractNo')
return cols
})
function getApprovalExportFilterParams() {
const params: Record<string, any> = { ...queryParams.value }
if (dateRange.value && dateRange.value.length === 2) {
params.beginTime = dateRange.value[0]
params.endTime = dateRange.value[1]
}
delete params.pageNum
delete params.pageSize
return params
}
const queryParams = ref<ApprovalQuery>({
pageNum: 1,
pageSize: 20,
@@ -356,7 +376,6 @@
const loading = ref(true)
const selectedRows = ref<ApprovalVO[]>([])
const submitLoading = ref(false)
const exportLoading = ref(false)
const total = ref(0)
const dateRange = ref<[string, string]>()
@@ -538,36 +557,6 @@
viewDialog.data = res.data
}
const handleExport = async () => {
if (dateRange.value && dateRange.value.length === 2) {
queryParams.value.beginTime = dateRange.value[0]
queryParams.value.endTime = dateRange.value[1]
} else {
queryParams.value.beginTime = undefined
queryParams.value.endTime = undefined
}
exportLoading.value = true
try {
const res = await exportApproval(queryParams.value)
const blob =
res instanceof Blob
? res
: new Blob([res as BlobPart], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `contract_approval_${new Date().getTime()}.xlsx`
link.click()
URL.revokeObjectURL(url)
} catch (error) {
console.error(error)
$modal.msgError('导出失败')
} finally {
exportLoading.value = false
}
}
onMounted(() => {
getList()
@@ -119,13 +119,14 @@
>
<el-icon><RefreshLeft /></el-icon>撤回
</el-button>
<el-button
class="btn-ghost"
:loading="exportLoading"
@click="handleExport"
>
<el-icon><Download /></el-icon>导出
</el-button>
<ListExportDropdown
:export-fn="exportChange"
:get-filter-params="getChangeExportFilterParams"
:has-selection="true"
:selected-ids="selectedRows.map((r) => String(r.id))"
:visible-columns="changeExportColumns"
filename="合同变更.xlsx"
/>
<ListToolbarEndActions
:loading="loading"
@@ -402,6 +403,7 @@
</template>
<script setup lang="ts" name="ContractChange">
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { ref, reactive, computed, onMounted } from 'vue'
@@ -472,6 +474,24 @@
resetToDefault
} = usePersistedListColumns('crm_contract_change_visible_columns', listColumnDefs)
const changeExportColumns = computed(() => {
const cols = [...visibleColumns.value]
if (!cols.includes('contractNo')) cols.unshift('contractNo')
return cols
})
function getChangeExportFilterParams() {
const params: Record<string, any> = { ...queryParams.value }
if (dateRange.value && dateRange.value.length === 2) {
params.beginTime = dateRange.value[0]
params.endTime = dateRange.value[1]
}
delete params.pageNum
delete params.pageSize
return params
}
const queryParams = ref<ChangeQuery>({
pageNum: 1,
pageSize: 20,
@@ -505,7 +525,6 @@
const selectedRows = ref<ChangeVO[]>([])
const loading = ref(true)
const submitLoading = ref(false)
const exportLoading = ref(false)
const total = ref(0)
const ids = ref<number[]>([])
const dateRange = ref<[string, string]>()
@@ -757,36 +776,6 @@
await refreshViewDialogIfOpen()
}
const handleExport = async () => {
if (dateRange.value && dateRange.value.length === 2) {
queryParams.value.beginTime = dateRange.value[0]
queryParams.value.endTime = dateRange.value[1]
} else {
queryParams.value.beginTime = undefined
queryParams.value.endTime = undefined
}
exportLoading.value = true
try {
const res = await exportChange(queryParams.value)
const blob =
res instanceof Blob
? res
: new Blob([res as BlobPart], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `contract_change_${new Date().getTime()}.xlsx`
link.click()
URL.revokeObjectURL(url)
} catch (error) {
console.error(error)
$modal.msgError('导出失败')
} finally {
exportLoading.value = false
}
}
const cancel = () => {
dialog.visible = false
@@ -75,13 +75,14 @@
>
<el-icon><Delete /></el-icon>删除
</el-button>
<el-button
class="btn-ghost"
:loading="exportLoading"
@click="handleExport"
>
<el-icon><Download /></el-icon>导出
</el-button>
<ListExportDropdown
:export-fn="exportTemplate"
:get-filter-params="getTemplateExportFilterParams"
:has-selection="true"
:selected-ids="selectedRows.map((r) => String(r.id))"
:visible-columns="templateExportColumns"
filename="合同模板.xlsx"
/>
<ListToolbarEndActions
:loading="loading"
@@ -332,6 +333,7 @@
</template>
<script setup lang="ts" name="ContractTemplate">
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { ref, reactive, computed, onMounted } from 'vue'
@@ -397,6 +399,20 @@
resetToDefault
} = usePersistedListColumns('crm_contract_template_visible_columns', listColumnDefs)
const templateExportColumns = computed(() => {
const cols = [...visibleColumns.value]
if (!cols.includes('templateName')) cols.unshift('templateName')
return cols
})
function getTemplateExportFilterParams() {
const params: Record<string, any> = { ...queryParams.value }
delete params.pageNum
delete params.pageSize
return params
}
const queryParams = ref<TemplateQuery>({
pageNum: 1,
pageSize: 20,
@@ -428,7 +444,6 @@
const selectedRows = ref<TemplateVO[]>([])
const loading = ref(true)
const submitLoading = ref(false)
const exportLoading = ref(false)
const total = ref(0)
const ids = ref<number[]>([])
@@ -618,29 +633,6 @@
}
}
const handleExport = async () => {
exportLoading.value = true
try {
const res = await exportTemplate(queryParams.value)
const blob =
res instanceof Blob
? res
: new Blob([res as BlobPart], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `contract_template_${new Date().getTime()}.xlsx`
link.click()
URL.revokeObjectURL(url)
} catch (error) {
console.error(error)
$modal.msgError('导出失败')
} finally {
exportLoading.value = false
}
}
const cancel = () => {
dialog.visible = false
+26 -18
View File
@@ -161,6 +161,14 @@
放入公海
</el-button>
<ListExportDropdown
:export-fn="fetchExportCustomer"
:get-filter-params="getCustomerExportFilterParams"
:has-selection="true"
:selected-ids="selectedRows.map((r) => String(r.id))"
:visible-columns="customerExportColumns"
filename="客户列表.xlsx"
/>
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
@@ -681,6 +689,7 @@ import TransferForm from './TransferDialog.vue'
import IntoPoolForm from './IntoPoolDialog.vue'
import FollowForm from './FollowFormDialog.vue'
import DealForm from './DealDialog.vue'
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
@@ -780,6 +789,23 @@ const {
]
})
const customerExportColumns = computed(() => {
const cols = [...visibleColumns.value]
if (!cols.includes('customerName')) cols.unshift('customerName')
return cols
})
function getCustomerExportFilterParams() {
const params = buildQueryParams()
delete params.page
delete params.pageSize
if (activeView.value && activeView.value !== 'all') {
params.viewMode = activeView.value
}
return params
}
// 子组件引用
const customerFormRef = ref<InstanceType<typeof CustomerForm>>()
const customerDetailRef = ref<InstanceType<typeof CustomerDetail>>()
@@ -986,24 +1012,6 @@ async function handleDelete(row?: any) {
await getList()
}
/** 导出按钮操作 */
async function handleExport() {
try {
const params = buildQueryParams()
const res = await fetchExportCustomer(params)
const blob = new Blob([res as any], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `客户列表_${new Date().getTime()}.xlsx`
link.click()
URL.revokeObjectURL(url)
} catch {
ElMessage.error('导出失败')
}
}
watchDebounced(
() => ({ ...searchForm, dateRange: dateRange.value }),
@@ -92,6 +92,14 @@
手动执行
</el-button>
<ListExportDropdown
:export-fn="fetchExportPoolRules"
:get-filter-params="getPoolRuleExportFilterParams"
:has-selection="true"
:selected-ids="selectedRows.map((r) => String(r.id))"
:visible-columns="poolRuleExportColumns"
filename="公海规则.xlsx"
/>
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
@@ -227,6 +235,7 @@
<script setup lang="ts">
import { watchDebounced } from '@vueuse/core'
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
@@ -247,6 +256,7 @@ import {
fetchDeletePoolRule,
fetchTogglePoolRule,
fetchExecutePoolRules,
fetchExportPoolRules,
} from '@/api/crm/pool'
import RuleDialog from './components/RuleDialog.vue'
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
@@ -276,6 +286,22 @@ const {
resetToDefault
} = usePersistedListColumns('crm_pool_rule_visible_columns', listColumnDefs)
const poolRuleExportColumns = computed(() => {
const cols = [...visibleColumns.value]
if (!cols.includes('poolName')) cols.unshift('poolName')
return cols
})
function getPoolRuleExportFilterParams() {
const quickKeyword = quickSearchKeyword.value?.trim()
return {
poolName: quickKeyword || searchForm.poolName || undefined,
ruleType: searchForm.ruleType || undefined,
status: searchForm.status || undefined
}
}
// 数据列表
const ruleList = ref<any[]>([])
const loading = ref(false)
@@ -71,6 +71,14 @@
分配客户
</el-button>
<ListExportDropdown
:export-fn="fetchExportPoolCustomers"
:get-filter-params="getPoolExportFilterParams"
:has-selection="true"
:selected-ids="ids"
:visible-columns="poolExportColumns"
filename="公海客户.xlsx"
/>
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
@@ -536,7 +544,8 @@ import {
More,
} from '@element-plus/icons-vue'
import { useDictStore } from '@/store/modules/dict'
import { fetchPoolCustomerList } from '@/api/crm/pool'
import { fetchPoolCustomerList, fetchExportPoolCustomers } from '@/api/crm/pool'
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
@@ -623,6 +632,24 @@ const {
]
})
const poolExportColumns = computed(() => {
const cols = [...visibleColumns.value]
if (!cols.includes('customerName')) cols.unshift('customerName')
if (!cols.includes('customerNo')) cols.unshift('customerNo')
return cols
})
function getPoolExportFilterParams() {
const quickKeyword = quickSearchKeyword.value?.trim()
const filterKeyword =
searchForm.customerName?.trim() || searchForm.customerNo?.trim() || undefined
return {
keyword: quickKeyword || filterKeyword,
level: searchForm.level || undefined
}
}
const claimDialogRef = ref()
const assignDialogRef = ref()
const customerDetailRef = ref()
+9 -18
View File
@@ -135,6 +135,14 @@
<el-icon><User /></el-icon>批量分配
</el-button>
<ListExportDropdown
:export-fn="exportOpportunity"
:get-filter-params="getOpportunityExportFilterParams"
:has-selection="true"
:selected-ids="ids.map(String)"
:visible-columns="opportunityExportColumns"
filename="商机列表.xlsx"
/>
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
@@ -587,6 +595,7 @@
import BatchUpdateStageDialog from './components/BatchUpdateStageDialog.vue'
import BatchUpdateDateDialog from './components/BatchUpdateDateDialog.vue'
import OpportunityDetailDrawer from './components/OpportunityDetailDrawer.vue'
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
@@ -594,7 +603,6 @@
const router = useRouter()
const selectedRows = ref<OpportunityVO[]>([])
const exportLoading = ref(false)
const assignLoading = ref(false)
// 列配置
@@ -841,23 +849,6 @@
await getList()
}
/** 导出按钮操作 */
const handleExport = async () => {
exportLoading.value = true
try {
const blob = await exportOpportunity({ ...queryParams.value })
const url = window.URL.createObjectURL(blob as Blob)
const link = document.createElement('a')
link.href = url
link.download = `商机数据_${Date.now()}.xlsx`
link.click()
window.URL.revokeObjectURL(url)
} catch {
ElMessage.error('导出失败')
} finally {
exportLoading.value = false
}
}
/** 变更阶段 */
const handleChangeStage = (row?: OpportunityVO) => {
+23 -24
View File
@@ -111,6 +111,14 @@
<el-icon><CircleClose /></el-icon>作废
</el-button>
<ListExportDropdown
:export-fn="exportQuote"
:get-filter-params="getQuoteExportFilterParams"
:has-selection="true"
:selected-ids="ids.map(String)"
:visible-columns="quoteExportColumns"
filename="报价单列表.xlsx"
/>
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
@@ -867,6 +875,7 @@ import { fitRichTextTablesToMaxWidth } from '@/utils/tinymce-table-excel-resize'
import QuoteFormDialog from './components/QuoteFormDialog.vue'
import QuoteDetailDialog from './components/QuoteDetailDialog.vue'
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import {
@@ -1001,6 +1010,20 @@ const {
]
})
const quoteExportColumns = computed(() => {
const cols = [...visibleColumns.value]
if (!cols.includes('quoteNo')) cols.unshift('quoteNo')
return cols
})
function getQuoteExportFilterParams() {
const params = { ...buildListQueryParams() }
delete params.pageNum
delete params.pageSize
return params
}
type QuoteViewName = 'all' | 'submitted' | 'accepted'
interface TabFilterState {
@@ -1054,7 +1077,6 @@ const loading = ref(true);
const total = ref(0);
const expandLoadingMap = reactive<Record<string, boolean>>({});
const selectedRows = ref<QuoteVO[]>([]);
const exportLoading = ref(false);
const single = ref(true);
const multiple = ref(true);
const ids = ref<(string | number)[]>([]);
@@ -1463,29 +1485,6 @@ const handleQuickSearchClear = () => {
handleQuickSearch();
};
/** 导出按钮操作 */
const handleExport = async () => {
exportLoading.value = true;
try {
const res = await exportQuote(buildListQueryParams())
const blob =
res instanceof Blob
? res
: new Blob([res as any], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `quote_${new Date().getTime()}.xlsx`
link.click()
URL.revokeObjectURL(url)
} catch (error) {
console.error('导出失败:', error)
} finally {
exportLoading.value = false;
}
}
/** 报价分析 */
const handleStatistics = () => {
+20 -51
View File
@@ -112,10 +112,14 @@
<el-icon><Delete /></el-icon>
删除
</el-button>
<el-button class="btn-ghost" :loading="exportLoading" @click="handleExport">
<el-icon><Download /></el-icon>
导出 Excel
</el-button>
<ListExportDropdown
:export-fn="(payload) => inventoryApi.export(payload)"
:get-filter-params="getInventoryExportFilterParams"
:has-selection="true"
:selected-ids="selectedRows.map((r) => String(r.id))"
:visible-columns="inventoryExportColumns"
filename="出入库列表.xlsx"
/>
<ListToolbarEndActions
:loading="loading"
@@ -536,6 +540,7 @@
import DbCrmCustomerSelect from '@/components/business/db/DbCrmCustomerSelect.vue'
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
import type { StatsRowColumn } from '@/utils/list-column-stats'
@@ -592,6 +597,17 @@
resetToDefault
} = usePersistedListColumns('db-inventory-list-columns', listColumnDefs)
const inventoryExportColumns = computed(() => {
const cols = [...visibleColumns.value]
if (!cols.includes('planNo')) cols.unshift('planNo')
return cols
})
function getInventoryExportFilterParams() {
return { ...buildListQueryParams() }
}
const inventoryTableWrapRef = ref<HTMLElement | null>(null)
const inventoryStatsRowRef = ref<{ resync?: () => void } | null>(null)
@@ -631,7 +647,6 @@
return (row as any)?.[prop]
}
const exportLoading = ref(false)
const saving = ref(false)
const tableData = ref<InventoryRecord[]>([])
const allRecords = ref<InventoryRecord[]>([])
@@ -1114,52 +1129,6 @@
}
}
const handleExport = async () => {
exportLoading.value = true
try {
let records = await inventoryApi.list(buildListQueryParams())
if (records.length === 0) {
$modal.msgWarning('没有数据可导出')
return
}
let list = [...records]
if (sortProp.value && sortOrder.value) {
list.sort((a, b) => compare(a, b, sortProp.value, sortOrder.value!))
}
const headers = ['日期', '往来单位', '业务类型', '产品', '数量(吨)', '车牌号', '计划号', '备注']
const csvContent = [
headers.join(','),
...list.map((r) =>
[
r.date,
r.partnerCompany,
r.businessType,
r.product,
r.quantity,
r.vehicleNo,
r.planNo || '',
r.remarks || ''
].join(',')
)
].join('\n')
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
const now = new Date()
const stamp = `${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}_${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`
a.download = `出入库列表_${stamp}.csv`
a.click()
URL.revokeObjectURL(url)
$modal.msgSuccess('导出成功')
} catch (error) {
console.error('导出失败', error)
} finally {
exportLoading.value = false
}
}
useDbInstantSearch(
() => ({
+20 -63
View File
@@ -112,10 +112,14 @@
<el-icon><Delete /></el-icon>
删除
</el-button>
<el-button class="btn-ghost" :loading="exportLoading" @click="handleExport">
<el-icon><Download /></el-icon>
导出 Excel
</el-button>
<ListExportDropdown
:export-fn="(payload) => planApi.export(payload)"
:get-filter-params="getPlanExportFilterParams"
:has-selection="true"
:selected-ids="selectedRows.map((r) => String(r.id))"
:visible-columns="planExportColumns"
filename="计划列表.xlsx"
/>
<ListToolbarEndActions
:loading="loading"
@@ -488,6 +492,7 @@
import DbCrmCustomerSelect from '@/components/business/db/DbCrmCustomerSelect.vue'
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
import type { StatsRowColumn } from '@/utils/list-column-stats'
@@ -534,6 +539,17 @@
resetToDefault
} = usePersistedListColumns('db-plans-list-columns', listColumnDefs)
const planExportColumns = computed(() => {
const cols = [...visibleColumns.value]
if (!cols.includes('planNo')) cols.unshift('planNo')
return cols
})
function getPlanExportFilterParams() {
return { ...buildListQueryParams() }
}
const plansTableWrapRef = ref<HTMLElement | null>(null)
const plansStatsRowRef = ref<{ resync?: () => void } | null>(null)
@@ -565,7 +581,6 @@
}
const loading = ref(false)
const exportLoading = ref(false)
const saving = ref(false)
const tableData = ref<Plan[]>([])
const total = ref(0)
@@ -993,64 +1008,6 @@
formRef.value?.resetFields()
}
const handleExport = async () => {
exportLoading.value = true
try {
const records = await fetchFilteredPlans()
if (records.length === 0) {
$modal.msgWarning('没有数据可导出')
return
}
const headers = [
'计划号',
'往来单位',
'计划日期',
'起始日期',
'截止日期',
'产品',
'规格',
'车牌号',
'计划量',
'已执行量',
'执行状态',
'备注'
]
const csvContent = [
headers.join(','),
...records.map((r) =>
[
r.planNo,
r.customerName,
r.planDate,
r.startDate,
r.endDate,
r.product,
r.specification,
r.vehicleNo,
r.plannedQuantity,
r.executedQuantity || 0,
r.executionStatus,
r.remarks || ''
].join(',')
)
].join('\n')
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
const now = new Date()
const stamp = `${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}_${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`
a.download = `计划列表_${stamp}.csv`
a.click()
URL.revokeObjectURL(url)
$modal.msgSuccess('导出成功')
} catch (error) {
console.error('导出失败', error)
} finally {
exportLoading.value = false
}
}
useDbInstantSearch(
() => ({
+20 -59
View File
@@ -91,10 +91,14 @@
<el-icon><Close /></el-icon>
作废
</el-button>
<el-button class="btn-ghost" :loading="exportLoading" @click="handleExport">
<el-icon><Download /></el-icon>
导出 Excel
</el-button>
<ListExportDropdown
:export-fn="(payload) => weighingApi.export(payload)"
:get-filter-params="getWeighingExportFilterParams"
:has-selection="true"
:selected-ids="selectedRows.map((r) => String(r.id))"
:visible-columns="weighingExportColumns"
filename="地磅单列表.xlsx"
/>
<ListToolbarEndActions
:loading="loading"
@@ -527,6 +531,7 @@
import DbCrmCustomerSelect from '@/components/business/db/DbCrmCustomerSelect.vue'
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
import type { StatsRowColumn } from '@/utils/list-column-stats'
@@ -581,6 +586,17 @@
resetToDefault
} = usePersistedListColumns('db-weighing-list-columns', listColumnDefs)
const weighingExportColumns = computed(() => {
const cols = [...visibleColumns.value]
if (!cols.includes('planNo')) cols.unshift('planNo')
return cols
})
function getWeighingExportFilterParams() {
return { ...buildListQueryParams() }
}
const weighingTableWrapRef = ref<HTMLElement | null>(null)
const weighingStatsRowRef = ref<{ resync?: () => void } | null>(null)
@@ -609,7 +625,6 @@
})
const loading = ref(false)
const exportLoading = ref(false)
const saving = ref(false)
const allData = ref<WeighingRecord[]>([])
const total = ref(0)
@@ -1075,60 +1090,6 @@
formRef.value?.resetFields()
}
const handleExport = async () => {
exportLoading.value = true
try {
const exportRecords = await weighingApi.list(buildListQueryParams())
if (exportRecords.length === 0) {
$modal.msgWarning('没有数据可导出')
return
}
const headers = [
'车牌号',
'货物名称',
'业务类型',
'毛重(kg)',
'皮重(kg)',
'净重(kg)',
'毛重时间',
'皮重时间',
'供货单位',
'收货单位'
]
const csvContent = [
headers.join(','),
...exportRecords.map((r) =>
[
r.vehicleNo,
r.cargoName,
r.businessType,
r.grossWeight,
r.tareWeight,
r.netWeight,
r.grossWeightTime,
r.tareWeightTime,
r.supplier,
r.consignee
].join(',')
)
].join('\n')
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
const now = new Date()
const stamp = `${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}_${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`
a.download = `地磅单列表_${stamp}.csv`
a.click()
URL.revokeObjectURL(url)
$modal.msgSuccess('导出成功')
} catch (error) {
console.error('导出失败', error)
} finally {
exportLoading.value = false
}
}
const clearInventoryRouteQuery = () => {
if (!route.query.inboundOutboundId) return
+29 -11
View File
@@ -47,16 +47,14 @@
批量删除
</el-button>
<ArtExcelExport
:data="batchExportData"
:list-total="pagination.total"
:load-all-data="loadAllCategoriesForExport"
:filename="categoryExportFilename"
sheetName="分类数据"
:columns="categoryExportColumns"
>
批量导出
</ArtExcelExport>
<ListExportDropdown
:export-fn="fetchExportDemoCategories"
:get-filter-params="getDemoCategoryExportFilterParams"
:has-selection="true"
:selected-ids="selectedRows.map((r) => String(r.id))"
:visible-columns="demoCategoryExportVisibleColumns"
filename="分类数据.xlsx"
/>
<ArtExcelExport
:data="categoryImportTemplateData"
@@ -510,7 +508,8 @@
</template>
<script setup lang="ts">
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import { useListMediaActive } from '@/composables/useListMediaActive'
/* eslint-disable vue/no-v-model-argument */
@@ -533,6 +532,7 @@ import ListToolbarEndActions from '@/components/business/list-toolbar-end-action
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import {
fetchCategoryList,
fetchExportDemoCategories,
fetchCreateCategory,
fetchUpdateCategory,
fetchDeleteCategory,
@@ -595,6 +595,24 @@ import ListToolbarEndActions from '@/components/business/list-toolbar-end-action
selectedRows.value = rows
}
const demoCategoryExportVisibleColumns = [
'id',
'name',
'parentId',
'parentName',
'sortOrder',
'priceCoefficient',
'isActive',
'imageUrl'
]
function getDemoCategoryExportFilterParams() {
return {
keyword: searchForm.keyword || undefined,
isActive: searchForm.isActive === '' ? undefined : searchForm.isActive
}
}
const categoryExportFilename = 'base_category_batch_export'
const categoryImportTemplateFilename = 'base_category_import_template'
+30 -10
View File
@@ -62,16 +62,14 @@
批量删除
</el-button>
<ArtExcelExport
:data="batchExportData"
:list-total="pagination.total"
:load-all-data="loadAllProductsForExport"
:filename="productExportFilename"
sheetName="商品数据"
:columns="productExportColumns"
>
批量导出
</ArtExcelExport>
<ListExportDropdown
:export-fn="fetchExportDemoProducts"
:get-filter-params="getDemoProductExportFilterParams"
:has-selection="true"
:selected-ids="selectedRows.map((r) => String(r.id))"
:visible-columns="demoProductExportVisibleColumns"
filename="商品数据.xlsx"
/>
<ListToolbarEndActions :loading="loading" @refresh="loadData" />
</div>
@@ -925,9 +923,11 @@ import ListToolbarEndActions from '@/components/business/list-toolbar-end-action
} from '@element-plus/icons-vue'
import { VueDraggable as draggable } from 'vue-draggable-plus'
import ProductViewDialog from '@/components/business/product-view-dialog/index.vue'
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import {
fetchProductList,
fetchExportDemoProducts,
fetchProductDetail,
fetchCreateProduct,
fetchUpdateProduct,
@@ -973,6 +973,26 @@ import ListToolbarEndActions from '@/components/business/list-toolbar-end-action
selectedRows.value = rows
}
const demoProductExportVisibleColumns = [
'id',
'name',
'skuCode',
'categoryName',
'measureValue',
'unitName',
'imageUrl',
'isActive',
'createAt'
]
function getDemoProductExportFilterParams() {
return {
keyword: searchForm.keyword || undefined,
categoryId: searchForm.categoryId || undefined,
isActive: searchForm.isActive === '' ? undefined : searchForm.isActive
}
}
const productExportFilename = 'base_product_batch_export'
const formatMeasureCell = (v: unknown) => {
if (v == null || v === '') return '-'
@@ -10,48 +10,29 @@
/>
<div class="filters">
<div class="filter-row">
<van-field
class="date-field"
readonly
is-link
clickable
:model-value="pickDate"
placeholder="选择日期"
@click="openDatePicker"
/>
<input
v-model="keyword"
type="search"
class="search-input"
placeholder="供方/需方"
enterkeyhint="search"
/>
</div>
<div class="chips">
<button
v-for="q in quickOptions"
:key="q.offset"
type="button"
class="chip"
:class="{ on: pickDate === q.ymd }"
@click="setQuick(q.offset)"
>
{{ q.label }}
</button>
</div>
</div>
<van-popup v-model:show="showDatePicker" position="bottom" round>
<van-date-picker
v-model="datePickerValue"
title="选择日期"
<van-calendar
class="history-calendar"
:poppable="false"
:show-confirm="false"
:show-title="false"
:show-mark="false"
:safe-area-inset-bottom="false"
switch-mode="month"
color="#059669"
:row-height="28"
:default-date="calendarDate"
:min-date="minDate"
:max-date="maxDate"
@confirm="onDateConfirm"
@cancel="showDatePicker = false"
@select="onCalendarSelect"
/>
</van-popup>
<input
v-model="keyword"
type="search"
class="search-input"
placeholder="车牌号/产品名称"
enterkeyhint="search"
/>
</div>
<van-empty v-if="errorMessage" :description="errorMessage" />
<van-list
@@ -72,16 +53,33 @@
>
<span class="index-tag">{{ index + 1 }}</span>
<div class="record-top">
<span class="plan-no">{{ row.orderNo || row.planNo || '—' }}</span>
<span class="card-title">{{ cardTitle(row) }}</span>
<span class="tag" :class="tagClass(row)">{{ row.statusLabel || '进行中' }}</span>
</div>
<div class="meta">类型:{{ kindLabel(row.kind) }} · 计划:{{ row.planNo || '—' }}</div>
<div class="meta">
往来:{{ row.partnerCompany || '—' }} · 车牌:{{ row.vehicleNo || '—' }}
车牌:<span class="plate-hl">{{ row.vehicleNo || '—' }}</span>
</div>
<div class="meta">
供方:{{ row.supplierName || '—' }} · 需方:{{ row.customerName || '—' }}
取货:{{ formatDay(row.pickupTime) }} · 送货:{{ formatDay(row.deliveryTime) }}
</div>
<div class="meta">
路线:{{ row.pickupAddress || '—' }} → {{ row.destinationAddress || '—' }}
</div>
<template v-if="!(row.products || []).length">
<div class="meta">产品:—</div>
<div class="meta">规格型号:—</div>
<div class="meta">包装规格:—</div>
<div class="meta">数量:— · 单位:—</div>
</template>
<template v-for="(prod, pIndex) in row.products || []" :key="pIndex">
<div class="meta">产品:{{ prod.productName || '—' }}</div>
<div class="meta">规格型号:{{ prod.model || '—' }}</div>
<div class="meta">包装规格:{{ prod.packageSpec || '—' }}</div>
<div class="meta">
数量:{{ formatQty(prod.quantity) }} · 单位:{{ prod.unit || '—' }}
</div>
</template>
<div class="meta">运输计划号:{{ row.planNo || '—' }}</div>
</div>
</van-list>
</div>
@@ -99,9 +97,7 @@
import 'vant/es/toast/style'
import 'vant/es/list/style'
import 'vant/es/field/style'
import 'vant/es/popup/style'
import 'vant/es/date-picker/style'
import 'vant/es/calendar/style'
import '@/styles/public-mobile-vant.scss'
const PAGE_SIZE = 20
@@ -118,19 +114,18 @@
const records = ref<PublicIoOrderHistoryRecord[]>([])
const total = ref(0)
const page = ref(1)
const pickDate = ref(addDays(0))
const pickDate = ref(formatYmd(startOfToday()))
const calendarDate = ref(startOfToday())
const keyword = ref('')
const showDatePicker = ref(false)
const datePickerValue = ref<string[]>(ymdToParts(pickDate.value))
const minDate = new Date(new Date().getFullYear() - 2, 0, 1)
const maxDate = new Date(new Date().getFullYear() + 2, 11, 31)
let requestSeq = 0
const quickOptions = [
{ label: '今日', offset: 0, ymd: addDays(0) },
{ label: '明日', offset: 1, ymd: addDays(1) },
{ label: '后天', offset: 2, ymd: addDays(2) }
]
function startOfToday() {
const d = new Date()
d.setHours(0, 0, 0, 0)
return d
}
function formatYmd(d: Date) {
const y = d.getFullYear()
@@ -139,34 +134,33 @@
return `${y}-${m}-${day}`
}
function addDays(n: number) {
const d = new Date()
d.setHours(0, 0, 0, 0)
d.setDate(d.getDate() + n)
return formatYmd(d)
function formatDay(value?: string | null) {
if (!value) return '—'
const normalized = String(value).replace('T', ' ')
return normalized.length >= 10 ? normalized.slice(0, 10) : normalized
}
function ymdToParts(ymd: string): string[] {
const [y, m, d] = String(ymd || '').split('-')
if (!y || !m || !d) return ymdToParts(addDays(0))
return [y, m, d]
function formatQty(value?: number | null) {
if (value == null) return '—'
return String(value)
}
function openDatePicker() {
datePickerValue.value = ymdToParts(pickDate.value)
showDatePicker.value = true
function cardTitleDate(row: PublicIoOrderHistoryRecord) {
const fromPlan = formatDay(row.planDate)
if (fromPlan !== '—') return fromPlan
const fromPickup = formatDay(row.pickupTime)
if (fromPickup !== '—') return fromPickup
const fromDelivery = formatDay(row.deliveryTime)
if (fromDelivery !== '—') return fromDelivery
return pickDate.value
}
function onDateConfirm({ selectedValues }: { selectedValues: string[] }) {
pickDate.value = selectedValues.join('-')
showDatePicker.value = false
reload()
}
function kindLabel(kind?: string) {
if (kind === 'inbound') return '入库'
if (kind === 'outbound') return '出库'
return '—'
function cardTitle(row: PublicIoOrderHistoryRecord) {
const names = (row.products || [])
.map((p) => (p.productName || '').trim())
.filter(Boolean)
const productLabel = names.length ? names.join('、') : '—'
return `${cardTitleDate(row)} ${productLabel}`
}
function tagClass(row: PublicIoOrderHistoryRecord) {
@@ -183,9 +177,11 @@
router.push({ name: detailRouteName(), params: { token: token.value } })
}
function setQuick(offset: number) {
pickDate.value = addDays(offset)
datePickerValue.value = ymdToParts(pickDate.value)
function onCalendarSelect(date: Date) {
const next = formatYmd(date)
if (next === pickDate.value) return
pickDate.value = next
calendarDate.value = date
reload()
}
@@ -283,7 +279,7 @@
.filters {
margin-bottom: 12px;
padding: 12px 14px;
padding: 4px 4px 10px;
border-radius: 12px;
background: #fff;
border: 1px solid #e2e8f0;
@@ -292,78 +288,45 @@
box-sizing: border-box;
}
.filter-row {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
gap: 8px;
max-width: 100%;
min-width: 0;
}
.history-calendar {
height: auto;
--van-calendar-background: #fff;
--van-calendar-header-shadow: none;
--van-calendar-header-title-height: 32px;
--van-calendar-header-subtitle-font-size: 13px;
--van-calendar-weekdays-height: 24px;
--van-calendar-weekdays-font-size: 11px;
--van-calendar-day-height: 28px;
--van-calendar-day-font-size: 12px;
--van-calendar-day-margin-bottom: 0;
--van-calendar-selected-day-size: 26px;
--van-primary-color: #059669;
.date-field {
padding: 0 12px;
min-height: 40px;
min-width: 0;
width: 100%;
border: 1px solid #cbd5e1;
border-radius: 8px;
align-items: center;
background: #fff;
box-sizing: border-box;
:deep(.van-field__control) {
font-size: 14px;
color: #1e293b;
height: 40px;
line-height: 40px;
:deep(.van-calendar__footer) {
display: none;
}
:deep(.van-field__right-icon) {
margin-right: 0;
color: #64748b;
:deep(.van-calendar__body) {
overflow: hidden;
}
}
.search-input {
display: block;
width: 100%;
width: calc(100% - 8px);
max-width: 100%;
min-width: 0;
height: 40px;
height: 36px;
margin: 8px 4px 0;
padding: 0 12px;
border: 1px solid #cbd5e1;
border-radius: 8px;
font-size: 14px;
color: #1e293b;
color: #0f172a;
background: #fff;
box-sizing: border-box;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.chip {
height: 30px;
padding: 0 12px;
border-radius: 999px;
border: 1px solid #d1fae5;
background: #ecfdf5;
color: #047857;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.chip.on {
background: #059669;
border-color: #059669;
color: #fff;
}
.record-card {
position: relative;
margin-bottom: 10px;
@@ -402,7 +365,7 @@
padding-left: 4px;
}
.plan-no {
.card-title {
font-size: 15px;
font-weight: 700;
color: #0f172a;
@@ -435,7 +398,7 @@
.meta {
font-size: 13px;
color: #64748b;
color: #0f172a;
line-height: 1.45;
word-break: break-all;
@@ -443,4 +406,10 @@
margin-top: 4px;
}
}
.plate-hl {
color: #059669;
font-weight: 700;
font-size: 15px;
}
</style>
@@ -9,66 +9,68 @@
@click-left="goBack"
/>
<van-loading v-if="loading" vertical class="page-loading">加载中...</van-loading>
<van-empty v-else-if="errorMessage" :description="errorMessage" />
<van-loading v-if="bootLoading" vertical class="page-loading">加载中...</van-loading>
<van-empty v-else-if="fatalError" :description="fatalError" />
<template v-else>
<div class="filters">
<div class="filter-row">
<van-field
class="date-field"
readonly
is-link
clickable
:model-value="pickDate"
placeholder="选择日期"
@click="openDatePicker"
/>
<div class="chips">
<button
v-for="q in quickOptions"
:key="q.offset"
type="button"
class="chip"
:class="{ on: pickDate === q.ymd }"
@click="setQuick(q.offset)"
>
{{ q.label }}
</button>
</div>
</div>
</div>
<van-popup v-model:show="showDatePicker" position="bottom" round>
<van-date-picker
v-model="datePickerValue"
title="选择日期"
<van-calendar
class="history-calendar"
:poppable="false"
:show-confirm="false"
:show-title="false"
:show-mark="false"
:safe-area-inset-bottom="false"
switch-mode="month"
color="#059669"
:row-height="28"
:default-date="calendarDate"
:min-date="minDate"
:max-date="maxDate"
@confirm="onDateConfirm"
@cancel="showDatePicker = false"
@select="onCalendarSelect"
/>
</van-popup>
<van-empty v-if="!records.length" description="暂无运输记录" />
<div
v-for="(row, index) in records"
:key="row.planId"
class="record-card"
role="button"
@click="openRecord(row)"
>
<span class="index-tag">{{ index + 1 }}</span>
<div class="record-top">
<span class="plan-no">{{ row.planNo }}</span>
<span class="tag" :class="tagClass(row)">{{ row.statusLabel || '进行中' }}</span>
</div>
<div class="meta">
取货:{{ formatDay(row.pickupTime) }} · 送货:{{ formatDay(row.deliveryTime) }}
</div>
<div class="meta">
线路:{{ row.pickupAddress || '—' }} → {{ row.destinationAddress || '—' }}
</div>
</div>
<van-loading v-if="loading" vertical class="list-loading">加载中...</van-loading>
<van-empty v-else-if="!records.length" description="暂无运输记录" />
<template v-else>
<div
v-for="(row, index) in records"
:key="row.planId"
class="record-card"
role="button"
@click="openRecord(row)"
>
<span class="index-tag">{{ index + 1 }}</span>
<div class="record-top">
<span class="card-title">{{ cardTitle(row) }}</span>
<span class="tag" :class="tagClass(row)">{{ row.statusLabel || '进行中' }}</span>
</div>
<div class="meta">
车牌:<span class="plate-hl">{{ row.licensePlate || '—' }}</span>
</div>
<div class="meta">
取货:{{ formatDay(row.pickupTime) }} · 送货:{{ formatDay(row.deliveryTime) }}
</div>
<div class="meta">
路线:{{ row.pickupAddress || '—' }} → {{ row.destinationAddress || '—' }}
</div>
<template v-if="!(row.products || []).length">
<div class="meta">产品:—</div>
<div class="meta">规格型号:—</div>
<div class="meta">包装规格:—</div>
<div class="meta">数量:— · 单位:—</div>
</template>
<template v-for="(prod, pIndex) in row.products || []" :key="pIndex">
<div class="meta">产品:{{ prod.productName || '—' }}</div>
<div class="meta">规格型号:{{ prod.model || '—' }}</div>
<div class="meta">包装规格:{{ prod.packageSpec || '—' }}</div>
<div class="meta">
数量:{{ formatQty(prod.quantity) }} · 单位:{{ prod.unit || '—' }}
</div>
</template>
<div class="meta">运输计划号:{{ row.planNo || '—' }}</div>
</div>
</template>
</template>
</div>
</PublicMobileShell>
@@ -83,29 +85,27 @@
import PublicMobileShell from '@/layouts/PublicMobileShell.vue'
import 'vant/es/toast/style'
import 'vant/es/field/style'
import 'vant/es/popup/style'
import 'vant/es/date-picker/style'
import 'vant/es/calendar/style'
import '@/styles/public-mobile-vant.scss'
const route = useRoute()
const router = useRouter()
const token = computed(() => String(route.params.token || ''))
const loading = ref(true)
const errorMessage = ref('')
const bootLoading = ref(true)
const loading = ref(false)
const fatalError = ref('')
const records = ref<PublicTransportHistoryRecord[]>([])
const pickDate = ref(addDays(0))
const showDatePicker = ref(false)
const datePickerValue = ref<string[]>(ymdToParts(pickDate.value))
const pickDate = ref(formatYmd(startOfToday()))
const calendarDate = ref(startOfToday())
const minDate = new Date(new Date().getFullYear() - 2, 0, 1)
const maxDate = new Date(new Date().getFullYear() + 2, 11, 31)
const quickOptions = [
{ label: '今日', offset: 0, ymd: addDays(0) },
{ label: '明日', offset: 1, ymd: addDays(1) },
{ label: '后天', offset: 2, ymd: addDays(2) }
]
function startOfToday() {
const d = new Date()
d.setHours(0, 0, 0, 0)
return d
}
function formatYmd(d: Date) {
const y = d.getFullYear()
@@ -114,36 +114,33 @@
return `${y}-${m}-${day}`
}
function addDays(n: number) {
const d = new Date()
d.setHours(0, 0, 0, 0)
d.setDate(d.getDate() + n)
return formatYmd(d)
}
function ymdToParts(ymd: string): string[] {
const [y, m, d] = String(ymd || '').split('-')
if (!y || !m || !d) return ymdToParts(addDays(0))
return [y, m, d]
}
function openDatePicker() {
datePickerValue.value = ymdToParts(pickDate.value)
showDatePicker.value = true
}
function onDateConfirm({ selectedValues }: { selectedValues: string[] }) {
pickDate.value = selectedValues.join('-')
showDatePicker.value = false
load()
}
function formatDay(value?: string) {
if (!value) return '—'
const normalized = value.replace('T', ' ')
return normalized.length >= 10 ? normalized.slice(0, 10) : normalized
}
function formatQty(value?: number | null) {
if (value == null) return '—'
return String(value)
}
function cardTitleDate(row: PublicTransportHistoryRecord) {
const fromPickup = formatDay(row.pickupTime)
if (fromPickup !== '—') return fromPickup
const fromDelivery = formatDay(row.deliveryTime)
if (fromDelivery !== '—') return fromDelivery
return pickDate.value
}
function cardTitle(row: PublicTransportHistoryRecord) {
const names = (row.products || [])
.map((p) => (p.productName || '').trim())
.filter(Boolean)
const productLabel = names.length ? names.join('、') : '—'
return `${cardTitleDate(row)} ${productLabel}`
}
function tagClass(row: PublicTransportHistoryRecord) {
if (row.planVoided || row.statusLabel === '已作废') return 'tag--danger'
if (row.planArchived || row.statusLabel === '已归档') return 'tag--muted'
@@ -154,10 +151,12 @@
router.push({ name: 'PublicPlTransportDriver', params: { token: token.value } })
}
function setQuick(offset: number) {
pickDate.value = addDays(offset)
datePickerValue.value = ymdToParts(pickDate.value)
load()
function onCalendarSelect(date: Date) {
const next = formatYmd(date)
if (next === pickDate.value) return
pickDate.value = next
calendarDate.value = date
load({ silentBoot: true })
}
function openRecord(row: PublicTransportHistoryRecord) {
@@ -169,30 +168,44 @@
router.push({ name: 'PublicPlTransportDriver', params: { token: accessToken } })
}
async function load() {
async function load(options?: { silentBoot?: boolean }) {
loading.value = true
errorMessage.value = ''
if (!options?.silentBoot) {
bootLoading.value = true
fatalError.value = ''
}
try {
const data = await publicTransportApi.getHistory(token.value, pickDate.value)
records.value = data?.records || []
fatalError.value = ''
} catch (e: any) {
errorMessage.value = e?.message || '链接无效或已失效'
records.value = []
if (!options?.silentBoot) {
fatalError.value = e?.message || '链接无效或已失效'
} else {
showFailToast(e?.message || '加载失败')
}
} finally {
loading.value = false
bootLoading.value = false
}
}
onMounted(load)
onMounted(() => load())
</script>
<style scoped lang="scss">
.page-loading {
.page-loading,
.list-loading {
display: flex;
justify-content: center;
padding: 48px 0;
}
.list-loading {
padding: 32px 0;
}
.transport-nav-bar {
margin: -12px -16px 12px;
border-radius: 0 0 12px 12px;
@@ -214,7 +227,7 @@
.filters {
margin-bottom: 12px;
padding: 12px 14px;
padding: 4px 4px 2px;
border-radius: 12px;
background: #fff;
border: 1px solid #e2e8f0;
@@ -223,69 +236,29 @@
box-sizing: border-box;
}
.filter-row {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
max-width: 100%;
}
.history-calendar {
height: auto;
--van-calendar-background: #fff;
--van-calendar-header-shadow: none;
--van-calendar-header-title-height: 32px;
--van-calendar-header-subtitle-font-size: 13px;
--van-calendar-weekdays-height: 24px;
--van-calendar-weekdays-font-size: 11px;
--van-calendar-day-height: 28px;
--van-calendar-day-font-size: 12px;
--van-calendar-day-margin-bottom: 0;
--van-calendar-selected-day-size: 26px;
--van-primary-color: #059669;
.date-field {
flex: 0 1 11.5em;
min-width: 0;
padding: 0 10px;
min-height: 40px;
border: 1px solid #cbd5e1;
border-radius: 8px;
align-items: center;
background: #fff;
box-sizing: border-box;
:deep(.van-field__control) {
font-size: 14px;
color: #1e293b;
height: 40px;
line-height: 40px;
:deep(.van-calendar__footer) {
display: none;
}
:deep(.van-field__right-icon) {
margin-right: 0;
color: #64748b;
:deep(.van-calendar__body) {
overflow: hidden;
}
}
.chips {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 6px;
margin-top: 0;
flex: 1 1 auto;
min-width: 0;
justify-content: flex-end;
}
.chip {
height: 30px;
padding: 0 10px;
border-radius: 999px;
border: 1px solid #d1fae5;
background: #ecfdf5;
color: #047857;
font-size: 12px;
font-weight: 600;
cursor: pointer;
flex-shrink: 0;
white-space: nowrap;
}
.chip.on {
background: #059669;
border-color: #059669;
color: #fff;
}
.record-card {
position: relative;
margin-bottom: 10px;
@@ -324,7 +297,7 @@
padding-left: 4px;
}
.plan-no {
.card-title {
font-size: 15px;
font-weight: 700;
color: #0f172a;
@@ -357,7 +330,7 @@
.meta {
font-size: 13px;
color: #64748b;
color: #0f172a;
line-height: 1.45;
word-break: break-all;
@@ -365,4 +338,10 @@
margin-top: 4px;
}
}
.plate-hl {
color: #059669;
font-weight: 700;
font-size: 15px;
}
</style>