| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="app-container"> |
| | | <!-- æç´¢åºå --> |
| | | <view class="search-section"> |
| | | <view class="search-row"> |
| | | <up-input |
| | | v-model="searchForm.customerName" |
| | | placeholder="请è¾å
¥å®¢æ·åç§°" |
| | | prefixIcon="search" |
| | | :customStyle="{ marginBottom: '10rpx' }" |
| | | @confirm="handleQuery" |
| | | /> |
| | | </view> |
| | | <view class="search-row"> |
| | | <up-input |
| | | v-model="searchForm.customerType" |
| | | placeholder="è¯·éæ©å®¢æ·åç±»" |
| | | suffixIcon="arrow-down" |
| | | readonly |
| | | @click="showTypePicker = true" |
| | | /> |
| | | </view> |
| | | <view class="search-actions"> |
| | | <up-button type="primary" size="small" @click="handleQuery">æç´¢</up-button> |
| | | <up-button size="small" @click="resetSearch">éç½®</up-button> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- æä½æé® --> |
| | | <view class="action-bar"> |
| | | <up-button type="primary" size="small" icon="plus" @click="openForm('add')">æ°å¢</up-button> |
| | | <up-button size="small" icon="download" @click="handleOut">导åº</up-button> |
| | | <up-button type="info" size="small" icon="upload" @click="handleImport">导å
¥</up-button> |
| | | <up-button type="error" size="small" icon="trash" @click="handleDelete">å é¤</up-button> |
| | | </view> |
| | | |
| | | <!-- 客æ·å表 --> |
| | | <scroll-view class="customer-list" scroll-y @scrolltolower="loadMore"> |
| | | <view |
| | | v-for="item in tableData" |
| | | :key="item.id" |
| | | class="customer-card" |
| | | @click="openDetailDialog(item)" |
| | | > |
| | | <view class="card-header"> |
| | | <text class="customer-name">{{ item.customerName }}</text> |
| | | <up-tag :text="item.customerType" type="primary" size="mini" /> |
| | | </view> |
| | | <view class="card-body"> |
| | | <view class="info-row"> |
| | | <text class="label">è系人ï¼</text> |
| | | <text class="value">{{ item.contactPerson || '-' }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">èç³»çµè¯ï¼</text> |
| | | <text class="value">{{ item.contactPhone || '-' }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">è·è¿è¿åº¦ï¼</text> |
| | | <text class="value">{{ item.followUpLevel || '-' }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">è·è¿æ¶é´ï¼</text> |
| | | <text class="value">{{ item.followUpTime || '-' }}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">ç»´æ¤äººï¼</text> |
| | | <text class="value">{{ item.maintainer || '-' }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="card-footer"> |
| | | <up-button type="primary" text="ç¼è¾" size="mini" @click.stop="openForm('edit', item)" /> |
| | | <up-button type="warning" text="æ´½è°è¿åº¦" size="mini" @click.stop="openNegotiationDialog(item)" /> |
| | | <up-button type="success" text="å访æé" size="mini" @click.stop="openReminderDialog(item)" /> |
| | | </view> |
| | | </view> |
| | | <up-loadmore :status="loadStatus" /> |
| | | </scroll-view> |
| | | |
| | | <!-- 客æ·åç±»éæ©å¨ --> |
| | | <up-picker |
| | | :show="showTypePicker" |
| | | :columns="[customerTypeOptions]" |
| | | @confirm="onTypeConfirm" |
| | | @cancel="showTypePicker = false" |
| | | /> |
| | | |
| | | <!-- æ°å¢/ç¼è¾å®¢æ·å¼¹çª --> |
| | | <up-popup :show="dialogFormVisible" mode="bottom" :round="10" @close="closeDia"> |
| | | <view class="popup-header"> |
| | | <text class="popup-title">{{ operationType === 'add' ? 'æ°å¢å®¢æ·ä¿¡æ¯' : 'ç¼è¾å®¢æ·ä¿¡æ¯' }}</text> |
| | | <up-icon name="close" size="20" @click="closeDia" /> |
| | | </view> |
| | | <scroll-view class="popup-body" scroll-y> |
| | | <up-form :model="form" :rules="rules" ref="formRef" labelPosition="top"> |
| | | <up-form-item label="客æ·åç§°" prop="customerName" required> |
| | | <up-input v-model="form.customerName" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="纳ç¨äººè¯å«å·" prop="taxpayerIdentificationNumber" required> |
| | | <up-input v-model="form.taxpayerIdentificationNumber" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="å
¬å¸å°å" prop="companyAddress" required> |
| | | <up-input v-model="form.companyAddress" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="å
¬å¸çµè¯" prop="companyPhone" required> |
| | | <up-input v-model="form.companyPhone" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="é¶è¡åºæ¬æ·" prop="basicBankAccount" required> |
| | | <up-input v-model="form.basicBankAccount" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="é¶è¡è´¦å·" prop="bankAccount" required> |
| | | <up-input v-model="form.bankAccount" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="弿·è¡å·" prop="bankCode" required> |
| | | <up-input v-model="form.bankCode" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item label="客æ·åç±»" prop="customerType" required> |
| | | <up-input |
| | | v-model="form.customerType" |
| | | placeholder="è¯·éæ©" |
| | | suffixIcon="arrow-down" |
| | | readonly |
| | | @click="showFormTypePicker = true" |
| | | /> |
| | | </up-form-item> |
| | | <view v-for="(contact, index) in formYYs.contactList" :key="index" class="contact-section"> |
| | | <up-form-item :label="`è系人${index + 1}`"> |
| | | <up-input v-model="contact.contactPerson" placeholder="请è¾å
¥" /> |
| | | </up-form-item> |
| | | <up-form-item :label="`èç³»çµè¯${index + 1}`"> |
| | | <view class="contact-phone-row"> |
| | | <up-input v-model="contact.contactPhone" placeholder="请è¾å
¥" /> |
| | | <up-button type="error" size="mini" icon="minus" @click="removeContact(index)" /> |
| | | </view> |
| | | </up-form-item> |
| | | </view> |
| | | <up-button type="primary" plain size="small" icon="plus" @click="addNewContact" style="margin-bottom: 20rpx;"> |
| | | æ°å¢è系人 |
| | | </up-button> |
| | | <up-form-item label="ç»´æ¤äºº" prop="maintainer"> |
| | | <up-input v-model="form.maintainer" disabled /> |
| | | </up-form-item> |
| | | <up-form-item label="ç»´æ¤æ¶é´" prop="maintenanceTime"> |
| | | <up-input |
| | | v-model="form.maintenanceTime" |
| | | placeholder="è¯·éæ©" |
| | | suffixIcon="calendar" |
| | | readonly |
| | | @click="showDatePicker = true" |
| | | /> |
| | | </up-form-item> |
| | | </up-form> |
| | | </scroll-view> |
| | | <view class="popup-footer"> |
| | | <up-button type="primary" @click="submitForm">确认</up-button> |
| | | <up-button @click="closeDia">åæ¶</up-button> |
| | | </view> |
| | | </up-popup> |
| | | |
| | | <!-- 客æ·åç±»éæ©å¨ï¼è¡¨åå
ï¼ --> |
| | | <up-picker |
| | | :show="showFormTypePicker" |
| | | :columns="[customerTypeOptions]" |
| | | @confirm="onFormTypeConfirm" |
| | | @cancel="showFormTypePicker = false" |
| | | /> |
| | | |
| | | <!-- æ¥æéæ©å¨ --> |
| | | <up-datetime-picker |
| | | :show="showDatePicker" |
| | | v-model="datePickerValue" |
| | | mode="date" |
| | | @confirm="onDateConfirm" |
| | | @cancel="showDatePicker = false" |
| | | /> |
| | | |
| | | <!-- 导å
¥å¼¹çª --> |
| | | <up-popup :show="upload.open" mode="center" :round="10" @close="upload.open = false"> |
| | | <view class="popup-content" style="width: 600rpx; padding: 30rpx;"> |
| | | <text class="popup-title">{{ upload.title }}</text> |
| | | <view class="upload-area" @click="chooseFile"> |
| | | <up-icon name="upload" size="48" color="#909399" /> |
| | | <text class="upload-text">ç¹å»éæ©æä»¶</text> |
| | | <text class="upload-tip">ä»
æ¯æ xlsãxlsx æ ¼å¼</text> |
| | | </view> |
| | | <text v-if="upload.fileName" class="file-name">已鿩: {{ upload.fileName }}</text> |
| | | <view class="popup-footer"> |
| | | <up-button type="primary" size="small" @click="submitFileForm">ç¡®å®</up-button> |
| | | <up-button size="small" @click="upload.open = false">åæ¶</up-button> |
| | | </view> |
| | | <up-button type="primary" text="ä¸è½½æ¨¡æ¿" size="mini" plain @click="importTemplate" style="margin-top: 20rpx;" /> |
| | | </view> |
| | | </up-popup> |
| | | |
| | | <!-- å访æéå¼¹çª --> |
| | | <up-popup :show="reminderDialogVisible" mode="bottom" :round="10" @close="closeReminderDialog"> |
| | | <view class="popup-header"> |
| | | <text class="popup-title">å访æé</text> |
| | | <up-icon name="close" size="20" @click="closeReminderDialog" /> |
| | | </view> |
| | | <view class="popup-body"> |
| | | <up-form :model="reminderForm" :rules="reminderRules" ref="reminderFormRef" labelPosition="top"> |
| | | <up-form-item label="客æ·åç§°"> |
| | | <up-input v-model="reminderForm.customerName" disabled /> |
| | | </up-form-item> |
| | | <up-form-item label="æéå¼å
³"> |
| | | <up-switch v-model="reminderForm.reminderSwitch" /> |
| | | </up-form-item> |
| | | <up-form-item label="æéå
容" prop="reminderContent"> |
| | | <up-textarea v-model="reminderForm.reminderContent" placeholder="请è¾å
¥æéå
容" maxlength="100" count /> |
| | | </up-form-item> |
| | | <up-form-item label="æéæ¶é´" prop="reminderTime"> |
| | | <up-input |
| | | v-model="reminderForm.reminderTime" |
| | | placeholder="è¯·éæ©æéæ¶é´" |
| | | suffixIcon="clock" |
| | | readonly |
| | | @click="showReminderTimePicker = true" |
| | | /> |
| | | </up-form-item> |
| | | </up-form> |
| | | </view> |
| | | <view class="popup-footer"> |
| | | <up-button type="primary" @click="submitReminderForm">确认</up-button> |
| | | <up-button @click="closeReminderDialog">åæ¶</up-button> |
| | | </view> |
| | | </up-popup> |
| | | |
| | | <!-- æéæ¶é´éæ©å¨ --> |
| | | <up-datetime-picker |
| | | :show="showReminderTimePicker" |
| | | v-model="reminderTimeValue" |
| | | mode="datetime" |
| | | @confirm="onReminderTimeConfirm" |
| | | @cancel="showReminderTimePicker = false" |
| | | /> |
| | | |
| | | <!-- æ´½è°è¿åº¦å¼¹çª --> |
| | | <up-popup :show="negotiationDialogVisible" mode="bottom" :round="10" @close="closeNegotiationDialog"> |
| | | <view class="popup-header"> |
| | | <text class="popup-title">{{ negotiationForm.editIndex !== undefined ? 'ä¿®æ¹è¿åº¦' : 'æ·»å è¿åº¦' }}</text> |
| | | <up-icon name="close" size="20" @click="closeNegotiationDialog" /> |
| | | </view> |
| | | <view class="popup-body"> |
| | | <up-form :model="negotiationForm" :rules="negotiationRules" ref="negotiationFormRef" labelPosition="top"> |
| | | <up-form-item label="è·è¿æ¹å¼" prop="followUpMethod" required> |
| | | <up-input |
| | | v-model="negotiationForm.followUpMethod" |
| | | placeholder="è¯·éæ©" |
| | | suffixIcon="arrow-down" |
| | | readonly |
| | | @click="showFollowMethodPicker = true" |
| | | /> |
| | | </up-form-item> |
| | | <up-form-item label="è·è¿ç¨åº¦" prop="followUpLevel" required> |
| | | <up-input |
| | | v-model="negotiationForm.followUpLevel" |
| | | placeholder="è¯·éæ©" |
| | | suffixIcon="arrow-down" |
| | | readonly |
| | | @click="showFollowLevelPicker = true" |
| | | /> |
| | | </up-form-item> |
| | | <up-form-item label="è·è¿æ¶é´" prop="followUpTime" required> |
| | | <up-input |
| | | v-model="negotiationForm.followUpTime" |
| | | placeholder="è¯·éæ©" |
| | | suffixIcon="clock" |
| | | readonly |
| | | @click="showFollowTimePicker = true" |
| | | /> |
| | | </up-form-item> |
| | | <up-form-item label="è·è¿äºº"> |
| | | <up-input v-model="negotiationForm.followerUserName" disabled /> |
| | | </up-form-item> |
| | | <up-form-item label="å
容" prop="content" required> |
| | | <up-textarea v-model="negotiationForm.content" placeholder="请è¾å
¥" :rows="4" /> |
| | | </up-form-item> |
| | | </up-form> |
| | | </view> |
| | | <view class="popup-footer"> |
| | | <up-button type="primary" @click="submitNegotiationForm">确认</up-button> |
| | | <up-button @click="closeNegotiationDialog">åæ¶</up-button> |
| | | </view> |
| | | </up-popup> |
| | | |
| | | <!-- è·è¿æ¹å¼éæ©å¨ --> |
| | | <up-picker |
| | | :show="showFollowMethodPicker" |
| | | :columns="[followMethodOptions]" |
| | | @confirm="onFollowMethodConfirm" |
| | | @cancel="showFollowMethodPicker = false" |
| | | /> |
| | | |
| | | <!-- è·è¿ç¨åº¦éæ©å¨ --> |
| | | <up-picker |
| | | :show="showFollowLevelPicker" |
| | | :columns="[followLevelOptions]" |
| | | @confirm="onFollowLevelConfirm" |
| | | @cancel="showFollowLevelPicker = false" |
| | | /> |
| | | |
| | | <!-- è·è¿æ¶é´éæ©å¨ --> |
| | | <up-datetime-picker |
| | | :show="showFollowTimePicker" |
| | | v-model="followTimeValue" |
| | | mode="datetime" |
| | | @confirm="onFollowTimeConfirm" |
| | | @cancel="showFollowTimePicker = false" |
| | | /> |
| | | |
| | | <!-- 客æ·è¯¦æ
å¼¹çª --> |
| | | <up-popup :show="detailDialogVisible" mode="bottom" :round="10" @close="closeDetailDialog"> |
| | | <view class="popup-header"> |
| | | <text class="popup-title">客æ·è¯¦æ
</text> |
| | | <up-icon name="close" size="20" @click="closeDetailDialog" /> |
| | | </view> |
| | | <scroll-view class="popup-body" scroll-y style="max-height: 70vh;"> |
| | | <!-- 客æ·åºæ¬ä¿¡æ¯ --> |
| | | <view class="detail-section"> |
| | | <view class="section-header"> |
| | | <text class="section-title">客æ·åºæ¬ä¿¡æ¯</text> |
| | | </view> |
| | | <view class="info-list"> |
| | | <view class="info-item"> |
| | | <text class="info-label">客æ·åç§°ï¼</text> |
| | | <text class="info-value">{{ detailForm.customerName }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">客æ·åç±»ï¼</text> |
| | | <text class="info-value">{{ detailForm.customerType }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">纳ç¨äººè¯å«å·ï¼</text> |
| | | <text class="info-value">{{ detailForm.taxpayerIdentificationNumber }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">å
¬å¸çµè¯ï¼</text> |
| | | <text class="info-value">{{ detailForm.companyPhone }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">å
¬å¸å°åï¼</text> |
| | | <text class="info-value">{{ detailForm.companyAddress }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">é¶è¡åºæ¬æ·ï¼</text> |
| | | <text class="info-value">{{ detailForm.basicBankAccount }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">é¶è¡è´¦å·ï¼</text> |
| | | <text class="info-value">{{ detailForm.bankAccount }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">弿·è¡å·ï¼</text> |
| | | <text class="info-value">{{ detailForm.bankCode }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">è系人ï¼</text> |
| | | <text class="info-value">{{ detailForm.contactPerson }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">èç³»çµè¯ï¼</text> |
| | | <text class="info-value">{{ detailForm.contactPhone }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">ç»´æ¤äººï¼</text> |
| | | <text class="info-value">{{ detailForm.maintainer }}</text> |
| | | </view> |
| | | <view class="info-item"> |
| | | <text class="info-label">ç»´æ¤æ¶é´ï¼</text> |
| | | <text class="info-value">{{ detailForm.maintenanceTime }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- æ´½è°è¿åº¦è®°å½ --> |
| | | <view class="detail-section"> |
| | | <view class="section-header"> |
| | | <text class="section-title">æ´½è°è¿åº¦è®°å½</text> |
| | | <up-button type="primary" size="mini" @click="openNegotiationDialog(detailForm)">æ·»å è¿åº¦</up-button> |
| | | </view> |
| | | <view v-if="negotiationRecords.length === 0" class="empty-text">ææ è®°å½</view> |
| | | <view v-for="(record, index) in negotiationRecords" :key="index" class="record-card"> |
| | | <view class="record-header"> |
| | | <text class="record-time">{{ record.followUpTime }}</text> |
| | | <up-tag :text="record.followUpMethod" type="info" size="mini" /> |
| | | </view> |
| | | <view class="record-body"> |
| | | <text class="record-level">{{ record.followUpLevel }}</text> |
| | | <text class="record-content">{{ record.content }}</text> |
| | | </view> |
| | | <view class="record-footer"> |
| | | <text class="record-user">è·è¿äººï¼{{ record.followerUserName }}</text> |
| | | <view class="record-actions"> |
| | | <up-button type="primary" text="éä»¶" size="mini" @click="openAttachmentDialog(record)" /> |
| | | <up-button type="warning" text="ç¼è¾" size="mini" @click="editNegotiationRecord(record, index)" /> |
| | | <up-button type="error" text="å é¤" size="mini" @click="deleteNegotiationRecord(record, index)" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </scroll-view> |
| | | </up-popup> |
| | | |
| | | <!-- éä»¶å¼¹çª --> |
| | | <up-popup :show="attachmentDialogVisible" mode="bottom" :round="10" @close="closeAttachmentDialog"> |
| | | <view class="popup-header"> |
| | | <text class="popup-title">é件管ç</text> |
| | | <up-icon name="close" size="20" @click="closeAttachmentDialog" /> |
| | | </view> |
| | | <scroll-view class="popup-body" scroll-y style="max-height: 60vh;"> |
| | | <view v-if="currentAttachmentList.length === 0" class="empty-text">ææ éä»¶</view> |
| | | <view v-for="(file, index) in currentAttachmentList" :key="index" class="file-item"> |
| | | <view class="file-info"> |
| | | <up-icon name="file-text" size="24" /> |
| | | <view class="file-detail"> |
| | | <text class="file-name">{{ file.name }}</text> |
| | | <text class="file-size">{{ formatFileSize(file.size) }}</text> |
| | | </view> |
| | | </view> |
| | | <view class="file-actions"> |
| | | <up-button type="primary" size="mini" text="ä¸è½½" @click="downloadAttachment(file)" /> |
| | | <up-button type="error" size="mini" text="å é¤" @click="deleteAttachment(file, index)" /> |
| | | </view> |
| | | </view> |
| | | </scroll-view> |
| | | <view class="popup-footer"> |
| | | <up-button type="primary" @click="chooseAttachment">ä¸ä¼ éä»¶</up-button> |
| | | <up-button @click="closeAttachmentDialog">å
³é</up-button> |
| | | </view> |
| | | </up-popup> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { onMounted, ref, reactive, getCurrentInstance, toRefs } from 'vue'; |
| | | import { |
| | | addCustomer, |
| | | delCustomer, |
| | | getCustomer, |
| | | listCustomer, |
| | | updateCustomer, |
| | | addCustomerFollow, |
| | | updateCustomerFollow, |
| | | delCustomerFollow, |
| | | addReturnVisit, |
| | | getReturnVisit, |
| | | } from '@/api/basicData/customerFile.js'; |
| | | import { userListNoPage } from '@/api/system/user.js'; |
| | | import useUserStore from '@/store/modules/user'; |
| | | import { getToken } from '@/utils/auth.js'; |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | const userStore = useUserStore(); |
| | | |
| | | // æç´¢ç¸å
³ |
| | | const showTypePicker = ref(false); |
| | | const customerTypeOptions = ['é¶å®å®¢æ·', 'è¿éå客æ·']; |
| | | |
| | | // 表åç¸å
³ |
| | | const showFormTypePicker = ref(false); |
| | | const showDatePicker = ref(false); |
| | | const datePickerValue = ref(Date.now()); |
| | | |
| | | // å访æéç¸å
³ |
| | | const reminderDialogVisible = ref(false); |
| | | const reminderFormRef = ref(); |
| | | const currentCustomerId = ref(); |
| | | const showReminderTimePicker = ref(false); |
| | | const reminderTimeValue = ref(Date.now()); |
| | | const reminderForm = reactive({ |
| | | customerName: '', |
| | | reminderSwitch: false, |
| | | reminderContent: '', |
| | | reminderTime: '', |
| | | }); |
| | | const reminderRules = { |
| | | reminderContent: [ |
| | | { required: true, message: '请è¾å
¥æéå
容', trigger: 'blur' }, |
| | | ], |
| | | reminderTime: [ |
| | | { required: true, message: 'è¯·éæ©æéæ¶é´', trigger: 'change' }, |
| | | ], |
| | | }; |
| | | |
| | | // æ´½è°è¿åº¦ç¸å
³ |
| | | const negotiationDialogVisible = ref(false); |
| | | const negotiationFormRef = ref(); |
| | | const showFollowMethodPicker = ref(false); |
| | | const showFollowLevelPicker = ref(false); |
| | | const showFollowTimePicker = ref(false); |
| | | const followTimeValue = ref(Date.now()); |
| | | const followMethodOptions = ['çµè¯', 'é®ä»¶', 'ä¸é¨', '微信', 'å
¶ä»']; |
| | | const followLevelOptions = ['æ½å¨å®¢æ·', '忬¡æè®¿', '夿¬¡æè®¿', 'æå客æ·', 'å·²ç¾çº¦å®¢æ·']; |
| | | const negotiationForm = reactive({ |
| | | customerName: '', |
| | | customerId: '', |
| | | followUpMethod: '', |
| | | followUpLevel: '', |
| | | followUpTime: '', |
| | | followerUserName: '', |
| | | content: '', |
| | | }); |
| | | const negotiationRules = { |
| | | followUpMethod: [ |
| | | { required: true, message: 'è¯·éæ©è·è¿æ¹å¼', trigger: 'change' }, |
| | | ], |
| | | followUpLevel: [ |
| | | { required: true, message: 'è¯·éæ©è·è¿ç¨åº¦', trigger: 'change' }, |
| | | ], |
| | | followUpTime: [ |
| | | { required: true, message: 'è¯·éæ©è·è¿æ¶é´', trigger: 'change' }, |
| | | ], |
| | | content: [{ required: true, message: '请è¾å
¥å
容', trigger: 'blur' }], |
| | | }; |
| | | |
| | | // 详æ
ç¸å
³ |
| | | const detailDialogVisible = ref(false); |
| | | const detailForm = reactive({ |
| | | customerName: '', |
| | | customerType: '', |
| | | taxpayerIdentificationNumber: '', |
| | | companyPhone: '', |
| | | companyAddress: '', |
| | | basicBankAccount: '', |
| | | bankAccount: '', |
| | | bankCode: '', |
| | | contactPerson: '', |
| | | contactPhone: '', |
| | | maintainer: '', |
| | | maintenanceTime: '', |
| | | }); |
| | | const negotiationRecords = ref([]); |
| | | |
| | | // éä»¶ç¸å
³ |
| | | const attachmentDialogVisible = ref(false); |
| | | const currentAttachmentList = ref([]); |
| | | const currentFollowRecord = ref({}); |
| | | |
| | | // å表ç¸å
³ |
| | | const tableData = ref([]); |
| | | const selectedRows = ref([]); |
| | | const userList = ref([]); |
| | | const tableLoading = ref(false); |
| | | const loadStatus = ref('loadmore'); |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }); |
| | | |
| | | // ç¨æ·ä¿¡æ¯è¡¨åå¼¹æ¡æ°æ® |
| | | const operationType = ref(''); |
| | | const dialogFormVisible = ref(false); |
| | | const formRef = ref(); |
| | | const formYYs = ref({ |
| | | contactList: [ |
| | | { |
| | | contactPerson: '', |
| | | contactPhone: '', |
| | | }, |
| | | ], |
| | | }); |
| | | const data = reactive({ |
| | | searchForm: { |
| | | customerName: '', |
| | | customerType: '', |
| | | }, |
| | | form: { |
| | | customerName: '', |
| | | taxpayerIdentificationNumber: '', |
| | | companyAddress: '', |
| | | companyPhone: '', |
| | | contactPerson: '', |
| | | contactPhone: '', |
| | | maintainer: '', |
| | | maintenanceTime: '', |
| | | basicBankAccount: '', |
| | | bankAccount: '', |
| | | bankCode: '', |
| | | customerType: '', |
| | | }, |
| | | rules: { |
| | | customerName: [{ required: true, message: '请è¾å
¥', trigger: 'blur' }], |
| | | taxpayerIdentificationNumber: [ |
| | | { required: true, message: '请è¾å
¥', trigger: 'blur' }, |
| | | ], |
| | | companyAddress: [{ required: true, message: '请è¾å
¥', trigger: 'blur' }], |
| | | companyPhone: [{ required: true, message: '请è¾å
¥', trigger: 'blur' }], |
| | | basicBankAccount: [{ required: true, message: '请è¾å
¥', trigger: 'blur' }], |
| | | bankAccount: [{ required: true, message: '请è¾å
¥', trigger: 'blur' }], |
| | | bankCode: [{ required: true, message: '请è¾å
¥', trigger: 'blur' }], |
| | | customerType: [{ required: true, message: 'è¯·éæ©', trigger: 'change' }], |
| | | }, |
| | | }); |
| | | const upload = reactive({ |
| | | open: false, |
| | | title: '', |
| | | isUploading: false, |
| | | headers: { Authorization: 'Bearer ' + getToken() }, |
| | | url: import.meta.env.VITE_APP_BASE_API + '/basic/customer/importData', |
| | | fileName: '', |
| | | filePath: '', |
| | | }); |
| | | |
| | | const { searchForm, form, rules } = toRefs(data); |
| | | |
| | | // 鿩客æ·åç±»ï¼æç´¢ï¼ |
| | | const onTypeConfirm = (e) => { |
| | | searchForm.value.customerType = e.value[0]; |
| | | showTypePicker.value = false; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | // 鿩客æ·åç±»ï¼è¡¨åï¼ |
| | | const onFormTypeConfirm = (e) => { |
| | | form.value.customerType = e.value[0]; |
| | | showFormTypePicker.value = false; |
| | | }; |
| | | |
| | | // éæ©æ¥æ |
| | | const onDateConfirm = (e) => { |
| | | const date = new Date(e.value); |
| | | form.value.maintenanceTime = formatDate(date); |
| | | showDatePicker.value = false; |
| | | }; |
| | | |
| | | // éæ©æéæ¶é´ |
| | | const onReminderTimeConfirm = (e) => { |
| | | const date = new Date(e.value); |
| | | reminderForm.reminderTime = formatDateTime(date); |
| | | showReminderTimePicker.value = false; |
| | | }; |
| | | |
| | | // éæ©è·è¿æ¹å¼ |
| | | const onFollowMethodConfirm = (e) => { |
| | | negotiationForm.followUpMethod = e.value[0]; |
| | | showFollowMethodPicker.value = false; |
| | | }; |
| | | |
| | | // éæ©è·è¿ç¨åº¦ |
| | | const onFollowLevelConfirm = (e) => { |
| | | negotiationForm.followUpLevel = e.value[0]; |
| | | showFollowLevelPicker.value = false; |
| | | }; |
| | | |
| | | // éæ©è·è¿æ¶é´ |
| | | const onFollowTimeConfirm = (e) => { |
| | | const date = new Date(e.value); |
| | | negotiationForm.followUpTime = formatDateTime(date); |
| | | showFollowTimePicker.value = false; |
| | | }; |
| | | |
| | | // æ ¼å¼åæ¥æ |
| | | const formatDate = (date) => { |
| | | const year = date.getFullYear(); |
| | | const month = String(date.getMonth() + 1).padStart(2, '0'); |
| | | const day = String(date.getDate()).padStart(2, '0'); |
| | | return `${year}-${month}-${day}`; |
| | | }; |
| | | |
| | | // æ ¼å¼åæ¥ææ¶é´ |
| | | const formatDateTime = (date) => { |
| | | const year = date.getFullYear(); |
| | | const month = String(date.getMonth() + 1).padStart(2, '0'); |
| | | const day = String(date.getDate()).padStart(2, '0'); |
| | | const hours = String(date.getHours()).padStart(2, '0'); |
| | | const minutes = String(date.getMinutes()).padStart(2, '0'); |
| | | const seconds = String(date.getSeconds()).padStart(2, '0'); |
| | | return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; |
| | | }; |
| | | |
| | | // éç½®æç´¢ |
| | | const resetSearch = () => { |
| | | searchForm.value.customerName = ''; |
| | | searchForm.value.customerType = ''; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | // æ°å¢è系人 |
| | | const addNewContact = () => { |
| | | formYYs.value.contactList.push({ |
| | | contactPerson: '', |
| | | contactPhone: '', |
| | | }); |
| | | }; |
| | | |
| | | // å é¤è系人 |
| | | const removeContact = (index) => { |
| | | if (formYYs.value.contactList.length > 1) { |
| | | formYYs.value.contactList.splice(index, 1); |
| | | } else { |
| | | uni.showToast({ title: 'è³å°ä¿çä¸ä¸ªè系人', icon: 'none' }); |
| | | } |
| | | }; |
| | | |
| | | // æ¥è¯¢å表 |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | // å è½½æ´å¤ |
| | | const loadMore = () => { |
| | | if (loadStatus.value === 'nomore') return; |
| | | page.current++; |
| | | getList(true); |
| | | }; |
| | | |
| | | const getList = (isLoadMore = false) => { |
| | | tableLoading.value = true; |
| | | loadStatus.value = 'loading'; |
| | | listCustomer({ ...searchForm.value, ...page }) |
| | | .then((res) => { |
| | | tableLoading.value = false; |
| | | if (isLoadMore) { |
| | | tableData.value = [...tableData.value, ...res.records]; |
| | | } else { |
| | | tableData.value = res.records; |
| | | } |
| | | page.total = res.total; |
| | | loadStatus.value = tableData.value.length >= res.total ? 'nomore' : 'loadmore'; |
| | | }) |
| | | .catch(() => { |
| | | tableLoading.value = false; |
| | | loadStatus.value = 'loadmore'; |
| | | }); |
| | | }; |
| | | |
| | | // æäº¤ä¸ä¼ æä»¶ |
| | | const chooseFile = () => { |
| | | uni.chooseFile({ |
| | | count: 1, |
| | | extension: ['.xls', '.xlsx'], |
| | | success: (res) => { |
| | | const tempFile = res.tempFiles[0]; |
| | | upload.fileName = tempFile.name; |
| | | upload.filePath = tempFile.path; |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | const submitFileForm = () => { |
| | | if (!upload.filePath) { |
| | | uni.showToast({ title: 'è¯·éæ©æä»¶', icon: 'none' }); |
| | | return; |
| | | } |
| | | upload.isUploading = true; |
| | | uni.uploadFile({ |
| | | url: upload.url, |
| | | filePath: upload.filePath, |
| | | name: 'file', |
| | | header: upload.headers, |
| | | success: (res) => { |
| | | upload.isUploading = false; |
| | | const data = JSON.parse(res.data); |
| | | if (data.code === 200) { |
| | | uni.showToast({ title: 'ä¸ä¼ æå', icon: 'success' }); |
| | | upload.open = false; |
| | | getList(); |
| | | } else { |
| | | uni.showToast({ title: data.msg || 'ä¸ä¼ 失败', icon: 'none' }); |
| | | } |
| | | }, |
| | | fail: () => { |
| | | upload.isUploading = false; |
| | | uni.showToast({ title: 'ä¸ä¼ 失败', icon: 'none' }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | // 导å
¥æé®æä½ |
| | | const handleImport = () => { |
| | | upload.title = '客æ·å¯¼å
¥'; |
| | | upload.open = true; |
| | | upload.fileName = ''; |
| | | upload.filePath = ''; |
| | | }; |
| | | |
| | | // ä¸è½½æ¨¡æ¿ |
| | | const importTemplate = () => { |
| | | proxy.download('/basic/customer/downloadTemplate', {}, '客æ·å¯¼å
¥æ¨¡æ¿.xlsx'); |
| | | }; |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openForm = (type, row) => { |
| | | operationType.value = type; |
| | | form.value = { |
| | | customerName: '', |
| | | taxpayerIdentificationNumber: '', |
| | | companyAddress: '', |
| | | companyPhone: '', |
| | | contactPerson: '', |
| | | contactPhone: '', |
| | | maintainer: userStore.nickName, |
| | | maintenanceTime: getCurrentDate(), |
| | | basicBankAccount: '', |
| | | bankAccount: '', |
| | | bankCode: '', |
| | | customerType: '', |
| | | }; |
| | | formYYs.value.contactList = [ |
| | | { |
| | | contactPerson: '', |
| | | contactPhone: '', |
| | | }, |
| | | ]; |
| | | userListNoPage().then((res) => { |
| | | userList.value = res.data; |
| | | }); |
| | | if (type === 'edit' && row) { |
| | | getCustomer(row.id).then((res) => { |
| | | form.value = { ...res.data }; |
| | | formYYs.value.contactList = res.data.contactPerson |
| | | ? res.data.contactPerson.split(',').map((item, index) => { |
| | | return { |
| | | contactPerson: item, |
| | | contactPhone: res.data.contactPhone |
| | | ? res.data.contactPhone.split(',')[index] || '' |
| | | : '', |
| | | }; |
| | | }) |
| | | : [{ contactPerson: '', contactPhone: '' }]; |
| | | }); |
| | | } |
| | | dialogFormVisible.value = true; |
| | | }; |
| | | |
| | | // æäº¤è¡¨å |
| | | const submitForm = () => { |
| | | formRef.value.validate().then(() => { |
| | | if (operationType.value === 'edit') { |
| | | submitEdit(); |
| | | } else { |
| | | submitAdd(); |
| | | } |
| | | }).catch(() => {}); |
| | | }; |
| | | |
| | | // æäº¤æ°å¢ |
| | | const submitAdd = () => { |
| | | if (formYYs.value.contactList.length < 1) { |
| | | uni.showToast({ title: '请è³å°æ·»å ä¸ä¸ªè系人', icon: 'none' }); |
| | | return; |
| | | } |
| | | form.value.contactPerson = formYYs.value.contactList |
| | | .map((item) => item.contactPerson) |
| | | .join(','); |
| | | form.value.contactPhone = formYYs.value.contactList |
| | | .map((item) => item.contactPhone) |
| | | .join(','); |
| | | addCustomer(form.value).then((res) => { |
| | | uni.showToast({ title: 'æäº¤æå', icon: 'success' }); |
| | | closeDia(); |
| | | getList(); |
| | | }); |
| | | }; |
| | | |
| | | // æäº¤ä¿®æ¹ |
| | | const submitEdit = () => { |
| | | form.value.contactPerson = formYYs.value.contactList |
| | | .map((item) => item.contactPerson) |
| | | .join(','); |
| | | form.value.contactPhone = formYYs.value.contactList |
| | | .map((item) => item.contactPhone) |
| | | .join(','); |
| | | updateCustomer(form.value).then((res) => { |
| | | uni.showToast({ title: 'æäº¤æå', icon: 'success' }); |
| | | closeDia(); |
| | | getList(); |
| | | }); |
| | | }; |
| | | |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | formRef.value?.resetFields?.(); |
| | | dialogFormVisible.value = false; |
| | | }; |
| | | |
| | | // å¯¼åº |
| | | const handleOut = () => { |
| | | uni.showModal({ |
| | | title: '导åº', |
| | | content: 'éä¸çå
容å°è¢«å¯¼åºï¼æ¯å¦ç¡®è®¤å¯¼åºï¼', |
| | | success: (res) => { |
| | | if (res.confirm) { |
| | | proxy.download('/basic/customer/export', {}, 'å®¢æ·æ¡£æ¡.xlsx'); |
| | | } |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | // å é¤ |
| | | const handleDelete = () => { |
| | | if (selectedRows.value.length === 0) { |
| | | uni.showToast({ title: 'è¯·éæ©æ°æ®', icon: 'none' }); |
| | | return; |
| | | } |
| | | const unauthorizedData = selectedRows.value.filter( |
| | | (item) => item.maintainer !== userStore.nickName |
| | | ); |
| | | if (unauthorizedData.length > 0) { |
| | | uni.showToast({ title: 'ä¸å¯å é¤ä»äººç»´æ¤çæ°æ®', icon: 'none' }); |
| | | return; |
| | | } |
| | | const ids = selectedRows.value.map((item) => item.id); |
| | | uni.showModal({ |
| | | title: 'å é¤æç¤º', |
| | | content: 'éä¸çå
容å°è¢«å é¤ï¼æ¯å¦ç¡®è®¤å é¤ï¼', |
| | | success: (res) => { |
| | | if (res.confirm) { |
| | | tableLoading.value = true; |
| | | delCustomer(ids) |
| | | .then(() => { |
| | | uni.showToast({ title: 'å 餿å', icon: 'success' }); |
| | | getList(); |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | // æå¼å访æéå¼¹çª |
| | | const openReminderDialog = (row) => { |
| | | currentCustomerId.value = row.id; |
| | | reminderForm.customerName = row.customerName; |
| | | reminderForm.reminderSwitch = false; |
| | | reminderForm.reminderContent = ''; |
| | | reminderForm.reminderTime = ''; |
| | | delete reminderForm.id; |
| | | |
| | | getReturnVisit(row.id) |
| | | .then((res) => { |
| | | if (res.code === 200 && res.data) { |
| | | reminderForm.reminderSwitch = res.data.isEnabled === 1; |
| | | reminderForm.reminderContent = res.data.content; |
| | | reminderForm.reminderTime = res.data.reminderTime; |
| | | reminderForm.id = res.data.id; |
| | | } |
| | | }) |
| | | .catch((error) => { |
| | | console.error('è·åå访æé失败:', error); |
| | | }); |
| | | |
| | | reminderDialogVisible.value = true; |
| | | }; |
| | | |
| | | // å
³éå访æéå¼¹çª |
| | | const closeReminderDialog = () => { |
| | | reminderFormRef.value?.resetFields?.(); |
| | | reminderDialogVisible.value = false; |
| | | }; |
| | | |
| | | // æäº¤å访æé |
| | | const submitReminderForm = () => { |
| | | reminderFormRef.value.validate().then(() => { |
| | | const submitvalue = reminderForm.id |
| | | ? { |
| | | id: reminderForm.id, |
| | | customerId: currentCustomerId.value, |
| | | isEnabled: reminderForm.reminderSwitch ? 1 : 0, |
| | | content: reminderForm.reminderContent, |
| | | reminderTime: reminderForm.reminderTime, |
| | | remindUserId: userStore.id, |
| | | } |
| | | : { |
| | | customerId: currentCustomerId.value, |
| | | isEnabled: reminderForm.reminderSwitch ? 1 : 0, |
| | | content: reminderForm.reminderContent, |
| | | reminderTime: reminderForm.reminderTime, |
| | | remindUserId: userStore.id, |
| | | }; |
| | | |
| | | addReturnVisit(submitvalue) |
| | | .then((res) => { |
| | | if (res.code === 200) { |
| | | uni.showToast({ title: 'å访æé设置æå', icon: 'success' }); |
| | | closeReminderDialog(); |
| | | } else { |
| | | uni.showToast({ title: res.msg || '设置失败', icon: 'none' }); |
| | | } |
| | | }) |
| | | .catch((error) => { |
| | | console.error('设置å访æé失败:', error); |
| | | uni.showToast({ title: '设置失败', icon: 'none' }); |
| | | }); |
| | | }).catch(() => {}); |
| | | }; |
| | | |
| | | // æå¼æ´½è°è¿åº¦å¼¹çª |
| | | const openNegotiationDialog = (row) => { |
| | | negotiationForm.customerName = row.customerName; |
| | | negotiationForm.customerId = row.id; |
| | | negotiationForm.followUpMethod = ''; |
| | | negotiationForm.followUpLevel = ''; |
| | | negotiationForm.followUpTime = ''; |
| | | negotiationForm.followerUserName = userStore.nickName; |
| | | negotiationForm.content = ''; |
| | | delete negotiationForm.editIndex; |
| | | delete negotiationForm.id; |
| | | negotiationDialogVisible.value = true; |
| | | }; |
| | | |
| | | // å
³éæ´½è°è¿åº¦å¼¹çª |
| | | const closeNegotiationDialog = () => { |
| | | negotiationFormRef.value?.resetFields?.(); |
| | | delete negotiationForm.editIndex; |
| | | delete negotiationForm.id; |
| | | negotiationDialogVisible.value = false; |
| | | }; |
| | | |
| | | // æäº¤æ´½è°è¿åº¦ |
| | | const submitNegotiationForm = () => { |
| | | negotiationFormRef.value.validate().then(() => { |
| | | const isEdit = negotiationForm.editIndex !== undefined; |
| | | |
| | | if (isEdit) { |
| | | updateCustomerFollow(negotiationForm).then((res) => { |
| | | getCustomer(negotiationForm.customerId).then((res) => { |
| | | negotiationRecords.value = res.data.followUpList || []; |
| | | }); |
| | | uni.showToast({ title: 'ä¿®æ¹æå', icon: 'success' }); |
| | | closeNegotiationDialog(); |
| | | }); |
| | | } else { |
| | | addCustomerFollow(negotiationForm).then((res) => { |
| | | const newRecord = { |
| | | followUpTime: negotiationForm.followUpTime, |
| | | followUpMethod: negotiationForm.followUpMethod, |
| | | followUpLevel: negotiationForm.followUpLevel, |
| | | followerUserName: negotiationForm.followerUserName, |
| | | content: negotiationForm.content, |
| | | }; |
| | | negotiationRecords.value.unshift(newRecord); |
| | | uni.showToast({ title: 'æäº¤æå', icon: 'success' }); |
| | | closeNegotiationDialog(); |
| | | getList(); |
| | | }); |
| | | } |
| | | }).catch(() => {}); |
| | | }; |
| | | |
| | | // æå¼è¯¦æ
å¼¹çª |
| | | const openDetailDialog = (row) => { |
| | | getCustomer(row.id).then((res) => { |
| | | Object.assign(detailForm, res.data); |
| | | negotiationRecords.value = res.data.followUpList || []; |
| | | detailDialogVisible.value = true; |
| | | }); |
| | | }; |
| | | |
| | | // å
³é详æ
å¼¹çª |
| | | const closeDetailDialog = () => { |
| | | detailDialogVisible.value = false; |
| | | }; |
| | | |
| | | // ä¿®æ¹æ´½è°è®°å½ |
| | | const editNegotiationRecord = (row, index) => { |
| | | Object.assign(negotiationForm, { |
| | | customerName: row.customerName, |
| | | customerId: row.customerId, |
| | | followUpMethod: row.followUpMethod, |
| | | followUpLevel: row.followUpLevel, |
| | | followUpTime: row.followUpTime, |
| | | followerUserName: row.followerUserName, |
| | | content: row.content, |
| | | id: row.id, |
| | | editIndex: index, |
| | | }); |
| | | negotiationDialogVisible.value = true; |
| | | }; |
| | | |
| | | // å 餿´½è°è®°å½ |
| | | const deleteNegotiationRecord = (row, index) => { |
| | | uni.showModal({ |
| | | title: 'å é¤æç¤º', |
| | | content: 'ç¡®å®è¦å é¤è¿æ¡æ´½è°è®°å½åï¼', |
| | | success: (res) => { |
| | | if (res.confirm) { |
| | | delCustomerFollow(row.id).then(() => { |
| | | getCustomer(row.customerId).then((res) => { |
| | | negotiationRecords.value = res.data.followUpList || []; |
| | | }); |
| | | uni.showToast({ title: 'å 餿å', icon: 'success' }); |
| | | }); |
| | | negotiationRecords.value.splice(index, 1); |
| | | } |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | // æå¼éä»¶å¼¹çª |
| | | const openAttachmentDialog = (row) => { |
| | | currentFollowRecord.value = row; |
| | | currentAttachmentList.value = (row.fileList || []).map((file, index) => ({ |
| | | name: file.fileName, |
| | | url: file.fileUrl, |
| | | size: file.fileSize, |
| | | id: file.id, |
| | | uid: file.id || index, |
| | | status: 'success', |
| | | })); |
| | | attachmentDialogVisible.value = true; |
| | | }; |
| | | |
| | | // å
³ééä»¶å¼¹çª |
| | | const closeAttachmentDialog = () => { |
| | | attachmentDialogVisible.value = false; |
| | | currentFollowRecord.value = {}; |
| | | currentAttachmentList.value = []; |
| | | }; |
| | | |
| | | // éæ©éä»¶ |
| | | const chooseAttachment = () => { |
| | | uni.chooseFile({ |
| | | count: 1, |
| | | success: (res) => { |
| | | const tempFile = res.tempFiles[0]; |
| | | uploadAttachment(tempFile); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | // ä¸ä¼ éä»¶ |
| | | const uploadAttachment = (file) => { |
| | | const maxSize = 50 * 1024 * 1024; |
| | | if (file.size > maxSize) { |
| | | uni.showToast({ title: 'æä»¶å¤§å°ä¸è½è¶
è¿50MB', icon: 'none' }); |
| | | return; |
| | | } |
| | | const uploadUrl = currentFollowRecord.value.id |
| | | ? import.meta.env.VITE_APP_BASE_API + |
| | | '/basic/customer-follow/upload/' + |
| | | currentFollowRecord.value.id |
| | | : import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload'; |
| | | |
| | | uni.uploadFile({ |
| | | url: uploadUrl, |
| | | filePath: file.path, |
| | | name: 'file', |
| | | header: { Authorization: 'Bearer ' + getToken() }, |
| | | success: (res) => { |
| | | const data = JSON.parse(res.data); |
| | | if (data.code === 200) { |
| | | uni.showToast({ title: 'ä¸ä¼ æå', icon: 'success' }); |
| | | currentAttachmentList.value.push({ |
| | | name: file.name, |
| | | size: file.size, |
| | | url: data.data?.url || '', |
| | | id: data.data?.id, |
| | | uid: Date.now(), |
| | | status: 'success', |
| | | }); |
| | | } else { |
| | | uni.showToast({ title: data.msg || 'ä¸ä¼ 失败', icon: 'none' }); |
| | | } |
| | | }, |
| | | fail: () => { |
| | | uni.showToast({ title: 'ä¸ä¼ 失败', icon: 'none' }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | // æ ¼å¼åæä»¶å¤§å° |
| | | const formatFileSize = (size) => { |
| | | if (!size) return '0 B'; |
| | | if (size < 1024) { |
| | | return size + ' B'; |
| | | } else if (size < 1024 * 1024) { |
| | | return (size / 1024).toFixed(2) + ' KB'; |
| | | } else { |
| | | return (size / (1024 * 1024)).toFixed(2) + ' MB'; |
| | | } |
| | | }; |
| | | |
| | | // ä¸è½½éä»¶ |
| | | const downloadAttachment = (row) => { |
| | | if (row.url) { |
| | | proxy.$download.name(row.url); |
| | | } else { |
| | | uni.showToast({ title: 'ä¸è½½é¾æ¥ä¸åå¨', icon: 'none' }); |
| | | } |
| | | }; |
| | | |
| | | // å é¤éä»¶ |
| | | const deleteAttachment = (row, index) => { |
| | | uni.showModal({ |
| | | title: 'å é¤æç¤º', |
| | | content: 'ç¡®å®è¦å é¤è¿ä¸ªéä»¶åï¼', |
| | | success: (res) => { |
| | | if (res.confirm) { |
| | | const deleteUrl = |
| | | import.meta.env.VITE_APP_BASE_API + |
| | | '/basic/customer-follow/file/' + |
| | | row.id; |
| | | uni.request({ |
| | | url: deleteUrl, |
| | | method: 'DELETE', |
| | | header: { |
| | | Authorization: 'Bearer ' + getToken(), |
| | | 'Content-Type': 'application/json', |
| | | }, |
| | | success: (res) => { |
| | | if (res.data.code === 200) { |
| | | currentAttachmentList.value.splice(index, 1); |
| | | uni.showToast({ title: 'å 餿å', icon: 'success' }); |
| | | } else { |
| | | uni.showToast({ title: res.data.msg || 'å é¤å¤±è´¥', icon: 'none' }); |
| | | } |
| | | }, |
| | | fail: () => { |
| | | uni.showToast({ title: 'å é¤å¤±è´¥', icon: 'none' }); |
| | | }, |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | // è·åå½åæ¥æ |
| | | function getCurrentDate() { |
| | | const today = new Date(); |
| | | const year = today.getFullYear(); |
| | | const month = String(today.getMonth() + 1).padStart(2, '0'); |
| | | const day = String(today.getDate()).padStart(2, '0'); |
| | | return `${year}-${month}-${day}`; |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .app-container { |
| | | min-height: 100vh; |
| | | background-color: #f5f5f5; |
| | | padding: 20rpx; |
| | | } |
| | | |
| | | .search-section { |
| | | background-color: #fff; |
| | | padding: 20rpx; |
| | | border-radius: 12rpx; |
| | | margin-bottom: 20rpx; |
| | | } |
| | | |
| | | .search-row { |
| | | margin-bottom: 16rpx; |
| | | } |
| | | |
| | | .search-actions { |
| | | display: flex; |
| | | gap: 20rpx; |
| | | justify-content: flex-end; |
| | | } |
| | | |
| | | .action-bar { |
| | | display: flex; |
| | | gap: 16rpx; |
| | | margin-bottom: 20rpx; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .customer-list { |
| | | height: calc(100vh - 300rpx); |
| | | } |
| | | |
| | | .customer-card { |
| | | background-color: #fff; |
| | | border-radius: 12rpx; |
| | | padding: 24rpx; |
| | | margin-bottom: 20rpx; |
| | | box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20rpx; |
| | | padding-bottom: 16rpx; |
| | | border-bottom: 1rpx solid #f0f0f0; |
| | | } |
| | | |
| | | .customer-name { |
| | | font-size: 32rpx; |
| | | font-weight: bold; |
| | | color: #333; |
| | | } |
| | | |
| | | .card-body { |
| | | margin-bottom: 20rpx; |
| | | } |
| | | |
| | | .info-row { |
| | | display: flex; |
| | | margin-bottom: 12rpx; |
| | | } |
| | | |
| | | .label { |
| | | color: #666; |
| | | font-size: 26rpx; |
| | | width: 140rpx; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .value { |
| | | color: #333; |
| | | font-size: 26rpx; |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-footer { |
| | | display: flex; |
| | | gap: 16rpx; |
| | | justify-content: flex-end; |
| | | padding-top: 16rpx; |
| | | border-top: 1rpx solid #f0f0f0; |
| | | } |
| | | |
| | | .popup-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 30rpx; |
| | | border-bottom: 1rpx solid #eee; |
| | | } |
| | | |
| | | .popup-title { |
| | | font-size: 32rpx; |
| | | font-weight: bold; |
| | | color: #333; |
| | | } |
| | | |
| | | .popup-body { |
| | | padding: 30rpx; |
| | | max-height: 60vh; |
| | | } |
| | | |
| | | .popup-footer { |
| | | display: flex; |
| | | gap: 20rpx; |
| | | padding: 20rpx 30rpx; |
| | | border-top: 1rpx solid #eee; |
| | | } |
| | | |
| | | .contact-section { |
| | | background-color: #f9f9f9; |
| | | padding: 20rpx; |
| | | border-radius: 8rpx; |
| | | margin-bottom: 20rpx; |
| | | } |
| | | |
| | | .contact-phone-row { |
| | | display: flex; |
| | | gap: 16rpx; |
| | | align-items: center; |
| | | } |
| | | |
| | | .upload-area { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | padding: 60rpx; |
| | | border: 2rpx dashed #dcdcdc; |
| | | border-radius: 8rpx; |
| | | margin: 30rpx 0; |
| | | } |
| | | |
| | | .upload-text { |
| | | font-size: 28rpx; |
| | | color: #666; |
| | | margin-top: 16rpx; |
| | | } |
| | | |
| | | .upload-tip { |
| | | font-size: 24rpx; |
| | | color: #999; |
| | | margin-top: 8rpx; |
| | | } |
| | | |
| | | .file-name { |
| | | font-size: 26rpx; |
| | | color: #333; |
| | | margin-bottom: 20rpx; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .detail-section { |
| | | margin-bottom: 30rpx; |
| | | } |
| | | |
| | | .section-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20rpx; |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 30rpx; |
| | | font-weight: bold; |
| | | color: #333; |
| | | } |
| | | |
| | | .info-list { |
| | | background-color: #f9f9f9; |
| | | padding: 20rpx; |
| | | border-radius: 8rpx; |
| | | } |
| | | |
| | | .info-item { |
| | | display: flex; |
| | | padding: 12rpx 0; |
| | | border-bottom: 1rpx solid #eee; |
| | | } |
| | | |
| | | .info-item:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .info-label { |
| | | color: #666; |
| | | font-size: 26rpx; |
| | | width: 200rpx; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .info-value { |
| | | color: #333; |
| | | font-size: 26rpx; |
| | | flex: 1; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .record-card { |
| | | background-color: #f9f9f9; |
| | | padding: 20rpx; |
| | | border-radius: 8rpx; |
| | | margin-bottom: 16rpx; |
| | | } |
| | | |
| | | .record-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 12rpx; |
| | | } |
| | | |
| | | .record-time { |
| | | font-size: 26rpx; |
| | | color: #666; |
| | | } |
| | | |
| | | .record-body { |
| | | margin-bottom: 12rpx; |
| | | } |
| | | |
| | | .record-level { |
| | | font-size: 28rpx; |
| | | font-weight: bold; |
| | | color: #333; |
| | | margin-bottom: 8rpx; |
| | | display: block; |
| | | } |
| | | |
| | | .record-content { |
| | | font-size: 26rpx; |
| | | color: #666; |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .record-footer { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding-top: 12rpx; |
| | | border-top: 1rpx solid #eee; |
| | | } |
| | | |
| | | .record-user { |
| | | font-size: 24rpx; |
| | | color: #999; |
| | | } |
| | | |
| | | .record-actions { |
| | | display: flex; |
| | | gap: 12rpx; |
| | | } |
| | | |
| | | .file-item { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 20rpx; |
| | | background-color: #f9f9f9; |
| | | border-radius: 8rpx; |
| | | margin-bottom: 16rpx; |
| | | } |
| | | |
| | | .file-info { |
| | | display: flex; |
| | | align-items: center; |
| | | flex: 1; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .file-detail { |
| | | margin-left: 16rpx; |
| | | flex: 1; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .file-name { |
| | | font-size: 26rpx; |
| | | color: #333; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .file-size { |
| | | font-size: 24rpx; |
| | | color: #999; |
| | | margin-top: 4rpx; |
| | | } |
| | | |
| | | .file-actions { |
| | | display: flex; |
| | | gap: 12rpx; |
| | | } |
| | | |
| | | .empty-text { |
| | | text-align: center; |
| | | padding: 40rpx; |
| | | color: #999; |
| | | font-size: 28rpx; |
| | | } |
| | | </style> |