| src/api/basicData/contact.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/api/collaborativeApproval/journal.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/api/salesManagement/opportunityManagement.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/components/filePreview/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/basicData/contact/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/basicData/customerFile/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/basicData/customerFileOpenSea/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/collaborativeApproval/journal/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/salesManagement/opportunityManagement/fileList.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/salesManagement/opportunityManagement/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/api/basicData/contact.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,53 @@ import request from '@/utils/request' // å页æ¥è¯¢è系人å表 export function listContact(query) { return request({ url: '/customerContact/listPage', method: 'get', params: query }) } // æ ¹æ®IDæ¥è¯¢è系人详æ export function getContactById(id) { return request({ url: `/customerContact/getById/${id}`, method: 'get' }) } // æ°å¢è系人 export function addContact(data) { return request({ url: '/customerContact/add', method: 'post', data: data }) } // ä¿®æ¹è系人 export function updateContact(data) { return request({ url: '/customerContact/update', method: 'put', data: data }) } // å é¤è系人 export function delContact(id) { return request({ url: `/customerContact/delete/${id}`, method: 'delete' }) } // æ¹éå é¤è系人 export function delContacts(ids) { return request({ url: '/customerContact/delete', method: 'delete', data: ids }) } src/api/collaborativeApproval/journal.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,62 @@ import request from "@/utils/request"; // æ¥è¯¢æ¥å¿å表 export function listJournal(query) { return request({ url: "/collaborativeApproval/journal/page", method: "get", params: query, }); } // æ¥è¯¢æ¥å¿è¯¦ç» export function getJournal(journalId) { return request({ url: "/collaborativeApproval/journal/" + journalId, method: "get", }); } // æ°å¢æ¥å¿ export function addJournal(data) { return request({ url: "/collaborativeApproval/journal/add", method: "post", data: data, }); } // ä¿®æ¹æ¥å¿ export function updateJournal(data) { return request({ url: "/collaborativeApproval/journal/update", method: "put", data: data, }); } // å 餿¥å¿ export function delJournal(ids) { return request({ url: "/collaborativeApproval/journal/" + ids, method: "delete", }); } // æ¨éæ¥å¿ export function pushJournal(data) { return request({ url: "/collaborativeApproval/journal/push", method: "post", data: data, }); } // è·åç¨æ·å表ï¼ç¨äºæ¨ééæ©ï¼ export function listUser(query) { return request({ url: "/system/user/list", method: "get", params: query, }); } src/api/salesManagement/opportunityManagement.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,61 @@ // åæºç®¡çæ¥å£ import request from "@/utils/request"; // å页æ¥è¯¢åæºå表 export function opportunityListPage(query) { return request({ url: "/businessOpportunity/listPage", method: "get", params: query, }); } // æ°å¢åæº export function addOpportunity(data) { return request({ url: "/businessOpportunity/add", method: "post", data: data, }); } // ä¿®æ¹åæº export function updateOpportunity(data) { return request({ url: "/businessOpportunity/update", method: "put", data: data, }); } // æ·»å åæº export function addDescription(data) { return request({ url: "/businessOpportunity/addDescription", method: "post", data: data, }); } // å é¤åæº export function delOpportunity(ids) { return request({ url: "/businessOpportunity/delete", method: "delete", data: ids, }); } // æ¥è¯¢ç export function getProvinceList() { return request({ url: "/businessOpportunity/getProvinceList", method: "get", }); } // æ¥è¯¢å¸ export function getCityList(id) { return request({ url: "/businessOpportunity/getCityList", method: "get", params: id, }); } src/components/filePreview/index.vue
@@ -164,7 +164,7 @@ }; const open = (url) => { fileUrl.value = window.location.protocol+'//'+window.location.host+ url; fileUrl.value = url; dialogVisible.value = true; }; const handleClose = () => { src/views/basicData/contact/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,514 @@ <template> <div class="app-container"> <div class="search_form" style="margin-bottom: 20px;"> <div> <span class="search_title">è系人å§åï¼</span> <el-input v-model="searchForm.contactPerson" style="width: 240px;margin-right: 10px" placeholder="请è¾å ¥" @keyup.enter="handleQuery" clearable :prefix-icon="Search" /> <span class="search_title">èç³»çµè¯ï¼</span> <el-input v-model="searchForm.contactPhone" style="width: 240px;margin-right: 10px" placeholder="请è¾å ¥" @keyup.enter="handleQuery" clearable /> <span class="search_title">æå±å®¢æ·ï¼</span> <el-select v-model="searchForm.customerId" placeholder="è¯·éæ©" style="width: 240px" clearable filterable @change="handleQuery"> <el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" /> </el-select> <el-button type="primary" @click="handleQuery" style="margin-left: 10px">æç´¢</el-button> <el-button @click="resetSearch">éç½®</el-button> </div> <div> <el-button type="primary" @click="openForm('add')">æ°å¢è系人</el-button> <el-button type="danger" plain @click="handleDelete">å é¤</el-button> </div> </div> <div class="table_list"> <PIMTable rowKey="id" :column="tableColumn" :tableData="tableData" :page="page" :isSelection="true" @selection-change="handleSelectionChange" :tableLoading="tableLoading" @pagination="pagination"> <template #customerName="{ row }"> <div class="customer-tags"> <el-tag v-for="(name, index) in formatCustomerNames(row.customerNames)" :key="index" size="small" type="info" class="customer-tag"> {{ name }} </el-tag> </div> </template> </PIMTable> </div> <!-- è系人表åå¯¹è¯æ¡ --> <FormDialog v-model="dialogFormVisible" :title="dialogTitle" :operation-type="operationType" width="600px" @close="closeDia" @confirm="submitForm" @cancel="closeDia"> <el-form :model="form" label-width="100px" :rules="rules" ref="formRef"> <el-form-item label="è系人ï¼" prop="contactPerson"> <el-input v-model="form.contactPerson" placeholder="请è¾å ¥è系人å§å" clearable /> </el-form-item> <el-form-item label="èç³»çµè¯ï¼" prop="contactPhone"> <el-input v-model="form.contactPhone" placeholder="请è¾å ¥èç³»çµè¯" clearable /> </el-form-item> <el-form-item label="æå±å®¢æ·ï¼" prop="customerIdList"> <el-select v-model="form.customerIdList" placeholder="è¯·éæ©æå±å®¢æ·ï¼å¯å¤éï¼" style="width: 100%" filterable clearable multiple> <el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" /> </el-select> </el-form-item> <el-form-item label="夿³¨ï¼" prop="remark"> <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请è¾å ¥å¤æ³¨" clearable /> </el-form-item> </el-form> </FormDialog> <!-- 客æ·è¯¦æ å¯¹è¯æ¡ --> <FormDialog v-model="customerDetailVisible" title="ç»å®å®¢æ·è¯¦æ " operation-type="detail" width="700px" @close="closeCustomerDetail" @cancel="closeCustomerDetail"> <div class="customer-detail"> <el-table :data="currentCustomerList" border style="width: 100%"> <el-table-column prop="customerName" label="客æ·åç§°" min-width="150" /> <el-table-column prop="customerType" label="客æ·åç±»" width="120" /> <el-table-column prop="companyPhone" label="å ¬å¸çµè¯" width="150" /> <el-table-column prop="companyAddress" label="å ¬å¸å°å" min-width="200" show-overflow-tooltip /> </el-table> </div> </FormDialog> </div> </template> <script setup> import { onMounted, ref, reactive, getCurrentInstance, computed } from "vue"; import { Search } from "@element-plus/icons-vue"; import { listContact, getContactById, addContact, updateContact, delContacts } from "@/api/basicData/contact.js"; import { listCustomer } from "@/api/basicData/customer.js"; import { ElMessageBox } from "element-plus"; import FormDialog from "@/components/Dialog/FormDialog.vue"; const { proxy } = getCurrentInstance(); // å¯¹è¯æ¡æ é¢ const dialogTitle = computed(() => { return operationType.value === "add" ? "æ°å¢è系人" : "ç¼è¾è系人"; }); // è¡¨æ ¼åå®ä¹ const tableColumn = ref([ { label: "è系人å§å", prop: "contactPerson", }, { label: "èç³»çµè¯", prop: "contactPhone", }, { label: "æå±å®¢æ·", prop: "customerNames", dataType: "slot", slot: "customerName", }, { label: "夿³¨", prop: "remark", showOverflowTooltip: true, }, { dataType: "action", label: "æä½", align: "center", fixed: "right", width: 200, operation: [ { name: "ç¼è¾", type: "text", clickFun: row => { openForm("edit", row); }, }, { name: "æ¥ç客æ·", type: "text", clickFun: row => { viewCustomerDetail(row); }, }, { name: "å é¤", type: "text", style: "color: #f56c6c", clickFun: row => { handleDeleteRow(row); }, }, ], }, ]); const tableData = ref([]); const selectedRows = ref([]); const customerList = ref([]); const customerMap = ref(new Map()); const tableLoading = ref(false); const page = reactive({ current: 1, size: 10, total: 0, }); // æç´¢è¡¨å const searchForm = reactive({ contactPerson: "", contactPhone: "", customerId: "", }); // å¯¹è¯æ¡ç¸å ³ const operationType = ref(""); const dialogFormVisible = ref(false); const formRef = ref(); // è¡¨åæ°æ® const form = reactive({ id: null, contactPerson: "", contactPhone: "", customerIdList: [], remark: "", }); // 表åéªè¯è§å const rules = { contactPerson: [{ required: true, message: "请è¾å ¥è系人å§å", trigger: "blur" }], contactPhone: [{ required: true, message: "请è¾å ¥èç³»çµè¯", trigger: "blur" }], customerIdList: [{ required: true, message: "è¯·éæ©æå±å®¢æ·", trigger: "change", type: "array" }], }; // 客æ·è¯¦æ ç¸å ³ const customerDetailVisible = ref(false); const currentCustomerList = ref([]); // è·å客æ·å表ï¼ç¨äºä¸æéæ©ï¼ const getCustomerList = () => { listCustomer({ current: -1, size: -1 }).then(res => { if (res.data && res.data.records) { customerList.value = res.data.records; // æå»ºå®¢æ·IDå°å®¢æ·ä¿¡æ¯çæ å° customerMap.value = new Map(); res.data.records.forEach(item => { customerMap.value.set(item.id, item); }); } }); }; // æ ¼å¼å客æ·åç§°ï¼å¤çåç¬¦ä¸²ææ°ç»æ ¼å¼ï¼ const formatCustomerNames = (customerNames) => { if (!customerNames) return []; if (Array.isArray(customerNames)) return customerNames; if (typeof customerNames === 'string') { return customerNames.split(',').map(s => s.trim()).filter(s => s); } return []; }; // éç½®æç´¢è¡¨å const resetSearch = () => { searchForm.contactPerson = ""; searchForm.contactPhone = ""; searchForm.customerId = ""; handleQuery(); }; // æ¥è¯¢å表 const handleQuery = () => { page.current = 1; getList(); }; const pagination = obj => { page.current = obj.page; page.size = obj.limit; getList(); }; // è·ååè¡¨æ°æ® const getList = () => { tableLoading.value = true; const params = { ...searchForm, current: page.current, size: page.size }; listContact(params).then(res => { tableLoading.value = false; if (res.data) { tableData.value = res.data.records || []; page.total = res.data.total || 0; } }).catch(() => { tableLoading.value = false; }); }; // è¡¨æ ¼éæ©æ°æ® const handleSelectionChange = selection => { selectedRows.value = selection; }; // æå¼è¡¨åå¯¹è¯æ¡ const openForm = (type, row) => { operationType.value = type; resetForm(); if (type === "edit" && row) { form.id = row.id; form.contactPerson = row.contactPerson; form.contactPhone = row.contactPhone; form.customerIdList = row.customerId ? row.customerId.toString().split(',').map(id => Number(id.trim())) : []; form.remark = row.remark || ""; } dialogFormVisible.value = true; }; // é置表å const resetForm = () => { form.id = null; form.contactPerson = ""; form.contactPhone = ""; form.customerIdList = []; form.remark = ""; if (formRef.value) { formRef.value.resetFields(); } }; // å ³éå¯¹è¯æ¡ const closeDia = () => { dialogFormVisible.value = false; resetForm(); }; // æäº¤è¡¨å const submitForm = () => { formRef.value.validate(valid => { if (valid) { if (operationType.value === "edit") { submitEdit(); } else { submitAdd(); } } }); }; // æäº¤æ°å¢ const submitAdd = () => { const submitData = { contactPerson: form.contactPerson, contactPhone: form.contactPhone, customerIdList: form.customerIdList, remark: form.remark, }; addContact(submitData).then(res => { if (res.code === 200) { proxy.$modal.msgSuccess("æ·»å æå"); closeDia(); getList(); } }); }; // æäº¤ç¼è¾ const submitEdit = () => { const submitData = { id: form.id, contactPerson: form.contactPerson, contactPhone: form.contactPhone, customerIdList: form.customerIdList, remark: form.remark, }; updateContact(submitData).then(res => { if (res.code === 200) { proxy.$modal.msgSuccess("ä¿®æ¹æå"); closeDia(); getList(); } }); }; // å é¤åè¡ const handleDeleteRow = (row) => { ElMessageBox.confirm( `ç¡®å®è¦å é¤è系人"${row.contactPerson}"åï¼`, "æç¤º", { confirmButtonText: "ç¡®å®", cancelButtonText: "åæ¶", type: "warning", } ).then(() => { delContacts([row.id]).then(res => { if (res.code === 200) { proxy.$modal.msgSuccess("å 餿å"); getList(); } }); }); }; // æ¹éå é¤ const handleDelete = () => { if (selectedRows.value.length === 0) { proxy.$modal.msgWarning("è¯·éæ©è¦å é¤çæ°æ®"); return; } ElMessageBox.confirm( `ç¡®å®è¦å é¤éä¸ç ${selectedRows.value.length} 个è系人åï¼`, "æç¤º", { confirmButtonText: "ç¡®å®", cancelButtonText: "åæ¶", type: "warning", } ).then(() => { const ids = selectedRows.value.map(row => row.id); delContacts(ids).then(res => { if (res.code === 200) { proxy.$modal.msgSuccess("æ¹éå 餿å"); getList(); } }); }); }; // æ¥ç客æ·è¯¦æ const viewCustomerDetail = (row) => { const customerIdStr = row.customerId; if (!customerIdStr) { proxy.$modal.msgError("该è系人æªç»å®å®¢æ·"); return; } // è·åææç»å®çå®¢æ· const customerIds = customerIdStr.toString().split(',').map(id => Number(id.trim())); const customers = customerIds.map(id => customerMap.value.get(id)).filter(item => item); if (customers.length > 0) { currentCustomerList.value = customers; customerDetailVisible.value = true; } else { proxy.$modal.msgError("客æ·ä¿¡æ¯ä¸åå¨"); } }; // å ³é客æ·è¯¦æ const closeCustomerDetail = () => { customerDetailVisible.value = false; currentCustomerList.value = []; }; onMounted(() => { getList(); getCustomerList(); }); </script> <style scoped> .search_form { display: flex; justify-content: space-between; align-items: center; } .search_title { font-size: 14px; color: #606266; margin-right: 5px; } .customer-detail { padding: 10px; } :deep(.el-descriptions__label) { width: 120px; font-weight: bold; } .customer-tags { display: flex; flex-wrap: wrap; gap: 5px; } .customer-tag { margin-right: 0; } </style> src/views/basicData/customerFile/index.vue
@@ -133,6 +133,7 @@ </el-form-item> </el-col> </el-row> <!-- è系人åè½å·²è¿ç§»å°è系人管çé¡µé¢ <el-row :gutter="30" v-for="(contact, index) in formYYs.contactList" :key="index"> @@ -165,6 +166,7 @@ </el-row> <el-button @click="addNewContact" style="margin-bottom: 10px;">+ æ°å¢è系人</el-button> --> <el-row :gutter="30"> <el-col :span="12"> <el-form-item label="ç»´æ¤äººï¼" @@ -430,6 +432,7 @@ </div> </el-col> </el-row> <!-- è系人信æ¯å·²è¿ç§»å°è系人管çé¡µé¢ <el-row :gutter="20"> <el-col :span="12"> <div class="info-item"> @@ -444,6 +447,7 @@ </div> </el-col> </el-row> --> <el-row :gutter="20"> <el-col :span="12"> <div class="info-item"> src/views/basicData/customerFileOpenSea/index.vue
@@ -133,6 +133,7 @@ </el-form-item> </el-col> </el-row> <!-- è系人åè½å·²è¿ç§»å°è系人管çé¡µé¢ <el-row :gutter="30" v-for="(contact, index) in formYYs.contactList" :key="index"> @@ -165,6 +166,7 @@ </el-row> <el-button @click="addNewContact" style="margin-bottom: 10px;">+ æ°å¢è系人</el-button> --> <el-row :gutter="30"> <el-col :span="12"> <el-form-item label="ç»´æ¤äººï¼" @@ -499,6 +501,7 @@ </div> </el-col> </el-row> <!-- è系人信æ¯å·²è¿ç§»å°è系人管çé¡µé¢ <el-row :gutter="20"> <el-col :span="12"> <div class="info-item"> @@ -513,6 +516,7 @@ </div> </el-col> </el-row> --> <el-row :gutter="20"> <el-col :span="12"> <div class="info-item"> src/views/collaborativeApproval/journal/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,703 @@ <template> <div class="app-container"> <!-- æç´¢è¡¨å --> <div class="search_form"> <el-form :model="searchForm" inline> <el-form-item label="æ¥å¿æ é¢"> <el-input v-model="searchForm.title" placeholder="请è¾å ¥æ¥å¿æ é¢" clearable /> </el-form-item> <el-form-item label="å建人"> <el-input v-model="searchForm.createUserName" placeholder="请è¾å ¥å建人" clearable /> </el-form-item> <el-form-item label="å建æ¶é´"> <el-date-picker v-model="searchForm.dateRange" type="daterange" range-separator="è³" start-placeholder="å¼å§æ¥æ" end-placeholder="ç»ææ¥æ" value-format="YYYY-MM-DD" /> </el-form-item> <el-form-item> <el-button type="primary" @click="handleQuery">æç´¢</el-button> <el-button @click="resetQuery">éç½®</el-button> </el-form-item> </el-form> <div> <el-button type="primary" @click="openForm('add')">ç¼åæ¥å¿</el-button> </div> </div> <!-- æ¥å¿ç®¡çTab --> <div class="journal-board"> <el-tabs v-model="activeJournalType" @tab-change="handleTabChange"> <el-tab-pane label="æ¥æ¥" name="daily"> <template #label> <span> <el-icon><Calendar /></el-icon> æ¥æ¥ <span class="tab-count" v-if="journalCount.daily > 0">({{ journalCount.daily }})</span> </span> </template> </el-tab-pane> <el-tab-pane label="卿¥" name="weekly"> <template #label> <span> <el-icon><Document /></el-icon> 卿¥ <span class="tab-count" v-if="journalCount.weekly > 0">({{ journalCount.weekly }})</span> </span> </template> </el-tab-pane> <el-tab-pane label="ææ¥" name="monthly"> <template #label> <span> <el-icon><DataAnalysis /></el-icon> ææ¥ <span class="tab-count" v-if="journalCount.monthly > 0">({{ journalCount.monthly }})</span> </span> </template> </el-tab-pane> </el-tabs> <!-- æ¥å¿è¡¨æ ¼ --> <el-table :data="journalList" v-loading="loading" border style="width: 100%"> <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="title" label="æ¥å¿æ é¢" min-width="200" show-overflow-tooltip> <template #default="scope"> <el-link type="primary" @click="handleView(scope.row)">{{ scope.row.title }}</el-link> </template> </el-table-column> <el-table-column prop="type" label="æ¥å¿ç±»å" width="100" align="center"> <template #default="scope"> <el-tag :type="getTypeTagType(scope.row.type)">{{ getTypeText(scope.row.type) }}</el-tag> </template> </el-table-column> <el-table-column prop="content" label="æ¥å¿å 容" min-width="300" show-overflow-tooltip /> <el-table-column prop="createUserName" label="å建人" width="120" align="center" /> <el-table-column prop="createTime" label="å建æ¶é´" width="160" align="center" /> <el-table-column prop="pushStatus" label="æ¨éç¶æ" width="100" align="center"> <template #default="scope"> <el-tag :type="scope.row.pushStatus === 1 ? 'success' : 'info'"> {{ scope.row.pushStatus === 1 ? 'å·²æ¨é' : 'æªæ¨é' }} </el-tag> </template> </el-table-column> <el-table-column label="æä½" width="250" align="center" fixed="right"> <template #default="scope"> <el-button link type="primary" @click="handleView(scope.row)">æ¥ç</el-button> <el-button link type="primary" @click="handleEdit(scope.row)">ç¼è¾</el-button> <el-button link type="success" @click="handlePush(scope.row)" v-if="scope.row.pushStatus !== 1">æ¨é</el-button> <el-button link type="danger" @click="handleDelete(scope.row.id)">å é¤</el-button> </template> </el-table-column> </el-table> <!-- å页 --> <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" /> <!-- ç©ºç¶æ --> <div class="empty-state" v-if="journalList.length === 0 && !loading"> <el-empty description="ææ æ¥å¿æ°æ®" /> </div> </div> <!-- ç¼å/ç¼è¾æ¥å¿å¯¹è¯æ¡ --> <el-dialog :title="dialogTitle" v-model="dialogVisible" width="900px" append-to-body @close="resetForm" > <el-form ref="formRef" :model="form" :rules="rules" label-width="100px"> <el-row> <el-col :span="12"> <el-form-item label="æ¥å¿ç±»å" prop="type"> <el-select v-model="form.type" placeholder="è¯·éæ©æ¥å¿ç±»å" style="width: 100%"> <el-option label="æ¥æ¥" value="daily" /> <el-option label="卿¥" value="weekly" /> <el-option label="ææ¥" value="monthly" /> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="æ¥å¿æ é¢" prop="title"> <el-input v-model="form.title" placeholder="请è¾å ¥æ¥å¿æ é¢" /> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="24"> <el-form-item label="æ¥å¿å 容" prop="content"> <el-input v-model="form.content" type="textarea" :rows="10" placeholder="请è¾å ¥æ¥å¿å 容" maxlength="2000" show-word-limit /> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="24"> <el-form-item label="夿³¨"> <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请è¾å ¥å¤æ³¨ä¿¡æ¯" maxlength="500" show-word-limit /> </el-form-item> </el-col> </el-row> </el-form> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="submitForm">ä¿ å</el-button> <el-button type="success" @click="submitAndPushForm">ä¿åå¹¶æ¨é</el-button> <el-button @click="dialogVisible = false">å æ¶</el-button> </div> </template> </el-dialog> <!-- æ¥çæ¥å¿å¯¹è¯æ¡ --> <el-dialog title="æ¥çæ¥å¿" v-model="viewDialogVisible" width="800px" append-to-body > <div class="journal-detail"> <div class="detail-header"> <h3 class="detail-title">{{ currentJournal.title }}</h3> <div class="detail-meta"> <el-tag :type="getTypeTagType(currentJournal.type)">{{ getTypeText(currentJournal.type) }}</el-tag> <el-tag :type="currentJournal.pushStatus === 1 ? 'success' : 'info'"> {{ currentJournal.pushStatus === 1 ? 'å·²æ¨é' : 'æªæ¨é' }} </el-tag> </div> </div> <div class="detail-info"> <span><el-icon><User /></el-icon> å建人ï¼{{ currentJournal.createUserName }}</span> <span><el-icon><Timer /></el-icon> å建æ¶é´ï¼{{ currentJournal.createTime }}</span> </div> <el-divider /> <div class="detail-content"> <div class="content-label">æ¥å¿å 容ï¼</div> <div class="content-text">{{ currentJournal.content }}</div> </div> <div class="detail-remark" v-if="currentJournal.remark"> <div class="content-label">夿³¨ï¼</div> <div class="content-text">{{ currentJournal.remark }}</div> </div> </div> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="handleEditFromView" v-if="currentJournal.pushStatus !== 1">ç¼ è¾</el-button> <el-button type="success" @click="handlePushFromView" v-if="currentJournal.pushStatus !== 1">æ¨ é</el-button> <el-button @click="viewDialogVisible = false">å ³ é</el-button> </div> </template> </el-dialog> <!-- æ¨éæ¥å¿å¯¹è¯æ¡ --> <el-dialog title="æ¨éæ¥å¿" v-model="pushDialogVisible" width="600px" append-to-body > <div class="push-info"> <p><strong>æ¥å¿æ é¢ï¼</strong>{{ currentJournal.title }}</p> <p><strong>æ¥å¿ç±»åï¼</strong>{{ getTypeText(currentJournal.type) }}</p> </div> <el-form ref="pushFormRef" :model="pushForm" :rules="pushRules" label-width="100px"> <el-form-item label="æ¨é人å" prop="userIds"> <el-select v-model="pushForm.userIds" multiple filterable remote reserve-keyword placeholder="请è¾å ¥ç¨æ·åæç´¢" :remote-method="remoteSearchUser" :loading="userLoading" style="width: 100%" > <el-option v-for="item in userOptions" :key="item.userId" :label="item.userName" :value="item.userId" > <span>{{ item.userName }}</span> <span style="float: right; color: #8492a6; font-size: 13px">{{ item.deptName }}</span> </el-option> </el-select> </el-form-item> <el-form-item label="æ¨é夿³¨"> <el-input v-model="pushForm.remark" type="textarea" :rows="3" placeholder="请è¾å ¥æ¨é夿³¨ä¿¡æ¯" /> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="submitPush">ç¡® å®</el-button> <el-button @click="pushDialogVisible = false">å æ¶</el-button> </div> </template> </el-dialog> </div> </template> <script setup> import { Calendar, Document, DataAnalysis, User, Timer } from "@element-plus/icons-vue"; import { onMounted, ref, reactive, toRefs } from "vue"; import { ElMessage, ElMessageBox } from "element-plus"; import useUserStore from "@/store/modules/user"; import { listJournal, getJournal, addJournal, updateJournal, delJournal, pushJournal, listUser } from "@/api/collaborativeApproval/journal.js"; import Pagination from "@/components/Pagination/index.vue"; const userStore = useUserStore(); // ååºå¼æ°æ® const data = reactive({ searchForm: { title: "", createUserName: "", dateRange: [] }, form: { id: undefined, type: "daily", title: "", content: "", remark: "", pushStatus: 0 }, rules: { type: [ { required: true, message: "è¯·éæ©æ¥å¿ç±»å", trigger: "change" } ], title: [ { required: true, message: "æ¥å¿æ é¢ä¸è½ä¸ºç©º", trigger: "blur" } ], content: [ { required: true, message: "æ¥å¿å 容ä¸è½ä¸ºç©º", trigger: "blur" } ] } }); const { searchForm, form, rules } = toRefs(data); // 页é¢ç¶æ const loading = ref(false); const journalList = ref([]); const total = ref(0); const activeJournalType = ref("daily"); const dialogVisible = ref(false); const dialogTitle = ref(""); const formRef = ref(); const viewDialogVisible = ref(false); const pushDialogVisible = ref(false); const pushFormRef = ref(); const currentJournal = ref({}); const userLoading = ref(false); const userOptions = ref([]); // æ¥è¯¢åæ° const queryParams = reactive({ pageNum: 1, pageSize: 10 }); // æ¨é表å const pushForm = reactive({ journalId: undefined, userIds: [], remark: "" }); const pushRules = { userIds: [ { required: true, message: "è¯·éæ©æ¨é人å", trigger: "change", type: "array" } ] }; // æ¥å¿æ°éç»è®¡ const journalCount = reactive({ daily: 0, weekly: 0, monthly: 0 }); // è·åæ¥å¿å表 const getList = () => { loading.value = true; const params = { pageNum: queryParams.pageNum, pageSize: queryParams.pageSize, type: activeJournalType.value }; if (searchForm.value.title) { params.title = searchForm.value.title; } if (searchForm.value.createUserName) { params.createUserName = searchForm.value.createUserName; } if (searchForm.value.dateRange && searchForm.value.dateRange.length === 2) { params.startTime = searchForm.value.dateRange[0]; params.endTime = searchForm.value.dateRange[1]; } listJournal(params).then(res => { if (res.code === 200) { journalList.value = res.data.records || []; total.value = res.data.total || 0; // æ´æ°å½åç±»åçæ°é journalCount[activeJournalType.value] = res.data.total || 0; } loading.value = false; }).catch(() => { loading.value = false; }); }; // è·åææç±»åæ°é const getAllTypeCounts = () => { ['daily', 'weekly', 'monthly'].forEach(type => { listJournal({ pageNum: 1, pageSize: 1, type }).then(res => { if (res.code === 200) { journalCount[type] = res.data.total || 0; } }); }); }; // Tab忢 const handleTabChange = (tabName) => { activeJournalType.value = tabName; queryParams.pageNum = 1; getList(); }; // æç´¢ const handleQuery = () => { queryParams.pageNum = 1; getList(); }; // éç½®æç´¢ const resetQuery = () => { searchForm.value = { title: "", createUserName: "", dateRange: [] }; handleQuery(); }; // è·åç±»åææ¬ const getTypeText = (type) => { const typeMap = { daily: "æ¥æ¥", weekly: "卿¥", monthly: "ææ¥" }; return typeMap[type] || type; }; // è·åç±»åæ ç¾æ ·å¼ const getTypeTagType = (type) => { const typeMap = { daily: "primary", weekly: "success", monthly: "warning" }; return typeMap[type] || ""; }; // æå¼è¡¨å const openForm = (type) => { if (type === 'add') { dialogTitle.value = "ç¼åæ¥å¿"; form.value = { id: undefined, type: activeJournalType.value, title: "", content: "", remark: "", pushStatus: 0 }; } dialogVisible.value = true; }; // ç¼è¾æ¥å¿ const handleEdit = (row) => { if (row.pushStatus === 1) { ElMessage.warning("å·²æ¨éçæ¥å¿ä¸å¯ç¼è¾"); return; } dialogTitle.value = "ç¼è¾æ¥å¿"; form.value = { ...row }; dialogVisible.value = true; }; // 仿¥ç页é¢ç¼è¾ const handleEditFromView = () => { viewDialogVisible.value = false; handleEdit(currentJournal.value); }; // æ¥çæ¥å¿ const handleView = (row) => { getJournal(row.id).then(res => { if (res.code === 200) { currentJournal.value = res.data; viewDialogVisible.value = true; } }); }; // å 餿¥å¿ const handleDelete = (id) => { ElMessageBox.confirm( "确认å é¤è¿æ¡æ¥å¿åï¼", "æç¤º", { confirmButtonText: "ç¡®å®", cancelButtonText: "åæ¶", type: "warning" } ).then(() => { delJournal(id).then(res => { if (res.code === 200) { ElMessage.success("å 餿å"); getList(); getAllTypeCounts(); } }); }); }; // æå¼æ¨éå¼¹çª const handlePush = (row) => { currentJournal.value = row; pushForm.journalId = row.id; pushForm.userIds = []; pushForm.remark = ""; pushDialogVisible.value = true; // å è½½ç¨æ·å表 loadUserOptions(); }; // 仿¥ç页颿¨é const handlePushFromView = () => { viewDialogVisible.value = false; handlePush(currentJournal.value); }; // å è½½ç¨æ·é项 const loadUserOptions = () => { listUser({ pageNum: 1, pageSize: 100 }).then(res => { if (res.code === 200) { userOptions.value = res.data.records || res.data || []; } }); }; // è¿ç¨æç´¢ç¨æ· const remoteSearchUser = (query) => { if (query) { userLoading.value = true; listUser({ userName: query, pageNum: 1, pageSize: 50 }).then(res => { if (res.code === 200) { userOptions.value = res.data.records || res.data || []; } userLoading.value = false; }).catch(() => { userLoading.value = false; }); } }; // æäº¤è¡¨å const submitForm = () => { formRef.value.validate((valid) => { if (valid) { const api = form.value.id ? updateJournal : addJournal; api(form.value).then(res => { if (res.code === 200) { ElMessage.success(form.value.id ? "ä¿®æ¹æå" : "æ°å¢æå"); dialogVisible.value = false; getList(); getAllTypeCounts(); } }); } }); }; // æäº¤å¹¶æ¨é const submitAndPushForm = () => { formRef.value.validate((valid) => { if (valid) { const api = form.value.id ? updateJournal : addJournal; api(form.value).then(res => { if (res.code === 200) { const journalId = form.value.id || res.data; dialogVisible.value = false; // æå¼æ¨éå¼¹çª currentJournal.value = { ...form.value, id: journalId }; pushForm.journalId = journalId; pushForm.userIds = []; pushForm.remark = ""; pushDialogVisible.value = true; loadUserOptions(); getList(); getAllTypeCounts(); } }); } }); }; // æäº¤æ¨é const submitPush = () => { pushFormRef.value.validate((valid) => { if (valid) { pushJournal(pushForm).then(res => { if (res.code === 200) { ElMessage.success("æ¨éæå"); pushDialogVisible.value = false; getList(); getAllTypeCounts(); } }); } }); }; // é置表å const resetForm = () => { formRef.value?.resetFields(); }; onMounted(() => { getList(); getAllTypeCounts(); }); </script> <style scoped> .search_form { display: flex; justify-content: space-between; align-items: flex-start; } .journal-board { background: #fff; padding: 20px; border-radius: 4px; } .tab-count { color: #f56c6c; font-size: 12px; } .empty-state { padding: 40px 0; } .journal-detail { padding: 20px; } .detail-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } .detail-title { margin: 0; font-size: 20px; color: #303133; } .detail-meta { display: flex; gap: 8px; } .detail-info { display: flex; gap: 24px; color: #606266; font-size: 14px; } .detail-info span { display: flex; align-items: center; gap: 4px; } .detail-content, .detail-remark { margin-top: 16px; } .content-label { font-weight: bold; color: #303133; margin-bottom: 8px; } .content-text { color: #606266; line-height: 1.8; white-space: pre-wrap; word-wrap: break-word; background: #f5f7fa; padding: 16px; border-radius: 4px; min-height: 60px; } .push-info { padding: 0 20px 20px; color: #606266; } .push-info p { margin: 8px 0; } .dialog-footer { display: flex; justify-content: flex-end; gap: 10px; } </style> src/views/salesManagement/opportunityManagement/fileList.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,77 @@ <template> <el-dialog v-model="dialogVisible" title="éä»¶" width="40%" :before-close="handleClose" draggable> <el-table :data="tableData" border height="40vh"> <el-table-column label="éä»¶åç§°" prop="originalFilename" min-width="400" show-overflow-tooltip /> <el-table-column fixed="right" label="æä½" width="150" align="center"> <template #default="scope"> <el-button link type="primary" size="small" @click="downLoadFile(scope.row)">ä¸è½½</el-button> <el-button link type="primary" size="small" @click="lookFile(scope.row)">é¢è§</el-button> <el-button link type="danger" size="small" @click="handleDelete(scope.row)">å é¤</el-button> </template> </el-table-column> </el-table> </el-dialog> <filePreview ref="filePreviewRef" /> </template> <script setup> import { ref } from 'vue' import filePreview from '@/components/filePreview/index.vue' import { ElMessageBox, ElMessage } from 'element-plus' import { deleteAttachment } from '@/api/basicData/storageAttachment.js' const dialogVisible = ref(false) const tableData = ref([]) const currentRowId = ref(null) const { proxy } = getCurrentInstance(); const filePreviewRef = ref() const emit = defineEmits(['refresh']) const handleClose = () => { dialogVisible.value = false } const open = (list, rowId) => { dialogVisible.value = true tableData.value = list || [] currentRowId.value = rowId } const downLoadFile = (row) => { proxy.$download.name(row.url); } const lookFile = (row) => { filePreviewRef.value.open(row.url) } // å é¤éä»¶ const handleDelete = (row) => { ElMessageBox.confirm(`确认å é¤éä»¶"${row.originalFilename}"åï¼`, 'æç¤º', { confirmButtonText: 'ç¡®å®', cancelButtonText: 'åæ¶', type: 'warning' }).then(() => { deleteAttachment([row.storageAttachmentId]).then(() => { ElMessage.success('å 餿å') // ä»å表ä¸ç§»é¤å·²å é¤çéä»¶ const index = tableData.value.findIndex(item => item.id === row.id) if (index !== -1) { tableData.value.splice(index, 1) } // 触åå·æ°äºä»¶ emit('refresh', currentRowId.value) }).catch(() => { ElMessage.error('å é¤å¤±è´¥') }) }).catch(() => { ElMessage.info('已忶å é¤') }) } defineExpose({ open }) </script> <style scoped></style> src/views/salesManagement/opportunityManagement/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,1122 @@ <template> <div class="app-container"> <!-- æç´¢åºå --> <div class="search_form"> <el-form :model="searchForm" :inline="true"> <el-form-item label="客æ·åç§°"> <el-input v-model="searchForm.customerName" placeholder="请è¾å ¥å®¢æ·åç§°" clearable prefix-icon="Search" style="width: 200px;" @change="handleQuery" /> </el-form-item> <el-form-item label="åå¸"> <el-input v-model="searchForm.city" placeholder="请è¾å ¥åå¸åç§°" clearable prefix-icon="Search" style="width: 200px" @change="handleQuery" /> </el-form-item> <el-form-item label="ç¶æ"> <el-select v-model="searchForm.status" placeholder="è¯·éæ©ç¶æ" clearable style="width: 160px" @change="handleQuery" > <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" /> </el-select> </el-form-item> <el-form-item label="å½å ¥äºº"> <el-select v-model="searchForm.entryPerson" placeholder="è¯·éæ©å½å ¥äºº" clearable filterable style="width: 200px" @change="handleQuery" > <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName" /> </el-select> </el-form-item> <el-form-item label="å½å ¥æ¥æï¼"> <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange" placeholder="è¯·éæ©" clearable @change="changeDaterange" /> </el-form-item> <el-form-item> <el-button type="primary" @click="handleQuery">æç´¢</el-button> <el-button @click="resetQuery">éç½®</el-button> <el-button type="primary" @click="handleAdd">æ°å»º</el-button> <el-button type="danger" plain @click="handleDelete">å é¤</el-button> </el-form-item> </el-form> </div> <!-- è¡¨æ ¼åºå --> <div class="table_list"> <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange" :row-key="(row) => row.id" height="calc(100vh - 18.5em)" stripe show-summary :summary-method="contractAmountSummaryMethod" > <el-table-column align="center" type="selection" width="55" fixed="left"/> <el-table-column align="center" label="åºå·" type="index" width="60" /> <el-table-column label="ç¶æ" prop="status" width="120"> <template #default="{ row }"> <el-tag :type="getStatusTagType(row.status)" effect="light" > {{ getStatusText(row.status) }} </el-tag> </template> </el-table-column> <el-table-column label="ç份" prop="province" show-overflow-tooltip /> <el-table-column label="å¸" prop="city" show-overflow-tooltip/> <el-table-column label="客æ·åç§°" prop="customerName" show-overflow-tooltip /> <el-table-column label="è¡ä¸" prop="industry" show-overflow-tooltip /> <el-table-column label="åæºæ¥æº" prop="businessSource" show-overflow-tooltip /> <el-table-column label="ç¾çº¦éé¢" prop="contractAmount" show-overflow-tooltip width="150" /> <!-- <el-table-column label="å®¢æ·æè¿°" prop="description" show-overflow-tooltip min-width="200" /> --> <el-table-column label="å½å ¥äºº" prop="entryPerson" show-overflow-tooltip width="120" /> <el-table-column label="æ´æ°æ¥æ" prop="updateTime" width="120"> <template #default="{ row }"> {{ formatDate(row.updateTime) }} </template> </el-table-column> <el-table-column label="æä½" fixed="right" width="240" align="center"> <template #default="{ row }"> <el-button link type="primary" size="small" @click="handleEdit(row)" > ç¼è¾ </el-button> <el-button link type="primary" size="small" @click="handleAddOperation(row)" > æ·»å æè®¿è®°å½ </el-button> <el-button link type="primary" size="small" @click="handleDetail(row)" > 详æ </el-button> <el-button link type="primary" size="small" @click="handleAttachment(row)" > éä»¶ </el-button> </template> </el-table-column> </el-table> <!-- å页ç»ä»¶ --> <pagination v-show="total > 0" :total="total" layout="total, sizes, prev, pager, next, jumper" :page="page.current" :limit="page.size" @pagination="paginationChange" /> </div> <!-- æ°å¢/ç¼è¾å¯¹è¯æ¡ --> <el-dialog v-model="dialogFormVisible" :title="operationType === 'add' ? 'æ°å»ºåæº' : operationType === 'edit' ? 'ç¼è¾åæº' : operationType === 'addOperation' ? 'æ·»å åæº' : 'åæºè¯¦æ '" width="1000px" @close="closeDialog" > <el-form :model="form" :rules="rules" ref="formRef" label-width="100px" label-position="left" > <el-form-item label="ç¶æ" prop="status"> <el-select v-model="form.status" placeholder="è¯·éæ©ç¶æ" style="width: 100%" :disabled="operationType === 'detail'"> <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" /> </el-select> </el-form-item> <el-form-item label="ç份" prop="province"> <el-select v-model="form.province" filterable placeholder="è¯·éæ©ç份" @change="getCityListChange" style="width: 100%" :disabled="operationType === 'detail' || operationType === 'addOperation'"> <el-option v-for="item in provinceOptions" :key="item.id" :label="item.name" :value="item.id" /> </el-select> </el-form-item> <el-form-item label="å¸" prop="city"> <el-select v-model="form.city" filterable placeholder="è¯·éæ©å¸" style="width: 100%" :disabled="operationType === 'detail' || operationType === 'addOperation'"> <el-option v-for="item in cityOptions" :key="item.id" :label="item.name" :value="item.id" /> </el-select> </el-form-item> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="客æ·åç§°" prop="customerName"> <el-select v-model="form.customerName" placeholder="è¯·éæ©" clearable style="width: 100%" :disabled="operationType === 'detail' || operationType === 'addOperation'" > <el-option v-for="item in customerOption" :key="item.customerName" :label="item.customerName" :value="item.customerName" > {{ item.customerName + "ââ" + item.taxpayerIdentificationNumber }} </el-option> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="åæºæ¥æº" prop="businessSource"> <el-input v-model="form.businessSource" placeholder="请è¾å ¥åæºæ¥æº" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="è¡ä¸"> <el-input v-model="form.industry" placeholder="请è¾å ¥è¡ä¸" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="主è¥äº§å"> <el-input v-model="form.mainProducts" placeholder="请è¾å ¥ä¸»è¥äº§å" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="主è¥ä¸å¡æ¶å ¥"> <el-input v-model="form.mainBusinessRevenue" placeholder="请è¾å ¥ä¸»è¥ä¸å¡æ¶å ¥" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="客æ·è§æ¨¡"> <el-input v-model="form.customerScale" placeholder="请è¾å ¥å®¢æ·è§æ¨¡" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="24"> <el-form-item label="ä¿¡æ¯åç°ç¶"> <el-input v-model="form.informationState" placeholder="请è¾å ¥ä¿¡æ¯åç°ç¶" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" /> </el-form-item> </el-col> </el-row> <el-form-item label="ç¾çº¦éé¢" prop="contractAmount"> <el-input v-model="form.contractAmount" placeholder="请è¾å ¥ç¾çº¦éé¢" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" > <template #append>å </template> </el-input> </el-form-item> <el-form-item label="æè®¿è®°å½" prop="description"> <el-input v-model="form.description" type="textarea" :autosize="{ minRows: 4, maxRows: 10 }" placeholder="请è¾å ¥æè®¿è®°å½" show-word-limit :disabled="operationType === 'detail'" /> </el-form-item> <el-form-item label="æ¹é å 容" prop="renContent"> <el-input v-model="form.renContent" type="textarea" :autosize="{ minRows: 3, maxRows: 8 }" :placeholder="renovationPlaceholder" show-word-limit :disabled="operationType === 'detail' || operationType === 'addOperation'" /> </el-form-item> <el-form-item label="仿¬¾æè¿°" prop="paymentDescription"> <el-input v-model="form.paymentDescription" type="textarea" :autosize="{ minRows: 3, maxRows: 10 }" placeholder="æ¯å¦å«èµï¼ä¼ä¸æ¯å¦å¼ç¥¨ï¼ä¼ä¸æ¯å¦å补贴æé¢å¤åºé±ï¼" show-word-limit :disabled="operationType === 'detail' || operationType === 'addOperation'" /> </el-form-item> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="å½å ¥äºº" prop="entryPerson"> <el-select v-model="form.entryPerson" placeholder="è¯·éæ©" clearable @change="changs" :disabled="operationType === 'detail' || operationType === 'addOperation'"> <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName" /> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="å½å ¥æ¥æ" prop="entryDate"> <el-date-picker style="width: 100%" v-model="form.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="date" placeholder="è¯·éæ©" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" /> </el-form-item> </el-col> </el-row> <!-- éä»¶ä¸ä¼ ï¼éè¯¦æ æ¨¡å¼ä¸æ¾ç¤ºï¼ --> <el-row :gutter="30" v-if="operationType !== 'detail'"> <el-col :span="24"> <el-form-item label="éä»¶ææï¼"> <FileUpload v-model:file-list="fileList" /> </el-form-item> </el-col> </el-row> </el-form> <!-- éä»¶æ¥çï¼ä» å¨è¯¦æ 模å¼ä¸æ¾ç¤ºï¼ --> <div v-if="operationType === 'detail'" class="attachment-section"> <el-divider content-position="left">éä»¶ææ</el-divider> <div v-if="form.businessCommonFiles && form.businessCommonFiles.length > 0"> <el-table :data="form.businessCommonFiles" border stripe style="width: 100%"> <el-table-column label="éä»¶åç§°" prop="originalFilename" min-width="400" show-overflow-tooltip /> <el-table-column fixed="right" label="æä½" width="150" align="center"> <template #default="scope"> <el-button link type="primary" size="small" @click="downloadAttachment(scope.row)">ä¸è½½</el-button> <el-button link type="primary" size="small" @click="previewAttachment(scope.row)">é¢è§</el-button> </template> </el-table-column> </el-table> </div> <div v-else style="text-align: center; padding: 20px; color: #999;"> ææ éä»¶ </div> </div> <!-- åæ´è®°å½æ¶é´çº¿ï¼ä» å¨è¯¦æ 模å¼ä¸æ¾ç¤ºï¼ --> <div v-if="operationType === 'detail'" class="change-history-section"> <el-divider content-position="left">åæ´è®°å½</el-divider> <el-timeline> <el-timeline-item v-for="record in changeHistory" :key="record.id" :timestamp="record.timestamp" :type="record.type === 'current' ? 'primary' : record.type === 'update' ? 'success' : 'info'" :hollow="record.type === 'current'" placement="top" > <el-card shadow="hover" class="timeline-card"> <template #header> <div class="card-header"> <span class="action-type">{{ record.action }}</span> <span class="operator">æä½äººï¼{{ record.operator }}</span> </div> </template> <div class="change-content"> <div class="status-change" v-if="record.status"> <span class="label">ç¶æï¼</span> <el-tag :type="record.type === 'current' ? 'primary' : 'info'" size="small"> {{ getStatusLabel(record.status) }} </el-tag> </div> <div class="description-change" v-if="record.description"> <span class="label">æè®¿è®°å½ï¼</span> <span class="description-text">{{ record.description }}</span> </div> </div> </el-card> </el-timeline-item> </el-timeline> </div> <template #footer> <div class="dialog-footer"> <el-button @click="closeDialog">åæ¶</el-button> <el-button type="primary" @click="submitForm" v-if="operationType !== 'detail'"> ç¡®å® </el-button> </div> </template> </el-dialog> <!-- éä»¶åè¡¨å¯¹è¯æ¡ --> <FileList ref="fileListRef" @refresh="handleFileListRefresh" /> <!-- æä»¶é¢è§ç»ä»¶ --> <filePreview ref="filePreviewRef" /> </div> </template> <script setup> import { ref, reactive, onMounted, getCurrentInstance } from 'vue' import { Search } from '@element-plus/icons-vue' import { ElMessageBox, ElMessage } from 'element-plus' import pagination from '@/components/PIMTable/Pagination.vue' import useUserStore from '@/store/modules/user' import dayjs from 'dayjs' import { opportunityListPage, addOpportunity, updateOpportunity, delOpportunity, addDescription, getProvinceList, getCityList } from '@/api/salesManagement/opportunityManagement.js' import { userListNoPage } from '@/api/system/user.js' import {customerList, getSalesLedgerWithProducts} from '@/api/salesManagement/salesLedger.js' import FileList from './fileList.vue' import filePreview from '@/components/filePreview/index.vue' const { proxy } = getCurrentInstance() const userStore = useUserStore() // è¡¨æ ¼æ°æ® const tableData = ref([]) const selectedRows = ref([]) const tableLoading = ref(false) const userList = ref([]) const customerOption = ref([]) const DEFAULT_USER_QUERY = { postCode: 'Market_Sales' } const DEFAULT_CUSTOMER_QUERY = { customerType: 2 } let userListPromise = null let customerListPromise = null const loadUserList = async (query = DEFAULT_USER_QUERY) => { if (userListPromise) return userListPromise userListPromise = (async () => { try { const res = await userListNoPage(query) userList.value = res?.data || [] return userList.value } catch (err) { console.error('è·åç¨æ·å表失败:', err) userList.value = [] userListPromise = null throw err } })() return userListPromise } const loadCustomerList = async (query = DEFAULT_CUSTOMER_QUERY) => { if (customerListPromise) return customerListPromise customerListPromise = (async () => { try { const res = await customerList(query) customerOption.value = res || [] return customerOption.value } catch (err) { console.error('è·å客æ·å表失败:', err) customerOption.value = [] customerListPromise = null throw err } })() return customerListPromise } // å页é ç½® const page = reactive({ current: 1, size: 100, }) const total = ref(0) // æç´¢è¡¨å const searchForm = reactive({ customerName: '', city: '', status: '', entryPerson: '', entryDate: [], entryDateStart: '', entryDateEnd: '' }) // å¯¹è¯æ¡ç¸å ³ const dialogFormVisible = ref(false) const operationType = ref('') // add, detail const formRef = ref() const form = reactive({ id: undefined, status: undefined, province: '', city: '', customerName: '', industry: '', informationState: '', mainBusinessRevenue: '', customerScale: '', mainProducts: '', businessSource: '', contractAmount: '', description: '', renContent: '', paymentDescription: '', entryPerson: userStore.nickName, entryDate: dayjs().format('YYYY-MM-DD') }) const renovationPlaceholder = '1.æ ååï¼\n2.å®å¶åï¼\n3.å¤éï¼' // åæ´è®°å½æ°æ®ï¼æ¨¡ææ°æ®ï¼ const changeHistory = ref([]) // æä»¶å表 const fileList = ref([]) // FileListç»ä»¶å¼ç¨ const fileListRef = ref(null) const currentAttachmentRow = ref(null) const filePreviewRef = ref(null) // è·åç¶ææ ç¾ const getStatusLabel = (statusValue) => { const status = statusOptions.find(item => item.value === statusValue) return status ? status.label : statusValue } // 表åéªè¯è§å const rules = reactive({ customerName: [ { required: true, message: 'è¯·éæ©å®¢æ·', trigger: 'change' } ], status: [ { required: true, message: 'è¯·éæ©ç¶æ', trigger: 'change' } ], entryPerson: [ { required: true, message: 'è¯·éæ©å½å ¥äºº', trigger: 'change' } ], entryDate: [ { required: true, message: 'è¯·éæ©å½å ¥æ¥æ', trigger: 'change' } ] }) // ç¶æé项 const statusOptions = [ { value: 'æ°å»º', label: 'æ°å»º' }, { value: '项ç®è·è¸ª', label: '项ç®è·è¸ª' }, { value: 'æ¾å¼', label: 'æ¾å¼' }, { value: 'ååç¾çº¦', label: 'ååç¾çº¦' }, { value: '夿¡ç³æ¥', label: '夿¡ç³æ¥' }, { value: '项ç®äº¤ä»', label: '项ç®äº¤ä»' }, { value: '项ç®éªæ¶', label: '项ç®éªæ¶' }, { value: '项ç®å款', label: '项ç®å款' }, { value: 'å补贴', label: 'å补贴' } ] // ç份é项 const provinceOptions = ref([]) const cityOptions = ref([]) // è·åç¶ææ ç¾ç±»å const getStatusTagType = (status) => { const typeMap = { 'æ°å»º': 'info', '项ç®è·è¸ª': 'primary', 'æ¾å¼': 'danger', 'ååç¾çº¦': 'warning', '夿¡ç³æ¥': 'primary', '项ç®äº¤ä»': 'success', '项ç®éªæ¶': 'success', '项ç®å款': 'success', 'å补贴': 'success' } return typeMap[status] || 'info' } // è·åç¶æææ¬ const getStatusText = (status) => { const textMap = { 'æ°å»º': 'æ°å»º', '项ç®è·è¸ª': '项ç®è·è¸ª', 'æ¾å¼': 'æ¾å¼', 'ååç¾çº¦': 'ååç¾çº¦', '夿¡ç³æ¥': '夿¡ç³æ¥', '项ç®äº¤ä»': '项ç®äº¤ä»', '项ç®éªæ¶': '项ç®éªæ¶', '项ç®å款': '项ç®å款', 'å补贴': 'å补贴' } return textMap[status] || 'æªç¥' } // æ ¼å¼åæ¥æ const formatDate = (date) => { if (!date) return '' return dayjs(date).format('YYYY-MM-DD') } // æ¥è¯¢å表 const handleQuery = () => { page.current = 1 getList() } // éç½®æ¥è¯¢ const resetQuery = () => { Object.assign(searchForm, { customerName: '', city: '', status: '', entryPerson: '', entryDate: [], entryDateStart: '', entryDateEnd: '' }) handleQuery() } // æ¥æèå´åå const changeDaterange = (val) => { if (val && val.length === 2) { searchForm.entryDateStart = val[0] searchForm.entryDateEnd = val[1] } else { searchForm.entryDateStart = '' searchForm.entryDateEnd = '' } handleQuery() } // è·ååè¡¨æ°æ® const getList = () => { tableLoading.value = true // å建æ¥è¯¢åæ°ï¼æé¤entryDateåæ®µï¼åªä½¿ç¨entryDateStartåentryDateEnd const { entryDate, ...queryParams } = searchForm const params = { ...queryParams, ...page } // å é¤ç©ºå¼åæ° Object.keys(params).forEach(key => { if (params[key] === '' || params[key] === null || params[key] === undefined) { delete params[key] } }) opportunityListPage(params).then(res => { tableData.value = res.data.records || [] total.value = res.data.total || 0 }).catch(err => { console.error('è·ååæºå表失败:', err) tableData.value = [] total.value = 0 }).finally(() => { tableLoading.value = false }) } // ç¾çº¦éé¢å计ï¼å¤ç¨å ¨å± summarizeTableï¼ const contractAmountSummaryMethod = (param) => { return proxy.summarizeTable(param, ["contractAmount"]); } // å页åå const paginationChange = (pagination) => { page.current = pagination.page page.size = pagination.limit getList() } // éæ©åå const handleSelectionChange = (selection) => { selectedRows.value = selection } // æ°å»ºåæº const handleAdd = async () => { operationType.value = 'add' resetForm() // å è½½ç¨æ·å表å客æ·å表 await loadUserList() await loadCustomerList() getProvinceList().then(res => { provinceOptions.value = res.data }) dialogFormVisible.value = true } const getCityListChange = (id) => { getCityList({provinceId: id}).then(res => { cityOptions.value = res.data }) } // æ·»å æä½ const handleAddOperation = async (row) => { operationType.value = 'addOperation' // æ¸ ç©ºéä»¶å表 fileList.value = [] // å è½½ç¨æ·å表å客æ·å表 await loadUserList() await loadCustomerList() // 使ç¨å½åè¡æ°æ®ä½ä¸ºåºç¡ï¼ä½åªè½ä¿®æ¹ç¶æåæè®¿è®°å½ï¼ä»æ¬¾æè¿°ãæ¹é å 容çä¿çåæ¾ Object.assign(form, row, { // ä¿çåå§åæºIDï¼ç¨äºå ³èæä½è®°å½ status: row.status, description: '', // æ¸ ç©ºæè®¿è®°å½ï¼å è®¸éæ°å¡«å entryPerson: userStore.nickName, // 设置å½å ¥äººä¸ºå½åè´¦å· entryDate: dayjs().format('YYYY-MM-DD') // 设置å½å ¥æ¶é´ä¸ºå½å¤© }) dialogFormVisible.value = true } // æ¥ç详æ const handleDetail = async (row) => { operationType.value = 'detail' // å è½½ç¨æ·å表å客æ·å表 await loadUserList() await loadCustomerList() // 使ç¨updateTimeä½ä¸ºå½å ¥æ¶é´åæ¾ Object.assign(form, row, { entryDateStart: row.updateTime || row.entryDateStart }) // çææ¨¡æåæ´è®°å½ generateChangeHistory(row) dialogFormVisible.value = true } // çæåæ´è®°å½ const generateChangeHistory = (row) => { // 使ç¨businessDescriptionæ°ç»æ°æ®çæåæ´è®°å½ 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, type: index === 0 ? 'current' : 'info', action: index === 0 ? 'å½åç¶æ' : 'åå²è®°å½' }) }) } changeHistory.value = history } // ç¼è¾åæº const handleEdit = async (row) => { operationType.value = 'edit' // å è½½ç¨æ·å表å客æ·å表 await loadUserList() await loadCustomerList() // å è½½ç份å表 await getProvinceList().then(res => { provinceOptions.value = res.data }) // 妿å端è¿åçæ¯nameï¼éè¦è½¬æ¢ä¸ºid let provinceId = row.province let cityId = row.city // 妿provinceæ¯nameåç¬¦ä¸²ï¼æ¥æ¾å¯¹åºçid if (row.province && typeof row.province === 'string' && !/^\d+$/.test(row.province)) { const provinceOption = provinceOptions.value.find(item => item.name === row.province) if (provinceOption) { provinceId = provinceOption.id // å 载对åºçåå¸å表 await getCityList({ provinceId: provinceId }).then(res => { cityOptions.value = res.data // 妿cityæ¯nameåç¬¦ä¸²ï¼æ¥æ¾å¯¹åºçid if (row.city && typeof row.city === 'string' && !/^\d+$/.test(row.city)) { const cityOption = cityOptions.value.find(item => item.name === row.city) if (cityOption) { cityId = cityOption.id } } }) } } else if (row.province) { // 妿provinceæ¯idï¼ç´æ¥å è½½åå¸å表 await getCityList({ id: row.province }).then(res => { cityOptions.value = res.data }) } // 使ç¨å½åè´¦å·åå½å¤©æ¥æä½ä¸ºé»è®¤å¼ Object.assign(form, row, { province: provinceId, // 使ç¨è½¬æ¢åçid city: cityId, // 使ç¨è½¬æ¢åçid entryPerson: userStore.nickName, // 设置å½å ¥äººä¸ºå½åè´¦å· entryDate: dayjs().format('YYYY-MM-DD') // 设置å½å ¥æ¶é´ä¸ºå½å¤© }) dialogFormVisible.value = true } // å½å ¥äººååå¤ç const changs = (value) => { // å¯ä»¥æ ¹æ®éè¦æ·»å å¤çé»è¾ } // æäº¤è¡¨å const submitForm = () => { formRef.value.validate(valid => { if (valid) { // æ¶ééä»¶æä»¶å表 const businessCommonFiles = fileList.value || [] // å°ç份åå¸çid转æ¢ä¸ºname const provinceName = form.province ? provinceOptions.value.find(item => item.id === form.province)?.name || form.province : '' const cityName = form.city ? cityOptions.value.find(item => item.id === form.city)?.name || form.city : '' let api let successMessage let submitData if (operationType.value === 'add') { api = addOpportunity successMessage = 'æ°å»ºæå' submitData = { ...form, province: provinceName, // ä¼ nameè䏿¯id city: cityName, // ä¼ nameè䏿¯id businessCommonFiles: businessCommonFiles, type: 9 // åæºç®¡ççç±»åæ è¯ } } else if (operationType.value === 'addOperation') { api = addDescription successMessage = 'æ·»å æä½æå' // æ·»å æä½æ¶ä¼ éç¶æãæè¿°ãå½å ¥äººãå½å ¥æ¥æãéä»¶ååæºID submitData = { status: form.status, description: form.description, paymentDescription: form.paymentDescription, entryPerson: form.entryPerson, entryDate: form.entryDate, businessCommonFiles: businessCommonFiles, type: 9, // åæºç®¡ççç±»åæ è¯ businessOpportunityId: form.id // ä¼ éåæºID } } else { api = updateOpportunity successMessage = 'ä¿®æ¹æå' submitData = { ...form, province: provinceName, // ä¼ nameè䏿¯id city: cityName, // ä¼ nameè䏿¯id businessCommonFiles: businessCommonFiles, type: 9 // åæºç®¡ççç±»åæ è¯ } } api(submitData).then(res => { if (res.code === 200) { proxy.$modal.msgSuccess(successMessage) closeDialog() getList() } else { proxy.$modal.msgError(res.msg || 'æä½å¤±è´¥') } }).catch(err => { console.log(err); }) } }) } // å é¤åæº const handleDelete = () => { if (selectedRows.value.length === 0) { proxy.$modal.msgWarning('è¯·éæ©è¦å é¤çåæº') return } ElMessageBox.confirm('ç¡®å®å é¤éä¸çåæºåï¼', 'æç¤º', { confirmButtonText: 'ç¡®å®', cancelButtonText: 'åæ¶', type: 'warning' }).then(() => { const ids = selectedRows.value.map(item => item.id) delOpportunity(ids).then(res => { if (res.code === 200) { proxy.$modal.msgSuccess('å 餿å') getList() } else { proxy.$modal.msgError(res.msg || 'å é¤å¤±è´¥') } }).catch(err => { proxy.$modal.msgError('å é¤å¤±è´¥') }) }).catch(() => { // ç¨æ·åæ¶å é¤ }) } // é置表å const resetForm = () => { Object.assign(form, { id: undefined, status: '', province: '', city: '', customerName: '', industry: '', informationState: '', mainBusinessRevenue: '', customerScale: '', mainProducts: '', businessSource: '', contractAmount: '', description: '', renContent: '', paymentDescription: '', entryPerson: userStore.nickName, entryDate: dayjs().format('YYYY-MM-DD') }) if (formRef.value) { formRef.value.clearValidate() } } // å ³éå¯¹è¯æ¡ const closeDialog = () => { dialogFormVisible.value = false resetForm() } // æ¥çéä»¶ function handleAttachment(row) { currentAttachmentRow.value = row fileListRef.value.open(row.businessCommonFiles, row.id) } // ä¸è½½éä»¶ï¼è¯¦æ 页é¢ï¼ function downloadAttachment(row) { proxy.$download.name(row.url) } // é¢è§éä»¶ï¼è¯¦æ 页é¢ï¼ function previewAttachment(row) { if (filePreviewRef.value) { filePreviewRef.value.open(row.previewURL) } else { // å¦ææ²¡æé¢è§ç»ä»¶ï¼ç´æ¥æå¼é¾æ¥ window.open(row.url, '_blank') } } // éä»¶åè¡¨å·æ° function handleFileListRefresh(rowId) { // éæ°è·ååè¡¨æ°æ® getList() // çå¾ åè¡¨æ°æ®æ´æ°åï¼æ¾å°å¯¹åºçè¡å¹¶æ´æ°éä»¶å表 setTimeout(() => { if (currentAttachmentRow.value && tableData.value) { const updatedRow = tableData.value.find(item => item.id === currentAttachmentRow.value.id) if (updatedRow && updatedRow.businessCommonFiles) { currentAttachmentRow.value = updatedRow fileListRef.value.open(updatedRow.businessCommonFiles, updatedRow.id) } } }, 300) } onMounted(async () => { // å è½½ç¨æ·åè¡¨ä¾æç´¢ä½¿ç¨ await loadUserList() getList() }) </script> <style scoped lang="scss"> .app-container { padding: 20px; } .search_form { display: flex; align-items: flex-start; justify-content: space-between; } .table_list { margin-top: unset; } .dialog-footer { text-align: right; } :deep(.el-form-item__label) { font-weight: 500; } :deep(.el-table) { .el-table__header-wrapper { th { background-color: #f0f2f5; color: #333; font-weight: 600; } } } /* åæ´è®°å½æ¶é´çº¿æ ·å¼ */ .change-history-section { margin-top: 20px; .el-divider { margin: 20px 0; } .timeline-card { margin: 8px 0; border-radius: 8px; .card-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; .action-type { font-weight: 600; color: #333; } .operator { font-size: 12px; color: #666; } } .change-content { .status-change, .description-change { margin-bottom: 8px; .label { font-weight: 500; color: #666; margin-right: 8px; } .description-text { color: #333; line-height: 1.5; } } } } /* æ¶é´çº¿æ ·å¼ä¼å */ :deep(.el-timeline) { padding-left: 0; .el-timeline-item { .el-timeline-item__node { background-color: #409eff; &.el-timeline-item__node--primary { background-color: #409eff; } &.el-timeline-item__node--success { background-color: #67c23a; } &.el-timeline-item__node--info { background-color: #909399; } &.el-timeline-item__node--hollow { background-color: transparent; border-color: #409eff; } } .el-timeline-item__timestamp { color: #666; font-size: 12px; } } } } </style>