feat(mobile-h5): add CRM/PL option APIs and select-only pickers
Provide customer/product/vehicle pickers that search portal master data without create-on-type. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
4520ccbfcb
commit
a8f59183df
5 files changed
+437
No files matched your search
@@ -0,0 +1,46 @@
|
||||
import api from '@/utils/http'
|
||||
|
||||
export interface CrmCustomerOption {
|
||||
id: string
|
||||
customerName: string
|
||||
customerNo?: string
|
||||
}
|
||||
|
||||
export interface CrmProductOption {
|
||||
id: string
|
||||
productName: string
|
||||
model?: string
|
||||
unit?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
function extractList<T>(res: unknown): T[] {
|
||||
const data = (res as { list?: T[]; rows?: T[] }) || {}
|
||||
return data.list || data.rows || []
|
||||
}
|
||||
|
||||
/** CRM 客户远程搜索(只读) */
|
||||
export async function fetchCustomerOptions(keyword?: string): Promise<CrmCustomerOption[]> {
|
||||
const res = await api.post<{ list?: CrmCustomerOption[] }>({
|
||||
url: '/web/crm/customer/list',
|
||||
data: {
|
||||
page: 1,
|
||||
pageSize: keyword?.trim() ? 50 : 20,
|
||||
...(keyword?.trim() ? { customerName: keyword.trim() } : {})
|
||||
}
|
||||
})
|
||||
return extractList<CrmCustomerOption>(res)
|
||||
}
|
||||
|
||||
/** CRM 产品远程搜索(只读) */
|
||||
export async function fetchProductOptions(keyword?: string): Promise<CrmProductOption[]> {
|
||||
const res = await api.post<{ list?: CrmProductOption[] }>({
|
||||
url: '/web/crm/products/list',
|
||||
data: {
|
||||
page: 1,
|
||||
pageSize: keyword?.trim() ? 50 : 20,
|
||||
...(keyword?.trim() ? { productName: keyword.trim() } : {})
|
||||
}
|
||||
})
|
||||
return extractList<CrmProductOption>(res)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import api from '@/utils/http'
|
||||
|
||||
export interface VehicleOption {
|
||||
id: string
|
||||
licensePlate: string
|
||||
vehicleType?: string
|
||||
}
|
||||
|
||||
function extractList<T>(res: unknown): T[] {
|
||||
const data = (res as { list?: T[]; rows?: T[]; items?: T[] }) || {}
|
||||
return data.list || data.rows || data.items || (Array.isArray(res) ? (res as T[]) : [])
|
||||
}
|
||||
|
||||
/** 登录态:运输车辆(自有 + 三方),只读搜索 */
|
||||
export async function fetchVehicleOptions(q?: string): Promise<VehicleOption[]> {
|
||||
const res = await api.get<{ list?: VehicleOption[]; total?: number }>({
|
||||
url: '/transport/vehicle',
|
||||
params: {
|
||||
...(q?.trim() ? { q: q.trim() } : {}),
|
||||
page: 1,
|
||||
pageSize: 100
|
||||
}
|
||||
})
|
||||
return extractList<VehicleOption>(res)
|
||||
}
|
||||
|
||||
/** 免登:现场页车牌 options(依赖 Task 6 公共接口) */
|
||||
export async function fetchPublicVehiclePlates(params: {
|
||||
q?: string
|
||||
inventoryId?: string
|
||||
planId?: string
|
||||
}): Promise<Array<{ licensePlate: string; vehicleType?: string }>> {
|
||||
const res = await api.get<Array<{ licensePlate: string; vehicleType?: string }> | {
|
||||
list?: Array<{ licensePlate: string; vehicleType?: string }>
|
||||
}>({
|
||||
url: '/public/db/vehicle-plates',
|
||||
params: {
|
||||
q: params.q,
|
||||
inventoryId: params.inventoryId,
|
||||
planId: params.planId
|
||||
}
|
||||
})
|
||||
if (Array.isArray(res)) return res
|
||||
return extractList(res)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<van-popup v-model:show="visible" round position="bottom" :style="{ maxHeight: '70vh' }">
|
||||
<div class="picker-container">
|
||||
<div class="search-container">
|
||||
<van-search
|
||||
v-model="keyword"
|
||||
placeholder="搜索客户名称"
|
||||
shape="round"
|
||||
@search="loadOptions"
|
||||
@clear="loadOptions"
|
||||
@update:model-value="onKeywordInput"
|
||||
/>
|
||||
</div>
|
||||
<div class="option-list">
|
||||
<van-cell
|
||||
v-for="item in options"
|
||||
:key="item.id"
|
||||
:title="item.customerName"
|
||||
:label="item.customerNo"
|
||||
clickable
|
||||
@click="onSelect(item)"
|
||||
/>
|
||||
<van-empty
|
||||
v-if="!loading && options.length === 0"
|
||||
description="暂无匹配,请到门户维护"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { fetchCustomerOptions, type CrmCustomerOption } from '@/api/crmOptionsApi'
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:show', v: boolean): void
|
||||
(e: 'select', item: CrmCustomerOption): void
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const keyword = ref('')
|
||||
const loading = ref(false)
|
||||
const options = ref<CrmCustomerOption[]>([])
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(v) => {
|
||||
visible.value = v
|
||||
if (v) {
|
||||
keyword.value = ''
|
||||
loadOptions()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(visible, (v) => emit('update:show', v))
|
||||
|
||||
const loadOptions = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
options.value = await fetchCustomerOptions(keyword.value)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
options.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onKeywordInput = () => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => loadOptions(), 300)
|
||||
}
|
||||
|
||||
const onSelect = (item: CrmCustomerOption) => {
|
||||
emit('select', item)
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.picker-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 70vh;
|
||||
}
|
||||
.search-container {
|
||||
padding: 8px 12px 0;
|
||||
}
|
||||
.option-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<van-popup v-model:show="visible" round position="bottom" :style="{ maxHeight: '70vh' }">
|
||||
<div class="picker-container">
|
||||
<div class="search-container">
|
||||
<van-search
|
||||
v-model="keyword"
|
||||
placeholder="搜索产品名称"
|
||||
shape="round"
|
||||
@search="loadOptions"
|
||||
@clear="loadOptions"
|
||||
@update:model-value="onKeywordInput"
|
||||
/>
|
||||
</div>
|
||||
<div class="option-list">
|
||||
<van-cell
|
||||
v-for="item in options"
|
||||
:key="item.id"
|
||||
:title="item.productName"
|
||||
:label="formatMeta(item)"
|
||||
clickable
|
||||
@click="onSelect(item)"
|
||||
/>
|
||||
<van-empty
|
||||
v-if="!loading && options.length === 0"
|
||||
description="暂无匹配,请到门户维护"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { fetchProductOptions, type CrmProductOption } from '@/api/crmOptionsApi'
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:show', v: boolean): void
|
||||
(e: 'select', item: CrmProductOption): void
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const keyword = ref('')
|
||||
const loading = ref(false)
|
||||
const options = ref<CrmProductOption[]>([])
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(v) => {
|
||||
visible.value = v
|
||||
if (v) {
|
||||
keyword.value = ''
|
||||
loadOptions()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(visible, (v) => emit('update:show', v))
|
||||
|
||||
const formatMeta = (item: CrmProductOption) => {
|
||||
const parts: string[] = []
|
||||
if (item.model?.trim()) parts.push(item.model.trim())
|
||||
if (item.status === 'off') parts.push('已下架')
|
||||
return parts.join(' · ')
|
||||
}
|
||||
|
||||
const loadOptions = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
options.value = await fetchProductOptions(keyword.value)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
options.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onKeywordInput = () => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => loadOptions(), 300)
|
||||
}
|
||||
|
||||
const onSelect = (item: CrmProductOption) => {
|
||||
emit('select', item)
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.picker-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 70vh;
|
||||
}
|
||||
.search-container {
|
||||
padding: 8px 12px 0;
|
||||
}
|
||||
.option-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<van-popup v-model:show="visible" round position="bottom" :style="{ maxHeight: '70vh' }">
|
||||
<div class="picker-container">
|
||||
<div class="search-container">
|
||||
<van-search
|
||||
v-model="keyword"
|
||||
placeholder="搜索车牌号"
|
||||
shape="round"
|
||||
@search="loadOptions"
|
||||
@clear="loadOptions"
|
||||
@update:model-value="onKeywordInput"
|
||||
/>
|
||||
</div>
|
||||
<div class="option-list">
|
||||
<van-cell
|
||||
v-for="item in options"
|
||||
:key="item.id || item.licensePlate"
|
||||
:title="item.licensePlate"
|
||||
:label="typeLabel(item.vehicleType)"
|
||||
clickable
|
||||
@click="onSelect(item.licensePlate)"
|
||||
/>
|
||||
<van-empty
|
||||
v-if="!loading && options.length === 0"
|
||||
description="暂无匹配,请到门户维护"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import {
|
||||
fetchPublicVehiclePlates,
|
||||
fetchVehicleOptions,
|
||||
type VehicleOption
|
||||
} from '@/api/vehicleOptionsApi'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
show: boolean
|
||||
/** 现场免登模式 */
|
||||
publicMode?: boolean
|
||||
inventoryId?: string
|
||||
planId?: string
|
||||
}>(),
|
||||
{
|
||||
publicMode: false
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:show', v: boolean): void
|
||||
(e: 'select', licensePlate: string): void
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const keyword = ref('')
|
||||
const loading = ref(false)
|
||||
const options = ref<Array<{ id?: string; licensePlate: string; vehicleType?: string }>>([])
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(v) => {
|
||||
visible.value = v
|
||||
if (v) {
|
||||
keyword.value = ''
|
||||
loadOptions()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(visible, (v) => emit('update:show', v))
|
||||
|
||||
const typeLabel = (t?: string) => {
|
||||
if (t === 'self') return '自有'
|
||||
if (t === 'carrier') return '三方'
|
||||
return ''
|
||||
}
|
||||
|
||||
const loadOptions = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
if (props.publicMode) {
|
||||
const list = await fetchPublicVehiclePlates({
|
||||
q: keyword.value,
|
||||
inventoryId: props.inventoryId,
|
||||
planId: props.planId
|
||||
})
|
||||
options.value = list.map((x) => ({
|
||||
id: x.licensePlate,
|
||||
licensePlate: x.licensePlate,
|
||||
vehicleType: x.vehicleType
|
||||
}))
|
||||
} else {
|
||||
const list: VehicleOption[] = await fetchVehicleOptions(keyword.value)
|
||||
options.value = list
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
options.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onKeywordInput = () => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => loadOptions(), 300)
|
||||
}
|
||||
|
||||
const onSelect = (licensePlate: string) => {
|
||||
emit('select', licensePlate)
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.picker-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 70vh;
|
||||
}
|
||||
.search-container {
|
||||
padding: 8px 12px 0;
|
||||
}
|
||||
.option-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user