Files
shop-toy/src/pagesBank/visit/list.vue

471 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts" setup>
import { getVisitPlanList, deleteVisitPlan } from '@/pagesBank/api'
import type { VisitPlan } from '@/typings/bank'
import { VisitStatus } from '@/typings/bank'
definePage({
style: {
navigationBarTitleText: '拜访计划',
enablePullDownRefresh: true
},
})
const visitPlans = ref<VisitPlan[]>([])
const loading = ref(false)
const keyword = ref('')
const activeStatus = ref('')
const customerId = ref('')
const statusTabs = [
{ label: '全部', value: '' },
{ label: '待拜访', value: 'pending' },
{ label: '已完成', value: 'completed' },
{ label: '已取消', value: 'cancelled' },
]
async function loadData() {
loading.value = true
try {
const res = await getVisitPlanList({
status: activeStatus.value || undefined,
pageNum: 1,
pageSize: 20,
keyword: keyword.value
})
// 如果有客户ID筛选过滤结果
let list = res.list
if (customerId.value) {
list = list.filter(item => item.customerId === customerId.value)
}
visitPlans.value = list
} finally {
loading.value = false
uni.stopPullDownRefresh()
}
}
function handleSearch() {
loadData()
}
function handleTabChange(value: string) {
activeStatus.value = value
loadData()
}
function handleDetail(id: string) {
uni.navigateTo({ url: `/pagesBank/visit/detail?id=${id}` })
}
function handleDelete(id: string) {
uni.showModal({
title: '删除确认',
content: '确定要删除这条拜访计划吗?',
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '删除中...' })
try {
await deleteVisitPlan(id)
uni.showToast({ title: '删除成功', icon: 'success' })
loadData()
} finally {
uni.hideLoading()
}
}
}
})
}
function getStatusInfo(status: string) {
const map: Record<string, { text: string; color: string; bgColor: string }> = {
pending: { text: '待拜访', color: '#ff8f0d', bgColor: 'rgba(255, 143, 13, 0.1)' },
completed: { text: '已完成', color: '#00c05a', bgColor: 'rgba(0, 192, 90, 0.1)' },
cancelled: { text: '已取消', color: '#adb5bd', bgColor: 'rgba(173, 181, 189, 0.1)' },
}
return map[status] || { text: '未知', color: '#999', bgColor: '#f5f5f5' }
}
function formatDate(dateStr: string) {
const date = new Date(dateStr)
const month = date.getMonth() + 1
const day = date.getDate()
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
const weekday = weekdays[date.getDay()]
return `${month}${day}日 周${weekday}`
}
onMounted(() => {
loadData()
})
onPullDownRefresh(() => {
loadData()
})
</script>
<template>
<view class="visit-list-page">
<view class="sticky-header">
<view class="search-bar">
<view class="search-input">
<text class="i-carbon-search"></text>
<input
v-model="keyword"
placeholder="搜索客户/主题"
confirm-type="search"
@confirm="handleSearch"
/>
</view>
</view>
<view class="tabs">
<view
v-for="tab in statusTabs"
:key="tab.value"
class="tab-item"
:class="{ active: activeStatus === tab.value }"
@click="handleTabChange(tab.value)"
>
{{ tab.label }}
<view class="line"></view>
</view>
</view>
</view>
<view class="list-container">
<view v-if="loading && visitPlans.length === 0" class="loading-state">
<text>加载中...</text>
</view>
<view v-else-if="visitPlans.length === 0" class="empty-state">
<text class="i-carbon-calendar"></text>
<text>暂无拜访计划</text>
</view>
<view
v-for="item in visitPlans"
:key="item.id"
class="visit-card"
@click="handleDetail(item.id)"
>
<view class="card-header">
<view class="date-badge">
<text class="month">{{ new Date(item.date).getMonth() + 1 }}</text>
<text class="day">{{ new Date(item.date).getDate() }}</text>
</view>
<view class="header-info">
<text class="customer-name">{{ item.customerName }}</text>
<text class="topic">{{ item.topic }}</text>
</view>
<text
class="status-tag"
:style="{ color: getStatusInfo(item.status).color, backgroundColor: getStatusInfo(item.status).bgColor }"
>
{{ getStatusInfo(item.status).text }}
</text>
</view>
<view class="card-body">
<view class="info-row">
<text class="i-carbon-location-filled"></text>
<text class="location">{{ item.location }}</text>
</view>
<view v-if="item.products.length > 0" class="info-row">
<text class="i-carbon-tag"></text>
<text class="products">{{ item.products.map(p => p.name).join('、') }}</text>
</view>
<view v-if="item.photos.length > 0" class="photos-row">
<image
v-for="(photo, index) in item.photos.slice(0, 3)"
:key="index"
:src="photo"
mode="aspectFill"
class="photo"
/>
<view v-if="item.photos.length > 3" class="photo-more">
+{{ item.photos.length - 3 }}
</view>
</view>
</view>
<view class="card-footer">
<text class="time">{{ formatDate(item.date) }}</text>
<view class="actions" @click.stop>
<view class="action-btn delete" @click="handleDelete(item.id)">
<text class="i-carbon-trash-can"></text>
删除
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
.visit-list-page {
min-height: 100vh;
background: #f8f9fa;
padding-bottom: 30rpx;
}
.sticky-header {
position: sticky;
top: 0;
z-index: 100;
background: #fff;
padding: 20rpx 0 0;
}
.search-bar {
padding: 0 30rpx 20rpx;
.search-input {
height: 72rpx;
background: #f1f3f5;
border-radius: 36rpx;
display: flex;
align-items: center;
padding: 0 30rpx;
gap: 16rpx;
text {
font-size: 32rpx;
color: #adb5bd;
}
input {
flex: 1;
font-size: 26rpx;
}
}
}
.tabs {
display: flex;
justify-content: space-around;
border-bottom: 1rpx solid #f1f3f5;
.tab-item {
padding: 20rpx 0;
font-size: 28rpx;
color: #495057;
position: relative;
&.active {
color: #00c05a;
font-weight: 700;
.line {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 6rpx;
background: #00c05a;
border-radius: 3rpx;
}
}
}
}
.list-container {
padding: 24rpx 30rpx;
}
.visit-card {
background: #fff;
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.02);
.card-header {
display: flex;
align-items: flex-start;
gap: 20rpx;
margin-bottom: 20rpx;
.date-badge {
width: 80rpx;
height: 80rpx;
background: linear-gradient(135deg, #00c05a 0%, #34d19d 100%);
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
flex-shrink: 0;
.month {
font-size: 20rpx;
margin-bottom: 4rpx;
}
.day {
font-size: 32rpx;
font-weight: 700;
}
}
.header-info {
flex: 1;
min-width: 0;
.customer-name {
font-size: 30rpx;
font-weight: 700;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.topic {
font-size: 24rpx;
color: #666;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.status-tag {
font-size: 22rpx;
padding: 4rpx 16rpx;
border-radius: 8rpx;
font-weight: 600;
white-space: nowrap;
flex-shrink: 0;
}
}
.card-body {
background: #f8f9fa;
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 20rpx;
.info-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 12rpx;
&:last-child {
margin-bottom: 0;
}
text:first-child {
font-size: 28rpx;
color: #adb5bd;
}
.location,
.products {
flex: 1;
font-size: 26rpx;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.photos-row {
display: flex;
gap: 12rpx;
margin-top: 16rpx;
.photo {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
}
.photo-more {
width: 120rpx;
height: 120rpx;
background: rgba(0, 0, 0, 0.5);
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 28rpx;
font-weight: 600;
}
}
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1rpx solid #f1f3f5;
padding-top: 20rpx;
.time {
font-size: 24rpx;
color: #adb5bd;
}
.actions {
display: flex;
gap: 16rpx;
.action-btn {
display: flex;
align-items: center;
gap: 6rpx;
font-size: 24rpx;
padding: 8rpx 20rpx;
border-radius: 20rpx;
text {
font-size: 28rpx;
}
&.delete {
color: #fa4350;
background: rgba(250, 67, 80, 0.1);
}
}
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #adb5bd;
gap: 20rpx;
text:first-child {
font-size: 80rpx;
}
text:last-child {
font-size: 28rpx;
}
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #adb5bd;
text {
font-size: 28rpx;
}
}
</style>