| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="account-detail"> |
| | | <PageHeader :title="pageTitle" @back="goBack" /> |
| | | |
| | | <view class="detail-body"> |
| | | <view class="detail-card"> |
| | | <view class="section-header"> |
| | | <text class="section-title">åºæ¬ä¿¡æ¯</text> |
| | | <text class="section-subtitle">请填ååæºçåºç¡èµæ</text> |
| | | </view> |
| | | |
| | | <up-form |
| | | ref="formRef" |
| | | :rules="rules" |
| | | :model="form" |
| | | label-width="90" |
| | | @submit="onSubmit" |
| | | > |
| | | <!-- åºæ¬ä¿¡æ¯ --> |
| | | <up-form-item label="客æ·åç§°" prop="customerName" :required="isAddOrEdit"> |
| | | <up-input |
| | | v-model="form.customerName" |
| | | :disabled="isDetail || isAddOperation" |
| | | placeholder="请è¾å
¥å®¢æ·åç§°" |
| | | /> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="ç份" prop="province"> |
| | | <up-input |
| | | v-model="form.province" |
| | | readonly |
| | | :disabled="isDetail || isAddOperation" |
| | | placeholder="ç¹å»éæ©ç份" |
| | | @click="onProvinceClick" |
| | | /> |
| | | <template #right> |
| | | <up-icon |
| | | name="arrow-right" |
| | | @click="onProvinceClick" |
| | | ></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="åå¸" prop="city"> |
| | | <up-input |
| | | v-model="form.city" |
| | | readonly |
| | | :disabled="isDetail || isAddOperation" |
| | | placeholder="ç¹å»éæ©åå¸" |
| | | @click="onCityClick" |
| | | /> |
| | | <template #right> |
| | | <up-icon |
| | | name="arrow-right" |
| | | @click="onCityClick" |
| | | ></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="åæºæ¥æº" prop="businessSource"> |
| | | <up-input |
| | | v-model="form.businessSource" |
| | | :disabled="isDetail || isAddOperation" |
| | | placeholder="请è¾å
¥åæºæ¥æº" |
| | | /> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="è¡ä¸" prop="industry"> |
| | | <up-input |
| | | v-model="form.industry" |
| | | :disabled="isDetail || isAddOperation" |
| | | placeholder="请è¾å
¥è¡ä¸" |
| | | /> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="主è¥äº§å" prop="mainProducts"> |
| | | <up-input |
| | | v-model="form.mainProducts" |
| | | :disabled="isDetail || isAddOperation" |
| | | placeholder="请è¾å
¥ä¸»è¥äº§å" |
| | | /> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="主è¥ä¸å¡æ¶å
¥" prop="mainBusinessRevenue"> |
| | | <up-input |
| | | v-model="form.mainBusinessRevenue" |
| | | :disabled="isDetail || isAddOperation" |
| | | placeholder="请è¾å
¥ä¸»è¥ä¸å¡æ¶å
¥" |
| | | /> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="客æ·è§æ¨¡" prop="customerScale"> |
| | | <up-input |
| | | v-model="form.customerScale" |
| | | :disabled="isDetail || isAddOperation" |
| | | placeholder="请è¾å
¥å®¢æ·è§æ¨¡" |
| | | /> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="ä¿¡æ¯åç°ç¶" prop="informationState"> |
| | | <up-textarea |
| | | v-model="form.informationState" |
| | | :disabled="isDetail || isAddOperation" |
| | | placeholder="请è¾å
¥ä¿¡æ¯åç°ç¶" |
| | | autoHeight |
| | | :maxlength="300" |
| | | count |
| | | /> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="ç¶æ" prop="status" required @click="onStatusClick"> |
| | | <up-input |
| | | v-model="form.status" |
| | | readonly |
| | | :disabled="isDetail" |
| | | placeholder="ç¹å»éæ©ç¶æ" |
| | | @click="onStatusClick" |
| | | /> |
| | | <template #right> |
| | | <up-icon |
| | | name="arrow-right" |
| | | @click="onStatusClick" |
| | | ></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="ååéé¢(å
)" prop="contractAmount" :required="isAddOrEdit"> |
| | | <up-input |
| | | v-model="form.contractAmount" |
| | | :disabled="isDetail || isAddOperation" |
| | | type="number" |
| | | placeholder="请è¾å
¥ååéé¢" |
| | | /> |
| | | </up-form-item> |
| | | |
| | | <!-- æè¿°ä¿¡æ¯ --> |
| | | <view class="section-header section-header-inner"> |
| | | <text class="section-title">æè¿°ä¿¡æ¯</text> |
| | | <text class="section-subtitle">è¡¥å
åæºè¯´æï¼ä¾¿äºåç»è·è¿</text> |
| | | </view> |
| | | <up-form-item label="æ¹é å
容" prop="description" :required="isAddOrAddOperation"> |
| | | <up-textarea |
| | | v-model="form.description" |
| | | :disabled="isDetail" |
| | | :placeholder="renovationPlaceholder" |
| | | autoHeight |
| | | count |
| | | maxlength="500" |
| | | /> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="仿¬¾æè¿°" prop="paymentDescription"> |
| | | <up-textarea |
| | | v-model="form.paymentDescription" |
| | | :disabled="isDetail" |
| | | placeholder="æ¯å¦å«èµï¼ä¼ä¸æ¯å¦å¼ç¥¨ï¼ä¼ä¸æ¯å¦å补贴æé¢å¤åºé±ï¼" |
| | | autoHeight |
| | | count |
| | | maxlength="500" |
| | | /> |
| | | </up-form-item> |
| | | |
| | | <!-- éä»¶ææ --> |
| | | <view class="section-header section-header-inner"> |
| | | <text class="section-title">éä»¶ææ</text> |
| | | <text class="section-subtitle" v-if="!isDetail">æ¯æå¤æä»¶ä¸ä¼ </text> |
| | | </view> |
| | | |
| | | <view v-if="!isDetail" class="upload-wrap"> |
| | | <up-upload |
| | | :fileList="uploadFileList" |
| | | @afterRead="afterRead" |
| | | @delete="deleteUpload" |
| | | name="attachments" |
| | | multiple |
| | | :maxCount="10" |
| | | :previewImage="false" |
| | | > |
| | | <view class="upload-btn"> |
| | | <up-icon name="plus" size="18" color="#667085"></up-icon> |
| | | <text class="upload-text">ä¸ä¼ éä»¶</text> |
| | | </view> |
| | | </up-upload> |
| | | </view> |
| | | |
| | | <view v-if="existingFiles.length" class="existing-files"> |
| | | <view class="existing-title">å·²ä¸ä¼ </view> |
| | | <view v-for="f in existingFiles" :key="f.id || f.url" class="existing-item"> |
| | | <text class="file-name">{{ f.name || getFileName(f.url) }}</text> |
| | | <view class="file-actions"> |
| | | <u-button size="mini" type="primary" plain @click="downloadFile(f)">ä¸è½½</u-button> |
| | | <u-button |
| | | v-if="!isDetail" |
| | | size="mini" |
| | | type="error" |
| | | plain |
| | | @click="removeExistingFile(f)" |
| | | > |
| | | å é¤ |
| | | </u-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- å½å
¥ä¿¡æ¯ --> |
| | | <view class="section-header section-header-inner"> |
| | | <text class="section-title">å½å
¥ä¿¡æ¯</text> |
| | | </view> |
| | | <up-form-item label="å½å
¥äºº" prop="entryPerson" required> |
| | | <up-input |
| | | v-model="form.entryPerson" |
| | | :disabled="true" |
| | | /> |
| | | </up-form-item> |
| | | |
| | | <up-form-item label="å½å
¥æ¥æ" prop="entryDate" required @click="onEntryDateClick"> |
| | | <up-input |
| | | v-model="form.entryDate" |
| | | readonly |
| | | :disabled="isDetail" |
| | | placeholder="ç¹å»éæ©æ¥æ" |
| | | @click="onEntryDateClick" |
| | | /> |
| | | <template #right> |
| | | <up-icon |
| | | name="arrow-right" |
| | | @click="onEntryDateClick" |
| | | ></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | |
| | | <!-- åå²è®°å½ --> |
| | | <view v-if="changeHistory.length" class="change-history-section"> |
| | | <view class="history-title">åæ´è®°å½</view> |
| | | <view v-for="item in changeHistory" :key="item.id" class="history-item"> |
| | | <view class="history-header"> |
| | | <text class="history-status">{{ getStatusText(item.status) }}</text> |
| | | <text class="history-operator">{{ item.operator }}</text> |
| | | </view> |
| | | <view class="history-time">{{ item.timestamp }}</view> |
| | | <view v-if="item.description" class="history-desc"> |
| | | {{ item.description }} |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- åºé¨æé® --> |
| | | <view v-if="!isDetail" class="footer-btns"> |
| | | <u-button class="cancel-btn" @click="goBack">åæ¶</u-button> |
| | | <u-button class="save-btn" type="primary" @click="onSubmit" :loading="loading">ä¿å</u-button> |
| | | </view> |
| | | </up-form> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- ç份鿩 --> |
| | | <up-action-sheet |
| | | :show="showProvincePicker" |
| | | :actions="provinceActionList" |
| | | title="éæ©ç份" |
| | | @select="onProvinceSelect" |
| | | @close="showProvincePicker = false" |
| | | /> |
| | | |
| | | <!-- åå¸éæ© --> |
| | | <up-action-sheet |
| | | :show="showCityPicker" |
| | | :actions="cityActionList" |
| | | title="éæ©åå¸" |
| | | @select="onCitySelect" |
| | | @close="showCityPicker = false" |
| | | /> |
| | | |
| | | <!-- ç¶æéæ© --> |
| | | <up-action-sheet |
| | | :show="showStatusPicker" |
| | | :actions="statusActionList" |
| | | title="éæ©ç¶æ" |
| | | @select="onStatusSelect" |
| | | @close="showStatusPicker = false" |
| | | /> |
| | | |
| | | <!-- æ¥æéæ© --> |
| | | <up-popup :show="showDatePicker" mode="bottom" @close="showDatePicker = false"> |
| | | <up-datetime-picker |
| | | :show="true" |
| | | v-model="pickerDateValue" |
| | | mode="date" |
| | | @confirm="onDateConfirm" |
| | | @cancel="showDatePicker = false" |
| | | /> |
| | | </up-popup> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed } from 'vue' |
| | | import { onLoad } from '@dcloudio/uni-app' |
| | | import dayjs from 'dayjs' |
| | | import PageHeader from '@/components/PageHeader.vue' |
| | | import useUserStore from '@/store/modules/user' |
| | | import { |
| | | addOpportunity, |
| | | updateOpportunity, |
| | | addDescription, |
| | | getProvinceList, |
| | | getCityList, |
| | | delCommonFile |
| | | } from '@/api/salesManagement/opportunityManagement.js' |
| | | import { getToken } from '@/utils/auth' |
| | | import config from '@/config.js' |
| | | |
| | | const userStore = useUserStore() |
| | | |
| | | const formRef = ref(null) |
| | | const loading = ref(false) |
| | | const operationType = ref('add') |
| | | const renovationPlaceholder = '1.æ ååï¼\n2.å®å¶åï¼\n3.å¤éï¼' |
| | | |
| | | // éä»¶ä¸ä¼ |
| | | const uploadFileList = ref([]) // up-upload ç»å®å表 |
| | | const tempFileIds = ref([]) // æäº¤ç»å端çä¸´æ¶æä»¶ID |
| | | const existingFiles = ref([]) // å·²ä¸ä¼ çéä»¶ï¼è¯¦æ
/ç¼è¾åæ¾ï¼ |
| | | |
| | | const getFileName = (url) => { |
| | | try { |
| | | if (!url) return '' |
| | | return decodeURIComponent(url.split('/').pop()) |
| | | } catch (e) { |
| | | return url || '' |
| | | } |
| | | } |
| | | |
| | | const isImageUrl = (url) => /\.(png|jpe?g|gif|bmp|webp)$/i.test(url || '') |
| | | |
| | | const toAbsoluteUrl = (url) => { |
| | | if (!url) return '' |
| | | if (/^https?:\/\//i.test(url)) return url |
| | | return config.baseUrl.replace(/\/$/, '') + (url.startsWith('/') ? url : `/${url}`) |
| | | } |
| | | |
| | | // åªä¸è½½å°ææºï¼ä¸é¢è§ï¼ |
| | | const downloadFile = (file) => { |
| | | const url = toAbsoluteUrl(file?.url) |
| | | if (!url) return |
| | | |
| | | // H5 ç´æ¥æå¼é¾æ¥è§¦åä¸è½½ |
| | | if (typeof window !== 'undefined' && window?.open) { |
| | | window.open(url, '_blank') |
| | | return |
| | | } |
| | | |
| | | uni.showLoading({ title: 'ä¸è½½ä¸...' }) |
| | | uni.downloadFile({ |
| | | url, |
| | | success: (res) => { |
| | | if (res.statusCode !== 200) { |
| | | uni.hideLoading() |
| | | uni.showToast({ title: 'ä¸è½½å¤±è´¥', icon: 'none' }) |
| | | return |
| | | } |
| | | uni.saveFile({ |
| | | tempFilePath: res.tempFilePath, |
| | | success: () => { |
| | | uni.hideLoading() |
| | | uni.showToast({ title: 'å·²ä¸è½½å°æ¬å°', icon: 'success' }) |
| | | }, |
| | | fail: () => { |
| | | uni.hideLoading() |
| | | uni.showToast({ title: 'ä¿å失败', icon: 'none' }) |
| | | } |
| | | }) |
| | | }, |
| | | fail: () => { |
| | | uni.hideLoading() |
| | | uni.showToast({ title: 'ä¸è½½å¤±è´¥', icon: 'none' }) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const uploadSingle = (fileObj) => { |
| | | return new Promise((resolve, reject) => { |
| | | const filePath = fileObj?.url || fileObj?.tempFilePath || fileObj?.path |
| | | if (!filePath) { |
| | | reject(new Error('æªæ¾å°å¯ä¸ä¼ çæä»¶')) |
| | | return |
| | | } |
| | | uni.uploadFile({ |
| | | url: config.baseUrl + '/file/upload', |
| | | filePath, |
| | | name: 'file', |
| | | formData: { type: 9 }, |
| | | header: { Authorization: 'Bearer ' + getToken() }, |
| | | success: (res) => { |
| | | try { |
| | | const data = JSON.parse(res.data || '{}') |
| | | if (data.code === 200) { |
| | | resolve(data.data) |
| | | } else { |
| | | reject(new Error(data.msg || 'ä¸ä¼ 失败')) |
| | | } |
| | | } catch (e) { |
| | | reject(e) |
| | | } |
| | | }, |
| | | fail: (err) => reject(err) |
| | | }) |
| | | }) |
| | | } |
| | | |
| | | const afterRead = async (event) => { |
| | | const files = Array.isArray(event.file) ? event.file : [event.file] |
| | | for (const f of files) { |
| | | const item = { |
| | | url: f.url, |
| | | name: f.name, |
| | | status: 'uploading', |
| | | message: 'ä¸ä¼ ä¸...' |
| | | } |
| | | const idx = uploadFileList.value.length |
| | | uploadFileList.value.push(item) |
| | | try { |
| | | uni.showLoading({ title: 'ä¸ä¼ ä¸...' }) |
| | | const uploaded = await uploadSingle(f) |
| | | uni.hideLoading() |
| | | uploadFileList.value[idx] = { |
| | | ...uploadFileList.value[idx], |
| | | status: 'success', |
| | | message: '', |
| | | url: uploaded?.url || uploadFileList.value[idx]?.url, |
| | | name: uploaded?.name || uploadFileList.value[idx]?.name, |
| | | tempId: uploaded?.tempId |
| | | } |
| | | if (uploaded?.tempId) { |
| | | tempFileIds.value.push(uploaded.tempId) |
| | | } |
| | | } catch (e) { |
| | | uni.hideLoading() |
| | | uploadFileList.value[idx] = { |
| | | ...uploadFileList.value[idx], |
| | | status: 'failed', |
| | | message: 'ä¸ä¼ 失败' |
| | | } |
| | | uni.showToast({ title: 'ä¸ä¼ 失败', icon: 'none' }) |
| | | } |
| | | } |
| | | } |
| | | |
| | | const deleteUpload = (event) => { |
| | | const index = event?.index |
| | | if (index === undefined || index === null) return |
| | | const removed = uploadFileList.value[index] |
| | | uploadFileList.value.splice(index, 1) |
| | | if (removed?.tempId) { |
| | | const pos = tempFileIds.value.findIndex(id => String(id) === String(removed.tempId)) |
| | | if (pos > -1) tempFileIds.value.splice(pos, 1) |
| | | } |
| | | } |
| | | |
| | | const removeExistingFile = (file) => { |
| | | if (!file?.id) { |
| | | existingFiles.value = existingFiles.value.filter(f => f !== file) |
| | | return |
| | | } |
| | | uni.showModal({ |
| | | title: 'æç¤º', |
| | | content: 'ç¡®å®å é¤è¯¥éä»¶åï¼', |
| | | success: async (res) => { |
| | | if (!res.confirm) return |
| | | try { |
| | | const resp = await delCommonFile([file.id]) |
| | | if (resp.code === 200) { |
| | | uni.showToast({ title: 'å 餿å', icon: 'success' }) |
| | | existingFiles.value = existingFiles.value.filter(f => f.id !== file.id) |
| | | } else { |
| | | uni.showToast({ title: resp.msg || 'å é¤å¤±è´¥', icon: 'none' }) |
| | | } |
| | | } catch (e) { |
| | | uni.showToast({ title: 'å é¤å¤±è´¥', icon: 'none' }) |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const form = ref({ |
| | | id: undefined, |
| | | status: '', |
| | | province: '', |
| | | city: '', |
| | | customerName: '', |
| | | industry: '', |
| | | informationState: '', |
| | | mainBusinessRevenue: '', |
| | | customerScale: '', |
| | | mainProducts: '', |
| | | businessSource: '', |
| | | contractAmount: '', |
| | | description: '', |
| | | paymentDescription: '', |
| | | entryPerson: userStore.nickName, |
| | | entryDate: dayjs().format('YYYY-MM-DD'), |
| | | businessDescription: [], |
| | | businessCommonFiles: [] |
| | | }) |
| | | |
| | | const rules = { |
| | | customerName: [ |
| | | { required: true, message: '请è¾å
¥å®¢æ·åç§°', trigger: ['blur', 'change'] } |
| | | ], |
| | | status: [ |
| | | { required: true, message: 'è¯·éæ©ç¶æ', trigger: ['blur', 'change'] } |
| | | ], |
| | | contractAmount: [ |
| | | { required: true, message: '请è¾å
¥ååéé¢', trigger: ['blur', 'change'] } |
| | | ], |
| | | description: [ |
| | | { required: true, message: '请è¾å
¥æ¹é å
容', trigger: ['blur', 'change'] } |
| | | ], |
| | | entryPerson: [ |
| | | { required: true, message: '请è¾å
¥å½å
¥äºº', trigger: ['blur', 'change'] } |
| | | ], |
| | | entryDate: [ |
| | | { required: true, message: 'è¯·éæ©å½å
¥æ¥æ', trigger: ['blur', 'change'] } |
| | | ] |
| | | } |
| | | |
| | | // ç¶æ / çå¸é项 |
| | | const statusOptions = [ |
| | | { value: 'æ°å»º', label: 'æ°å»º' }, |
| | | { value: '项ç®è·è¸ª', label: '项ç®è·è¸ª' }, |
| | | { value: 'ååç¾çº¦', label: 'ååç¾çº¦' }, |
| | | { value: '夿¡ç³æ¥', label: '夿¡ç³æ¥' }, |
| | | { value: '项ç®äº¤ä»', label: '项ç®äº¤ä»' }, |
| | | { value: '项ç®éªæ¶', label: '项ç®éªæ¶' } |
| | | ] |
| | | |
| | | const provinceOptions = ref([]) |
| | | const cityOptions = ref([]) |
| | | const selectedProvinceId = ref(null) |
| | | |
| | | const statusActionList = computed(() => |
| | | statusOptions.map(item => ({ |
| | | name: item.label, |
| | | value: item.value |
| | | })) |
| | | ) |
| | | |
| | | const provinceActionList = computed(() => |
| | | provinceOptions.value.map(item => ({ |
| | | name: item.name, |
| | | value: item.id |
| | | })) |
| | | ) |
| | | |
| | | const cityActionList = computed(() => |
| | | cityOptions.value.map(item => ({ |
| | | name: item.name, |
| | | value: item.id |
| | | })) |
| | | ) |
| | | |
| | | const showProvincePicker = ref(false) |
| | | const showCityPicker = ref(false) |
| | | const showStatusPicker = ref(false) |
| | | const showDatePicker = ref(false) |
| | | const pickerDateValue = ref(Date.now()) |
| | | |
| | | const changeHistory = ref([]) |
| | | |
| | | const isDetail = computed(() => operationType.value === 'detail') |
| | | const isAddOperation = computed(() => operationType.value === 'addOperation') |
| | | const isAddOrEdit = computed(() => ['add', 'edit'].includes(operationType.value)) |
| | | const isAddOrAddOperation = computed(() => ['add', 'addOperation'].includes(operationType.value)) |
| | | |
| | | const pageTitle = computed(() => { |
| | | switch (operationType.value) { |
| | | case 'add': |
| | | return 'æ°å»ºåæº' |
| | | case 'edit': |
| | | return 'ç¼è¾åæº' |
| | | case 'addOperation': |
| | | return 'æ·»å æè¿°' |
| | | case 'detail': |
| | | default: |
| | | return 'åæºè¯¦æ
' |
| | | } |
| | | }) |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack() |
| | | } |
| | | |
| | | const getStatusText = (status) => { |
| | | const map = statusOptions.reduce((acc, cur) => { |
| | | acc[cur.value] = cur.label |
| | | return acc |
| | | }, {}) |
| | | return map[status] || status || 'æªç¥' |
| | | } |
| | | |
| | | // å è½½ç份 |
| | | const loadProvinces = async () => { |
| | | try { |
| | | const res = await getProvinceList() |
| | | provinceOptions.value = res.data || res.records || [] |
| | | } catch (e) { |
| | | console.error('è·åç份å表失败:', e) |
| | | } |
| | | } |
| | | |
| | | // æ ¹æ®ç份å è½½åå¸ |
| | | const loadCitiesByProvinceId = async (provinceId) => { |
| | | if (!provinceId) { |
| | | cityOptions.value = [] |
| | | return |
| | | } |
| | | try { |
| | | const res = await getCityList({ provinceId }) |
| | | cityOptions.value = res.data || res.records || [] |
| | | } catch (e) { |
| | | console.error('è·ååå¸å表失败:', e) |
| | | } |
| | | } |
| | | |
| | | // ç份 / åå¸éæ© |
| | | const onProvinceClick = () => { |
| | | if (isDetail.value || isAddOperation.value) return |
| | | showProvincePicker.value = true |
| | | } |
| | | |
| | | const onProvinceSelect = async (e) => { |
| | | selectedProvinceId.value = e.value |
| | | const target = provinceOptions.value.find(p => p.id === e.value) |
| | | form.value.province = target ? target.name : e.name |
| | | // éç½®åå¸å¹¶å è½½åå¸å表 |
| | | form.value.city = '' |
| | | await loadCitiesByProvinceId(e.value) |
| | | showProvincePicker.value = false |
| | | } |
| | | |
| | | const onCityClick = () => { |
| | | if (isDetail.value || isAddOperation.value) return |
| | | if (!selectedProvinceId.value) { |
| | | uni.showToast({ |
| | | title: '请å
éæ©ç份', |
| | | icon: 'none' |
| | | }) |
| | | return |
| | | } |
| | | showCityPicker.value = true |
| | | } |
| | | |
| | | const onCitySelect = (e) => { |
| | | const target = cityOptions.value.find(c => c.id === e.value) |
| | | form.value.city = target ? target.name : e.name |
| | | showCityPicker.value = false |
| | | } |
| | | |
| | | // ç¶æéæ© |
| | | const onStatusClick = () => { |
| | | if (isDetail.value) return |
| | | showStatusPicker.value = true |
| | | } |
| | | |
| | | const onStatusSelect = (e) => { |
| | | form.value.status = e.value |
| | | showStatusPicker.value = false |
| | | } |
| | | |
| | | // å½å
¥æ¥æéæ© |
| | | const onEntryDateClick = () => { |
| | | if (isDetail.value) return |
| | | showDatePicker.value = true |
| | | } |
| | | |
| | | const onDateConfirm = (e) => { |
| | | const val = e.value || e |
| | | form.value.entryDate = dayjs(val).format('YYYY-MM-DD') |
| | | showDatePicker.value = false |
| | | } |
| | | |
| | | // çæåæ´è®°å½ |
| | | const generateChangeHistory = (row) => { |
| | | const history = [] |
| | | if (row.businessDescription && Array.isArray(row.businessDescription)) { |
| | | row.businessDescription.forEach((item, index) => { |
| | | history.push({ |
| | | id: item.id || index, |
| | | timestamp: item.entryDate || item.updateTime || item.createTime, |
| | | operator: item.entryPerson || 'ç³»ç»', |
| | | status: item.status, |
| | | description: item.description |
| | | }) |
| | | }) |
| | | } |
| | | changeHistory.value = history |
| | | } |
| | | |
| | | // æäº¤è¡¨å |
| | | const onSubmit = () => { |
| | | if (isDetail.value) return |
| | | if (!formRef.value) return |
| | | |
| | | formRef.value.validate().then(async () => { |
| | | loading.value = true |
| | | try { |
| | | let api |
| | | let params |
| | | |
| | | if (operationType.value === 'add') { |
| | | api = addOpportunity |
| | | params = { |
| | | ...form.value, |
| | | type: 9, |
| | | tempFileIds: tempFileIds.value |
| | | } |
| | | } else if (operationType.value === 'edit') { |
| | | api = updateOpportunity |
| | | params = { |
| | | ...form.value, |
| | | type: 9, |
| | | tempFileIds: tempFileIds.value |
| | | } |
| | | } else if (operationType.value === 'addOperation') { |
| | | api = addDescription |
| | | params = { |
| | | status: form.value.status, |
| | | description: form.value.description, |
| | | paymentDescription: form.value.paymentDescription, |
| | | entryPerson: form.value.entryPerson, |
| | | entryDate: form.value.entryDate, |
| | | type: 9, |
| | | businessOpportunityId: form.value.id, |
| | | tempFileIds: tempFileIds.value |
| | | } |
| | | } |
| | | |
| | | const res = await api(params) |
| | | if (res.code === 200) { |
| | | uni.showToast({ |
| | | title: 'æä½æå', |
| | | icon: 'success' |
| | | }) |
| | | setTimeout(() => { |
| | | goBack() |
| | | }, 500) |
| | | } else { |
| | | uni.showToast({ |
| | | title: res.msg || 'æä½å¤±è´¥', |
| | | icon: 'none' |
| | | }) |
| | | } |
| | | } catch (e) { |
| | | console.error('åæºæä½å¤±è´¥:', e) |
| | | uni.showToast({ |
| | | title: 'æä½å¤±è´¥ï¼è¯·éè¯', |
| | | icon: 'none' |
| | | }) |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | }).catch(() => {}) |
| | | } |
| | | |
| | | onLoad(async () => { |
| | | // 读åæä½ç±»ååæ°æ® |
| | | const type = uni.getStorageSync('opportunityOperationType') || 'add' |
| | | operationType.value = type |
| | | |
| | | // å è½½ç份å表 |
| | | await loadProvinces() |
| | | |
| | | const raw = uni.getStorageSync('opportunityData') |
| | | let row = null |
| | | |
| | | // å
¼å®¹å¤ç§åå¨å½¢å¼ï¼å符串 / 对象 / null |
| | | if (raw) { |
| | | try { |
| | | if (typeof raw === 'string') { |
| | | row = JSON.parse(raw) |
| | | } else if (typeof raw === 'object') { |
| | | row = raw |
| | | } |
| | | } catch (e) { |
| | | console.error('è§£æåæºæ°æ®å¤±è´¥:', e) |
| | | row = null |
| | | } |
| | | } |
| | | |
| | | if (row && typeof row === 'object' && !Array.isArray(row)) { |
| | | try { |
| | | // ä¿çå·²æå段ï¼é¿å
è¦çæªå¨è¡¨åä¸ç¼è¾çåæ®µ |
| | | form.value = Object.assign({}, form.value, row) |
| | | |
| | | // å
¼å®¹å端è¿å nullï¼é¿å
ç»ä»¶å
é¨è¯» length æ¥é |
| | | const nullToEmpty = (v) => (v === null || v === undefined ? '' : v) |
| | | form.value.status = nullToEmpty(form.value.status) |
| | | form.value.province = nullToEmpty(form.value.province) |
| | | form.value.city = nullToEmpty(form.value.city) |
| | | form.value.customerName = nullToEmpty(form.value.customerName) |
| | | form.value.businessSource = nullToEmpty(form.value.businessSource) |
| | | form.value.industry = nullToEmpty(form.value.industry) |
| | | form.value.mainProducts = nullToEmpty(form.value.mainProducts) |
| | | form.value.mainBusinessRevenue = nullToEmpty(form.value.mainBusinessRevenue) |
| | | form.value.customerScale = nullToEmpty(form.value.customerScale) |
| | | form.value.informationState = nullToEmpty(form.value.informationState) |
| | | form.value.contractAmount = nullToEmpty(form.value.contractAmount) |
| | | form.value.description = nullToEmpty(form.value.description) |
| | | form.value.paymentDescription = nullToEmpty(form.value.paymentDescription) |
| | | form.value.entryPerson = nullToEmpty(form.value.entryPerson) |
| | | form.value.entryDate = nullToEmpty(form.value.entryDate) |
| | | form.value.businessDescription = Array.isArray(form.value.businessDescription) ? form.value.businessDescription : [] |
| | | form.value.businessCommonFiles = Array.isArray(form.value.businessCommonFiles) ? form.value.businessCommonFiles : [] |
| | | |
| | | // åæ¾éä»¶ |
| | | existingFiles.value = form.value.businessCommonFiles |
| | | uploadFileList.value = [] |
| | | tempFileIds.value = [] |
| | | |
| | | // åæ¾ç份ååå¸ |
| | | if (row.province) { |
| | | const provinceMatch = provinceOptions.value.find(p => p.name === row.province || String(p.id) === String(row.province)) |
| | | if (provinceMatch) { |
| | | selectedProvinceId.value = provinceMatch.id |
| | | form.value.province = provinceMatch.name |
| | | await loadCitiesByProvinceId(provinceMatch.id) |
| | | if (row.city) { |
| | | const cityMatch = cityOptions.value.find(c => c.name === row.city || String(c.id) === String(row.city)) |
| | | form.value.city = cityMatch ? cityMatch.name : row.city |
| | | } |
| | | } else { |
| | | form.value.province = row.province |
| | | form.value.city = row.city || '' |
| | | } |
| | | } else { |
| | | form.value.province = '' |
| | | form.value.city = row.city || '' |
| | | } |
| | | |
| | | if (!form.value.entryPerson) { |
| | | form.value.entryPerson = userStore.nickName |
| | | } |
| | | if (!form.value.entryDate) { |
| | | form.value.entryDate = dayjs().format('YYYY-MM-DD') |
| | | } |
| | | generateChangeHistory(row) |
| | | } catch (e) { |
| | | console.error('å¤çåæºæ°æ®å¤±è´¥:', e) |
| | | } |
| | | } else { |
| | | // æ°å»ºæ¨¡å¼é»è®¤å½å
¥ä¿¡æ¯ |
| | | form.value.entryPerson = userStore.nickName |
| | | form.value.entryDate = dayjs().format('YYYY-MM-DD') |
| | | existingFiles.value = [] |
| | | uploadFileList.value = [] |
| | | tempFileIds.value = [] |
| | | } |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import '@/styles/sales-common.scss'; |
| | | |
| | | .account-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 80px; |
| | | } |
| | | |
| | | .detail-body { |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .detail-card { |
| | | background: #ffffff; |
| | | border-radius: 16px; |
| | | box-shadow: 0 8px 24px rgba(15, 35, 52, 0.06); |
| | | padding: 8px 16px 16px; |
| | | } |
| | | |
| | | .section-header { |
| | | margin: 8px 0 4px; |
| | | } |
| | | |
| | | .section-header-inner { |
| | | margin-top: 18px; |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #1f2933; |
| | | } |
| | | |
| | | .section-subtitle { |
| | | margin-left: 8px; |
| | | font-size: 12px; |
| | | color: #9ca3af; |
| | | } |
| | | |
| | | .footer-btns { |
| | | display: flex; |
| | | gap: 12px; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .cancel-btn { |
| | | flex: 1; |
| | | } |
| | | |
| | | .save-btn { |
| | | flex: 1; |
| | | } |
| | | |
| | | .change-history-section { |
| | | padding: 16px 20px 0 20px; |
| | | } |
| | | |
| | | .history-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .history-item { |
| | | background: #ffffff; |
| | | border-radius: 8px; |
| | | padding: 12px; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .history-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .history-status { |
| | | font-size: 13px; |
| | | color: #2979ff; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .history-operator { |
| | | font-size: 12px; |
| | | color: #999; |
| | | } |
| | | |
| | | .history-time { |
| | | font-size: 12px; |
| | | color: #999; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .history-desc { |
| | | font-size: 13px; |
| | | color: #333; |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .upload-wrap { |
| | | padding: 6px 0 2px; |
| | | } |
| | | |
| | | .upload-btn { |
| | | height: 40px; |
| | | padding: 0 12px; |
| | | border-radius: 10px; |
| | | background: #f2f4f7; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | } |
| | | |
| | | .upload-text { |
| | | font-size: 13px; |
| | | color: #475467; |
| | | } |
| | | |
| | | .existing-files { |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .existing-title { |
| | | font-size: 12px; |
| | | color: #98a2b3; |
| | | margin-bottom: 6px; |
| | | } |
| | | |
| | | .existing-item { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 10px 0; |
| | | border-top: 1px solid #f2f4f7; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .file-name { |
| | | flex: 1; |
| | | font-size: 13px; |
| | | color: #1f2933; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .file-actions { |
| | | display: flex; |
| | | gap: 6px; |
| | | } |
| | | |
| | | // 表åç»èä¼å |
| | | :deep(.u-form) { |
| | | .u-form-item__body { |
| | | padding: 6px 0; |
| | | } |
| | | } |
| | | |
| | | :deep(.u-form-item__label) { |
| | | font-size: 13px; |
| | | color: #6b7280; |
| | | } |
| | | |
| | | :deep(.u-input__content__field) { |
| | | font-size: 14px; |
| | | } |
| | | |
| | | :deep(.u-textarea__field) { |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| | | |