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

+8 -2
View File
@@ -148,10 +148,16 @@ export async function checkConvert(id: number | string) {
return { data: unwrapData(res) }
}
export function exportClue(query?: ClueQuery) {
export function exportClue(data: Record<string, any> = {}) {
const { ids, columns, ...rest } = data
const body: Record<string, any> = {
...mapListQuery(rest as ClueQuery),
...(ids ? { ids } : {}),
...(columns ? { columns } : {})
}
return request.post({
url: `${BASE}/export`,
data: mapListQuery(query),
data: body,
responseType: 'blob',
showErrorMessage: false
})
+8 -2
View File
@@ -77,10 +77,16 @@ export function approveTransfer(data: {
})
}
export async function exportApproval(query: ApprovalQuery) {
export async function exportApproval(data: Record<string, any> = {}) {
const { ids, columns, ...rest } = data
const body: Record<string, any> = {
...mapListQuery(rest as ApprovalQuery),
...(ids ? { ids } : {}),
...(columns ? { columns } : {})
}
return request.post({
url: `${BASE}/export`,
data: mapListQuery(query),
data: body,
responseType: 'blob'
})
}
+8 -2
View File
@@ -81,10 +81,16 @@ export function executeChange(id: number | string) {
return request.put({ url: `${BASE}/execute/${String(id)}` })
}
export async function exportChange(query: ChangeQuery) {
export async function exportChange(data: Record<string, any> = {}) {
const { ids, columns, ...rest } = data
const body: Record<string, any> = {
...mapListQuery(rest as ChangeQuery),
...(ids ? { ids } : {}),
...(columns ? { columns } : {})
}
return request.post({
url: `${BASE}/export`,
data: mapListQuery(query),
data: body,
responseType: 'blob'
})
}
+8 -2
View File
@@ -194,10 +194,16 @@ export function uploadAttachments(data: {
})
}
export async function exportContract(query?: ContractQuery) {
export async function exportContract(data: Record<string, any> = {}) {
const { ids, columns, ...rest } = data
const body: Record<string, any> = {
...mapListQuery(rest as ContractQuery),
...(ids ? { ids } : {}),
...(columns ? { columns } : {})
}
return request.post({
url: `${BASE}/export`,
data: mapListQuery(query),
data: body,
responseType: 'blob'
})
}
+8 -2
View File
@@ -60,10 +60,16 @@ export function disableTemplate(id: number | string) {
return request.put({ url: `${BASE}/disable/${String(id)}` })
}
export async function exportTemplate(query: TemplateQuery) {
export async function exportTemplate(data: Record<string, any> = {}) {
const { ids, columns, ...rest } = data
const body: Record<string, any> = {
...mapListQuery(rest as TemplateQuery),
...(ids ? { ids } : {}),
...(columns ? { columns } : {})
}
return request.post({
url: `${BASE}/export`,
data: mapListQuery(query),
data: body,
responseType: 'blob'
})
}
+8 -2
View File
@@ -119,10 +119,16 @@ export function markLost(data: OpportunityMarkLostParams) {
})
}
export async function exportOpportunity(query?: OpportunityQuery) {
export async function exportOpportunity(data: Record<string, any> = {}) {
const { ids, columns, ...rest } = data
const body: Record<string, any> = {
...mapListQuery(rest as OpportunityQuery),
...(ids ? { ids } : {}),
...(columns ? { columns } : {})
}
return request.post({
url: `${BASE}/export`,
data: mapListQuery(query),
data: body,
responseType: 'blob'
})
}
+19
View File
@@ -26,6 +26,15 @@ export function fetchPoolRecordList(data?: Record<string, any>) {
return request.post({ url: '/web/crm/customer/pool/record/list', data })
}
/** 导出公海客户 */
export function fetchExportPoolCustomers(data: Record<string, any> = {}) {
return request.post({
url: '/web/crm/customer/pool/export',
data,
responseType: 'blob'
})
}
// ── 公海池规则 ──
/** 公海池规则列表 */
@@ -62,3 +71,13 @@ export function fetchTogglePoolRule(data: { id: string; status: string }) {
export function fetchExecutePoolRules(data?: Record<string, any>) {
return request.post({ url: '/web/crm/customer/pool/rule/execute', data })
}
/** 导出公海规则 */
export function fetchExportPoolRules(data: Record<string, any> = {}) {
return request.post({
url: '/web/crm/customer/pool/rule/export',
data,
responseType: 'blob'
})
}
+8 -2
View File
@@ -161,10 +161,16 @@ export function invalidQuote(id: string | number, remark?: string) {
return request.post({ url: `${BASE}/invalid`, data: { id: String(id), remark } })
}
export async function exportQuote(query?: QuoteQuery) {
export async function exportQuote(data: Record<string, any> = {}) {
const { ids, columns, ...rest } = data
const body: Record<string, any> = {
...mapListQuery(rest as QuoteQuery),
...(ids ? { ids } : {}),
...(columns ? { columns } : {})
}
return request.post({
url: `${BASE}/export`,
data: mapListQuery(query),
data: body,
responseType: 'blob'
})
}
+3
View File
@@ -104,6 +104,9 @@ export const inventoryApi = {
list(params?: InventoryListParams) {
return dbRequest.get<InventoryRecord[]>('/inventory', { params, timeout: 60000 })
},
export(data: Record<string, any>) {
return dbRequest.post<Blob>('/inventory/export', data, { responseType: 'blob' })
},
get(id: string) {
return dbRequest.get<InventoryRecord>(`/inventory/${id}`)
},
+3
View File
@@ -131,6 +131,9 @@ export const planApi = {
list(params?: PlanListParams) {
return dbRequest.get<Plan[]>('/plans', { params })
},
export(data: Record<string, any>) {
return dbRequest.post<Blob>('/plans/export', data, { responseType: 'blob' })
},
get(id: string) {
return dbRequest.get<Plan>(`/plans/${id}`)
},
+3
View File
@@ -104,6 +104,9 @@ export const weighingApi = {
list(params?: WeighingListParams) {
return dbRequest.get<WeighingRecord[]>('/weighing', { params })
},
export(data: Record<string, any>) {
return dbRequest.post<Blob>('/weighing/export', data, { responseType: 'blob' })
},
get(id: string) {
return dbRequest.get<WeighingRecord>(`/weighing/${id}`)
},
+111
View File
@@ -11,6 +11,7 @@ import {
demoUnits
} from './mock-data'
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
import request from '@/utils/http'
type PageParams = { page?: number; pageSize?: number; keyword?: string }
@@ -387,3 +388,113 @@ export async function fetchAttachmentDownloadUrl(_data: { fileUrl: string; fileN
await delay()
return { downloadUrl: '#' }
}
const DEMO_PRODUCT_EXPORT_COLS = [
{ field: 'id', title: 'id' },
{ field: 'name', title: '商品名称' },
{ field: 'skuCode', title: 'SKU编码' },
{ field: 'categoryName', title: '分类' },
{ field: 'measureValue', title: '计量' },
{ field: 'unitName', title: '单位' },
{ field: 'imageUrl', title: '图片URL' },
{ field: 'isActive', title: '状态' },
{ field: 'createAt', title: '创建时间' }
]
const DEMO_CATEGORY_EXPORT_COLS = [
{ field: 'id', title: 'id' },
{ field: 'name', title: '分类名称' },
{ field: 'parentId', title: '父分类ID' },
{ field: 'parentName', title: '父分类名称' },
{ field: 'sortOrder', title: '排序' },
{ field: 'priceCoefficient', title: '价格系数(%)' },
{ field: 'isActive', title: '状态' },
{ field: 'imageUrl', title: '图片URL' }
]
function pickExportColumns(
all: { field: string; title: string }[],
visible?: string[]
) {
if (!visible?.length) return all
const set = new Set(visible)
const picked = all.filter((c) => set.has(c.field))
return picked.length ? picked : all
}
/** Demo 商品列表导出(Mock 筛行 + 后端 xlsx) */
export async function fetchExportDemoProducts(payload: Record<string, any> = {}) {
await delay()
let list = [...products]
if (payload.categoryId) list = list.filter((p) => p.categoryId === payload.categoryId)
if (payload.isActive === true || payload.isActive === false) {
list = list.filter((p) => p.isActive === payload.isActive)
}
list = filterKeyword(list, ['name', 'skuCode'], payload.keyword)
if (payload.ids?.length) {
const idSet = new Set(payload.ids.map(String))
list = list.filter((p) => idSet.has(String(p.id)))
}
const cols = pickExportColumns(DEMO_PRODUCT_EXPORT_COLS, payload.columns)
const rows = list.map((row) => ({
id: row.id,
name: row.name,
skuCode: (row as any).skuCode || 'SKU-DEMO',
categoryName: row.categoryName,
measureValue: (row as any).measureValue ?? 1,
unitName: row.unitName,
imageUrl: row.imageUrl,
isActive: row.isActive ? '启用' : '禁用',
createAt: row.createAt
}))
return request.post({
url: '/web/admin/demo/xlsx-export',
data: {
sheetTitle: '商品数据',
filenameStem: '商品数据',
columns: cols,
rows
},
responseType: 'blob'
})
}
/** Demo 分类列表导出(Mock 筛行 + 后端 xlsx) */
export async function fetchExportDemoCategories(payload: Record<string, any> = {}) {
await delay()
let list = [...categories]
if (payload.isActive === true || payload.isActive === false) {
list = list.filter((c) => c.isActive === payload.isActive)
}
list = filterKeyword(list, ['name'], payload.keyword)
if (payload.ids?.length) {
const idSet = new Set(payload.ids.map(String))
list = list.filter((c) => idSet.has(String(c.id)))
}
const idToName = new Map(list.map((c) => [c.id, c.name]))
// parent may be outside filtered set — use full categories map
const allNames = new Map(categories.map((c) => [c.id, c.name]))
const cols = pickExportColumns(DEMO_CATEGORY_EXPORT_COLS, payload.columns)
const rows = list.map((row) => ({
id: row.id,
name: row.name,
parentId: row.parentId || '',
parentName: row.parentId ? allNames.get(row.parentId) || '' : '',
sortOrder: row.sortOrder,
priceCoefficient: row.priceCoefficient ?? 0,
isActive: row.isActive ? '启用' : '禁用',
imageUrl: row.imageUrl || ''
}))
return request.post({
url: '/web/admin/demo/xlsx-export',
data: {
sheetTitle: '分类数据',
filenameStem: '分类数据',
columns: cols,
rows
},
responseType: 'blob'
})
}
+13
View File
@@ -55,6 +55,14 @@ export interface PublicIoOrderPage {
items: PublicIoOrderItem[]
}
export interface PublicIoOrderHistoryProduct {
productName: string
model?: string
packageSpec?: string
quantity?: number | null
unit?: string
}
export interface PublicIoOrderHistoryRecord {
kind: 'inbound' | 'outbound' | string
orderId: string
@@ -66,6 +74,11 @@ export interface PublicIoOrderHistoryRecord {
supplierName?: string | null
customerName?: string | null
vehicleNo?: string | null
pickupTime?: string | null
deliveryTime?: string | null
pickupAddress?: string | null
destinationAddress?: string | null
products?: PublicIoOrderHistoryProduct[]
siteAccessToken?: string | null
siteUrl?: string | null
status?: string | null
+9
View File
@@ -64,6 +64,14 @@ export interface PublicUploadResult {
fileName?: string
}
export interface PublicTransportHistoryProduct {
productName: string
model?: string
packageSpec?: string
quantity?: number | null
unit?: string
}
export interface PublicTransportHistoryRecord {
planId: string
planNo: string
@@ -75,6 +83,7 @@ export interface PublicTransportHistoryRecord {
deliveryTime?: string
pickupAddress?: string
destinationAddress?: string
products?: PublicTransportHistoryProduct[]
planVoided?: boolean
planArchived?: boolean
statusLabel?: string
@@ -1,26 +1,20 @@
<template>
<div v-if="hasSelection" class="list-export-dropdown">
<el-button
class="btn-ghost list-export-dropdown__main"
:disabled="disabled"
:loading="exporting"
@click="handleExportFiltered"
>
<el-icon><Download /></el-icon>
导出筛选结果
</el-button>
<el-dropdown
trigger="click"
:disabled="disabled || exporting"
@command="handleCommand"
>
<el-button class="btn-ghost list-export-dropdown__caret" :disabled="disabled || exporting">
<el-icon><ArrowDown /></el-icon>
<el-button class="btn-ghost" :disabled="disabled" :loading="exporting">
<el-icon><Download /></el-icon>
导出
<el-icon class="list-export-dropdown__caret-icon"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="selected" :disabled="!selectedIds?.length">
导出已选
<el-dropdown-item command="filtered">导出筛选结果</el-dropdown-item>
<el-dropdown-item command="selected" :disabled="!selectedCount">
{{ selectedCount ? `导出已选(${selectedCount})` : '导出已选' }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
@@ -40,7 +34,7 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { ArrowDown, Download } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import {
@@ -77,6 +71,8 @@
const exporting = ref(false)
const selectedCount = computed(() => props.selectedIds?.length || 0)
function buildPayload(withIds: boolean): ListExportPayload {
const payload: ListExportPayload = { ...props.getFilterParams() }
if (props.visibleColumns?.length) {
@@ -111,6 +107,7 @@
}
function handleCommand(cmd: string) {
if (cmd === 'filtered') void runExport(false)
if (cmd === 'selected') void runExport(true)
}
</script>
@@ -118,7 +115,7 @@
<style scoped lang="scss">
.list-export-dropdown {
display: inline-flex;
align-items: stretch;
align-items: center;
vertical-align: middle;
:deep(.el-button) {
@@ -129,16 +126,7 @@
}
}
.list-export-dropdown__main {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
margin-right: -1px;
}
.list-export-dropdown__caret {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
min-width: 36px;
padding: 0 10px;
.list-export-dropdown__caret-icon {
margin-left: 4px;
}
</style>
@@ -36,6 +36,7 @@
value-format="YYYY-MM-DD"
clearable
style="width: 100%"
@change="handlePlanDateChange"
/>
</el-form-item>
<el-form-item label="归属公司" prop="belongsCompany">
@@ -383,14 +384,18 @@
</span>
</div>
<div class="pl-plan-logistics-block">
<div class="pl-plan-logistics-block-head">
<div class="pl-plan-logistics-block-wrap">
<el-button
class="pl-plan-logistics-swap-btn"
text
type="primary"
@click="handleSwapPickupDelivery"
>
<el-icon><Sort /></el-icon>取送货互换
</el-button>
<div class="pl-plan-logistics-block">
<h4 class="pl-plan-subtitle">送货信息</h4>
<el-button text type="primary" @click="handleSwapPickupDelivery">
<el-icon><Sort /></el-icon>取送货互换
</el-button>
</div>
<div class="form-grid-2">
<div class="form-grid-2">
<el-form-item label="送货地址" class="form-col-span-2">
<PlAddressWithLibrary
v-model="formData.destinationAddress"
@@ -415,6 +420,7 @@
<el-form-item label="注意事项" class="form-col-span-2">
<el-input v-model="formData.destinationNotes" placeholder="送货注意事项" />
</el-form-item>
</div>
</div>
</div>
</div>
@@ -1339,21 +1345,12 @@ function resetForm() {
basicFormRef.value?.clearValidate()
}
/** 新增时:取货/送达时间默认跟随计划日期;已手改则不覆盖 */
watch(
() => formData.planDate,
(next, prev) => {
if (isEdit.value || isCopy.value) return
const nextVal = next || ''
const prevVal = prev || ''
if (!formData.pickupTime || formData.pickupTime === prevVal) {
formData.pickupTime = nextVal
}
if (!formData.deliveryTime || formData.deliveryTime === prevVal) {
formData.deliveryTime = nextVal
}
}
)
/** 计划日期变更时,取货/送货时间始终跟随;二者仍可单独调整 */
function handlePlanDateChange(val: string | null) {
const nextVal = val || ''
formData.pickupTime = nextVal
formData.deliveryTime = nextVal
}
function handleOpened() {
loadMasterData().then(async () => {
@@ -1792,17 +1789,6 @@ async function handleSubmit(mode: 'save' | 'saveAndArrange' = 'save') {
margin: 0 0 10px;
}
.pl-plan-logistics-block-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.pl-plan-logistics-block-head .pl-plan-subtitle {
margin: 0;
}
.pl-plan-party-select-row {
display: flex;
align-items: flex-start;
@@ -1873,12 +1859,30 @@ async function handleSubmit(mode: 'save' | 'saveAndArrange' = 'save') {
margin-top: 8px;
}
.pl-plan-logistics-row {
margin-top: 34px;
}
.pl-plan-party-row .pl-plan-party-block,
.pl-plan-logistics-row .pl-plan-logistics-block {
.pl-plan-logistics-row .pl-plan-logistics-block,
.pl-plan-logistics-row .pl-plan-logistics-block-wrap {
flex: 1 1 0;
min-width: 0;
}
.pl-plan-logistics-block-wrap {
position: relative;
}
.pl-plan-logistics-swap-btn {
position: absolute;
top: -32px;
right: 0;
z-index: 1;
height: 26px;
padding: 0 4px;
}
.pl-plan-flow-bridge {
flex: 0 0 88px;
display: flex;
@@ -1934,10 +1938,6 @@ async function handleSubmit(mode: 'save' | 'saveAndArrange' = 'save') {
margin: 0 0 8px;
}
.pl-plan-logistics-block-head {
margin: 0 0 8px;
}
:deep(.el-form-item) {
margin-bottom: 0;
}
+49 -9
View File
@@ -88,7 +88,27 @@ dbAxios.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
})
dbAxios.interceptors.response.use(
(res) => res.data,
async (res) => {
if (res.config.responseType === 'blob') {
const contentType = String(res.headers['content-type'] || '')
if (contentType.includes('application/json')) {
const text = await (res.data as Blob).text()
try {
const json = JSON.parse(text) as { detail?: string; msg?: string }
const message =
typeof json.detail === 'string'
? json.detail
: json.msg || '导出失败'
ElMessage.error(message)
return Promise.reject(new Error(message))
} catch {
/* 非 JSON blob 直出 */
}
}
return res.data
}
return res.data
},
async (err) => {
const status = err.response?.status
const config = err.config as RetryableConfig | undefined
@@ -142,16 +162,36 @@ dbAxios.interceptors.response.use(
const showErrorMessage = config?.showErrorMessage !== false
if (showErrorMessage) {
const detail = err.response?.data?.detail
const message =
typeof detail === 'string'
? detail
: Array.isArray(detail)
? detail.map((x: { msg?: string }) => x.msg).filter(Boolean).join(';')
: err.response?.data?.msg
let message: string | undefined
const data = err.response?.data
if (data instanceof Blob) {
try {
const text = await data.text()
const json = JSON.parse(text) as { detail?: string; msg?: string }
message = typeof json.detail === 'string' ? json.detail : json.msg
} catch {
/* ignore */
}
} else {
const detail = data?.detail
message =
typeof detail === 'string'
? detail
: Array.isArray(detail)
? detail.map((x: { msg?: string }) => x.msg).filter(Boolean).join(';')
: data?.msg
}
ElMessage.error(message || err.message || '网络异常')
}
return Promise.reject(err)
return Promise.reject(
err instanceof Error && err.message
? err
: new Error(
(typeof err.response?.data?.detail === 'string'
? err.response.data.detail
: err.message) || '网络异常'
)
)
}
)
+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>