提交
This commit is contained in:
1 parent
dacfdc9ff6
commit
f9676f8759
906 files changed
+533306
-12946
No files matched your search
@@ -34,46 +34,47 @@
|
||||
</el-form>
|
||||
|
||||
<!-- 联系人列表 -->
|
||||
<el-table
|
||||
v-if="customerId"
|
||||
v-loading="loading"
|
||||
:data="contactList"
|
||||
@current-change="handleCurrentChange"
|
||||
:max-height="400"
|
||||
border
|
||||
highlight-current-row
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column label="联系人姓名" prop="contactName" min-width="150" :show-overflow-tooltip="true">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<span style="margin-right: 5px">{{ scope.row.contactName }}</span>
|
||||
<el-tag v-if="scope.row.isPrimary === 1" type="success" size="small" style="margin-right: 5px">主要</el-tag>
|
||||
<el-tag v-if="scope.row.isKey === 1" type="warning" size="small">决策人</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="性别" prop="gender" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<DictTag dict-type="crm_contact_gender" :value="scope.row.gender" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="手机号码" prop="mobile" width="130" />
|
||||
<el-table-column label="邮箱" prop="email" width="180" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="部门" prop="department" width="120" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="职位" prop="position" width="120" :show-overflow-tooltip="true" />
|
||||
</el-table>
|
||||
<div v-if="customerId" class="panel dialog-list-panel">
|
||||
<div class="list-body list-wrap dialog-list-body-only">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="contactList"
|
||||
class="list-table"
|
||||
stripe
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
:max-height="400"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="60" align="center" header-align="center" />
|
||||
<el-table-column label="联系人姓名" prop="contactName" min-width="150" align="center" header-align="center" :show-overflow-tooltip="true">
|
||||
<template #default="scope">
|
||||
<div class="contact-name-cell">
|
||||
<span>{{ scope.row.contactName }}</span>
|
||||
<el-tag v-if="scope.row.isPrimary === 1" type="success" size="small">主要</el-tag>
|
||||
<el-tag v-if="scope.row.isKey === 1" type="warning" size="small">决策人</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="性别" prop="gender" min-width="80" align="center" header-align="center">
|
||||
<template #default="scope">
|
||||
<DictTag dict-type="crm_contact_gender" :value="scope.row.gender" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="手机号码" prop="mobile" min-width="130" align="center" header-align="center" />
|
||||
<el-table-column label="邮箱" prop="email" min-width="180" align="center" header-align="center" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="部门" prop="department" min-width="120" align="center" header-align="center" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="职位" prop="position" min-width="120" align="center" header-align="center" :show-overflow-tooltip="true" />
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<ListPagination
|
||||
v-if="customerId"
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
class="mt-3"
|
||||
/>
|
||||
<ListPagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前选中联系人 -->
|
||||
<template v-if="currentContact">
|
||||
@@ -132,6 +133,7 @@ watch(
|
||||
dialogVisible.value = val
|
||||
if (val && props.customerId) {
|
||||
await dictStore.loadAllDict()
|
||||
resetQueryParams()
|
||||
getList()
|
||||
}
|
||||
}
|
||||
@@ -166,28 +168,34 @@ const total = ref(0)
|
||||
const currentContact = ref<ContactVO | null>(null)
|
||||
const queryFormRef = ref<ElFormInstance>()
|
||||
|
||||
const resetQueryParams = () => {
|
||||
queryParams.value.pageNum = 1
|
||||
queryParams.value.contactName = undefined
|
||||
queryParams.value.mobile = undefined
|
||||
queryParams.value.position = undefined
|
||||
queryFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const getList = async () => {
|
||||
if (!props.customerId) {
|
||||
contactList.value = []
|
||||
total.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
queryParams.value.customerId = props.customerId
|
||||
const keyword = [queryParams.value.contactName, queryParams.value.mobile, queryParams.value.position]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.trim()
|
||||
const res = await fetchContactList({
|
||||
page: queryParams.value.pageNum,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
customerId: String(props.customerId),
|
||||
keyword: keyword || undefined
|
||||
contactName: queryParams.value.contactName?.trim() || undefined,
|
||||
mobile: queryParams.value.mobile?.trim() || undefined,
|
||||
position: queryParams.value.position?.trim() || undefined
|
||||
})
|
||||
const data = (res as any)?.list ?? (res as any)?.rows ?? (res as any)?.data?.list ?? []
|
||||
contactList.value = data
|
||||
total.value = (res as any)?.total ?? (res as any)?.data?.total ?? data.length
|
||||
contactList.value = res.list || res.rows || []
|
||||
total.value = res.total ?? 0
|
||||
} catch (error) {
|
||||
console.error('获取联系人列表失败:', error)
|
||||
} finally {
|
||||
@@ -201,10 +209,24 @@ const handleQuery = () => {
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields()
|
||||
handleQuery()
|
||||
resetQueryParams()
|
||||
getList()
|
||||
}
|
||||
|
||||
watchDebounced(
|
||||
() => ({
|
||||
contactName: queryParams.value.contactName,
|
||||
mobile: queryParams.value.mobile,
|
||||
position: queryParams.value.position
|
||||
}),
|
||||
() => {
|
||||
if (!props.customerId) return
|
||||
queryParams.value.pageNum = 1
|
||||
getList()
|
||||
},
|
||||
{ debounce: 300, deep: true }
|
||||
)
|
||||
|
||||
const handleCurrentChange = (row: ContactVO) => {
|
||||
currentContact.value = row
|
||||
}
|
||||
@@ -243,4 +265,12 @@ const handleClose = () => {
|
||||
.form-buttons-right {
|
||||
margin-left: auto !important;
|
||||
}
|
||||
|
||||
.contact-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -36,43 +36,53 @@
|
||||
</el-form>
|
||||
|
||||
<!-- 客户列表 -->
|
||||
<el-table v-loading="loading" :data="customerList" @current-change="handleCurrentChange" :max-height="400" border highlight-current-row>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column label="客户名称" prop="customerName" min-width="230" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="客户类型" prop="customerType" width="150" align="center">
|
||||
<template #default="scope">
|
||||
<DictTag dict-type="crm_customer_type" :value="scope.row.customerType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="所属行业" prop="industry" width="120" :show-overflow-tooltip="true">
|
||||
<template #default="scope">
|
||||
<DictTag dict-type="crm_customer_industry" :value="scope.row.industry" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="客户级别" prop="level" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<DictTag dict-type="crm_customer_level" :value="scope.row.level" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="联系人" prop="contactName" width="100" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="手机号码" prop="mobile" width="120" />
|
||||
<el-table-column label="地区" prop="province" width="120">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.province }} {{ scope.row.city }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" prop="ownerUserName" width="100" :show-overflow-tooltip="true" />
|
||||
</el-table>
|
||||
<div class="panel dialog-list-panel">
|
||||
<div class="list-body list-wrap dialog-list-body-only">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="customerList"
|
||||
class="list-table"
|
||||
stripe
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
:max-height="400"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="60" align="center" header-align="center" />
|
||||
<el-table-column label="客户名称" prop="customerName" min-width="230" align="center" header-align="center" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="客户类型" prop="customerType" min-width="150" align="center" header-align="center">
|
||||
<template #default="scope">
|
||||
<DictTag dict-type="crm_customer_type" :value="scope.row.customerType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="所属行业" prop="industry" min-width="120" align="center" header-align="center" :show-overflow-tooltip="true">
|
||||
<template #default="scope">
|
||||
<DictTag dict-type="crm_customer_industry" :value="scope.row.industry" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="客户级别" prop="level" min-width="100" align="center" header-align="center">
|
||||
<template #default="scope">
|
||||
<DictTag dict-type="crm_customer_level" :value="scope.row.level" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="联系人" prop="contactName" min-width="100" align="center" header-align="center" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="手机号码" prop="mobile" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column label="地区" prop="province" min-width="120" align="center" header-align="center">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.province }} {{ scope.row.city }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" prop="ownerUserName" min-width="100" align="center" header-align="center" :show-overflow-tooltip="true" />
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<ListPagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
class="mt-3"
|
||||
/>
|
||||
<ListPagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前选中客户 -->
|
||||
<template v-if="currentCustomer">
|
||||
@@ -188,6 +198,20 @@ const resetQuery = () => {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
watchDebounced(
|
||||
() => ({
|
||||
customerName: queryParams.value.customerName,
|
||||
customerNo: queryParams.value.customerNo,
|
||||
customerType: queryParams.value.customerType,
|
||||
mobile: queryParams.value.mobile
|
||||
}),
|
||||
() => {
|
||||
queryParams.value.pageNum = 1
|
||||
getList()
|
||||
},
|
||||
{ debounce: 300, deep: true }
|
||||
)
|
||||
|
||||
const handleCurrentChange = (row: any) => {
|
||||
currentCustomer.value = row
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import { computed, ref, watch, onBeforeUnmount } from 'vue'
|
||||
import { PictureFilled } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/http'
|
||||
import { resolveCrmAttachmentUrl } from '@/utils/crm-attachment'
|
||||
|
||||
const props = defineProps({
|
||||
src: {
|
||||
@@ -166,8 +167,15 @@ watch(
|
||||
}
|
||||
|
||||
if (props.isOssId) {
|
||||
const ossIds = newSrc.split(',').filter((id) => id.trim())
|
||||
const urls = await getUrlsByOssIds(ossIds)
|
||||
const ossKeys = newSrc.split(',').filter((id) => id.trim())
|
||||
// 门户 CRM:MinIO 相对路径(含 /),直接拼公开前缀展示
|
||||
if (ossKeys.some((key) => key.includes('/'))) {
|
||||
const urls = ossKeys.map((key) => resolveCrmAttachmentUrl(key)).filter(Boolean)
|
||||
realSrc.value = urls[0] || ''
|
||||
realSrcList.value = urls
|
||||
return
|
||||
}
|
||||
const urls = await getUrlsByOssIds(ossKeys)
|
||||
realSrc.value = urls[0] || ''
|
||||
realSrcList.value = urls
|
||||
return
|
||||
|
||||
@@ -41,43 +41,53 @@
|
||||
</el-form>
|
||||
|
||||
<!-- 商机列表 -->
|
||||
<el-table v-loading="loading" :data="opportunityList" @current-change="handleCurrentChange" :max-height="400" border highlight-current-row>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column label="商机编号" prop="opportunityNo" width="140" align="center" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="商机名称" prop="opportunityName" min-width="180" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="客户名称" prop="customerName" width="150" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="商机阶段" prop="stage" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<DictTag dict-type="crm_opportunity_stage" :value="scope.row.stage" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预计成交率" prop="winRate" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-progress :percentage="scope.row.winRate || 0" :color="getWinRateColor(scope.row.winRate)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预计成交日期" prop="expectedCloseDate" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.expectedCloseDate || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" prop="ownerUserName" width="100" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="状态" prop="status" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<DictTag dict-type="crm_opportunity_status" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="panel dialog-list-panel">
|
||||
<div class="list-body list-wrap dialog-list-body-only">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="opportunityList"
|
||||
class="list-table"
|
||||
stripe
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
:max-height="400"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="60" align="center" header-align="center" />
|
||||
<el-table-column label="商机编号" prop="opportunityNo" min-width="140" align="center" header-align="center" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="商机名称" prop="opportunityName" min-width="180" align="center" header-align="center" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="客户名称" prop="customerName" min-width="150" align="center" header-align="center" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="商机阶段" prop="stage" min-width="120" align="center" header-align="center">
|
||||
<template #default="scope">
|
||||
<DictTag dict-type="crm_opportunity_stage" :value="scope.row.stage" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预计成交率" prop="winRate" min-width="100" align="center" header-align="center">
|
||||
<template #default="scope">
|
||||
<el-progress :percentage="scope.row.winRate || 0" :color="getWinRateColor(scope.row.winRate)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预计成交日期" prop="expectedCloseDate" min-width="120" align="center" header-align="center">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.expectedCloseDate || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" prop="ownerUserName" min-width="100" align="center" header-align="center" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="状态" prop="status" min-width="80" align="center" header-align="center">
|
||||
<template #default="scope">
|
||||
<DictTag dict-type="crm_opportunity_status" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<ListPagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
class="mt-3"
|
||||
/>
|
||||
<ListPagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前选中商机 -->
|
||||
<template v-if="currentOpportunity">
|
||||
@@ -202,6 +212,20 @@ const resetQuery = () => {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
watchDebounced(
|
||||
() => ({
|
||||
opportunityName: queryParams.value.opportunityName,
|
||||
opportunityNo: queryParams.value.opportunityNo,
|
||||
stage: queryParams.value.stage,
|
||||
status: queryParams.value.status
|
||||
}),
|
||||
() => {
|
||||
queryParams.value.pageNum = 1
|
||||
getList()
|
||||
},
|
||||
{ debounce: 300, deep: true }
|
||||
)
|
||||
|
||||
const handleCurrentChange = (row: OpportunityVO) => {
|
||||
currentOpportunity.value = row
|
||||
}
|
||||
|
||||
@@ -39,40 +39,54 @@
|
||||
</el-form>
|
||||
|
||||
<!-- 报价单列表 -->
|
||||
<el-table v-loading="loading" :data="quoteList" @current-change="handleCurrentChange" :max-height="400" border highlight-current-row>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column label="报价单编号" prop="quoteNo" width="140" align="center" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="状态" prop="status" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<DictTag dict-type="crm_quote_status" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报价主题" prop="quoteTitle" min-width="180" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="客户名称" prop="customerName" width="150" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="合同关联情况" prop="contractLinkedCount" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.contractLinkedCount != null ? `已关联${scope.row.contractLinkedCount}个合同` : '未关联' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报价金额" prop="finalAmount" width="120" align="right">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.finalAmount != null ? Number(scope.row.finalAmount).toLocaleString() : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报价日期" prop="quoteDate" width="120" align="center" />
|
||||
<el-table-column label="有效期" prop="expireDate" width="120" align="center" />
|
||||
<el-table-column label="负责人" prop="ownerUserName" width="100" :show-overflow-tooltip="true" />
|
||||
</el-table>
|
||||
<div class="panel dialog-list-panel">
|
||||
<div class="list-body list-wrap dialog-list-body-only">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="quoteList"
|
||||
class="list-table"
|
||||
stripe
|
||||
highlight-current-row
|
||||
@current-change="handleCurrentChange"
|
||||
:max-height="400"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="60" align="center" header-align="center" />
|
||||
<el-table-column label="报价单编号" prop="quoteNo" min-width="140" align="center" header-align="center" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="状态" prop="status" min-width="100" align="center" header-align="center">
|
||||
<template #default="scope">
|
||||
<DictTag dict-type="crm_quote_status" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报价主题" prop="quoteTitle" min-width="180" align="center" header-align="center" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="客户名称" prop="customerName" min-width="150" align="center" header-align="center" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="合同关联情况" prop="contractLinkedCount" min-width="120" align="center" header-align="center">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.contractLinkedCount != null ? `已关联${scope.row.contractLinkedCount}个合同` : '未关联' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报价金额(元)" prop="finalAmount" min-width="130" align="right" header-align="center">
|
||||
<template #default="scope">
|
||||
<span class="money-display">{{
|
||||
scope.row.finalAmount != null
|
||||
? `¥${Number(scope.row.finalAmount).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
||||
: '-'
|
||||
}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报价日期" prop="quoteDate" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column label="有效期" prop="expireDate" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column label="负责人" prop="ownerUserName" min-width="100" align="center" header-align="center" :show-overflow-tooltip="true" />
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<ListPagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
class="mt-3"
|
||||
/>
|
||||
<ListPagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前选中报价单 -->
|
||||
<template v-if="currentQuote">
|
||||
@@ -82,7 +96,13 @@
|
||||
<el-descriptions-item label="报价标题">{{ currentQuote.quoteTitle }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报价编号">{{ currentQuote.quoteNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户名称">{{ currentQuote.customerName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报价金额">{{ currentQuote.finalAmount != null ? Number(currentQuote.finalAmount).toLocaleString() : '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报价金额(元)">
|
||||
<span class="money-display">{{
|
||||
currentQuote.finalAmount != null
|
||||
? `¥${Number(currentQuote.finalAmount).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
||||
: '-'
|
||||
}}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="报价日期">{{ currentQuote.quoteDate || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="有效期至">{{ currentQuote.expireDate || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
@@ -207,6 +227,20 @@ const resetQuery = () => {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
watchDebounced(
|
||||
() => ({
|
||||
quoteNo: queryParams.value.quoteNo,
|
||||
quoteTitle: queryParams.value.quoteTitle,
|
||||
customerName: queryParams.value.customerName,
|
||||
status: queryParams.value.status
|
||||
}),
|
||||
() => {
|
||||
queryParams.value.pageNum = 1
|
||||
getList()
|
||||
},
|
||||
{ debounce: 300, deep: true }
|
||||
)
|
||||
|
||||
const handleCurrentChange = async (row: QuoteVO) => {
|
||||
if (!row) {
|
||||
currentQuote.value = null
|
||||
|
||||
@@ -7,9 +7,42 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed, onMounted } from 'vue';
|
||||
import Editor from '@tinymce/tinymce-vue';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { fetchUploadAttachment } from '@/api/admin';
|
||||
import { resolveCrmAttachmentUrl } from '@/utils/crm-attachment';
|
||||
import { buildAttachmentUploadFormData, CRM_UPLOAD_BIZ_TYPE } from '@/utils/crm-upload';
|
||||
|
||||
const userStore = useUserStore();
|
||||
/** 须高于 ElConfigProvider 初始 z-index(3000) 及多层 el-dialog 叠加 */
|
||||
const TINYMCE_Z_INDEX_FLOOR = 10000;
|
||||
|
||||
/** 读取当前页面 Element Plus 弹层最高 z-index,保证 TinyMCE 浮层在其上 */
|
||||
const resolveTinyMceBaseZIndex = (): number => {
|
||||
let maxZ = 3000;
|
||||
document.querySelectorAll('.el-overlay, .el-dialog, .el-popper').forEach((el) => {
|
||||
const z = Number.parseInt(window.getComputedStyle(el).zIndex || '0', 10);
|
||||
if (!Number.isNaN(z) && z > maxZ) maxZ = z;
|
||||
});
|
||||
return Math.max(maxZ + 200, TINYMCE_Z_INDEX_FLOOR);
|
||||
};
|
||||
|
||||
const syncTinyMceFloatingZIndex = (baseZ: number) => {
|
||||
const z = String(baseZ);
|
||||
const backdropZ = String(baseZ - 1);
|
||||
document.querySelectorAll('.tox-tinymce-aux').forEach((el) => {
|
||||
(el as HTMLElement).style.zIndex = z;
|
||||
});
|
||||
document.querySelectorAll('.tox-dialog-wrap').forEach((el) => {
|
||||
(el as HTMLElement).style.zIndex = z;
|
||||
});
|
||||
document.querySelectorAll('.tox-dialog').forEach((el) => {
|
||||
(el as HTMLElement).style.zIndex = String(baseZ + 1);
|
||||
});
|
||||
document.querySelectorAll('.tox-dialog__backdrop').forEach((el) => {
|
||||
(el as HTMLElement).style.zIndex = backdropZ;
|
||||
});
|
||||
document.querySelectorAll('.tox-menu, .tox-pop, .tox-toolbar__overflow, .tox-collection').forEach((el) => {
|
||||
(el as HTMLElement).style.zIndex = String(baseZ + 2);
|
||||
});
|
||||
};
|
||||
|
||||
interface Props {
|
||||
modelValue?: string;
|
||||
@@ -31,12 +64,6 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
// 获取 Token
|
||||
const getToken = () => userStore.accessToken || '';
|
||||
|
||||
// 获取上传 URL
|
||||
const getUploadUrl = () => '/web/admin/attachment/upload';
|
||||
|
||||
// 生成唯一 ID
|
||||
const editorId = `tinymce-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
@@ -82,7 +109,7 @@ const isSXSupplier = computed(() => {
|
||||
|
||||
// TinyMCE 配置
|
||||
const editorInit = computed(() => {
|
||||
// 根据供方编码决定是否显示背景图片
|
||||
const baseZIndex = resolveTinyMceBaseZIndex();
|
||||
const backgroundStyle = isSXSupplier.value
|
||||
? `
|
||||
/* 水印背景层 - 使用伪元素覆盖整个编辑器区域 */
|
||||
@@ -248,66 +275,36 @@ const editorInit = computed(() => {
|
||||
branding: false,
|
||||
promotion: false,
|
||||
|
||||
// 设置弹窗的 z-index,确保在 Element Plus Dialog 之上
|
||||
// Element Plus Dialog 默认 z-index 是 2000+,所以设置为 3000
|
||||
base_z_index: 3000,
|
||||
// 设置弹窗的 z-index,确保在 Element Plus Dialog 之上(须高于 App.vue ElConfigProvider :z-index="3000")
|
||||
base_z_index: baseZIndex,
|
||||
|
||||
// 图片上传配置
|
||||
// 图片上传配置(复用门户统一附件接口,与 CRM 其它上传一致)
|
||||
images_upload_handler: (blobInfo: any, progress: any) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', blobInfo.blob(), blobInfo.filename());
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', getUploadUrl());
|
||||
|
||||
// 设置请求头
|
||||
const token = getToken();
|
||||
const clientId = getClientId();
|
||||
|
||||
if (token) {
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
|
||||
}
|
||||
if (clientId) {
|
||||
xhr.setRequestHeader('clientid', clientId);
|
||||
}
|
||||
|
||||
// 上传进度
|
||||
xhr.upload.onprogress = (e) => {
|
||||
progress((e.loaded / e.total) * 100);
|
||||
};
|
||||
|
||||
// 上传成功
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
const json = JSON.parse(xhr.responseText);
|
||||
if (json.code === 200) {
|
||||
// 返回图片URL
|
||||
resolve(json.data.url);
|
||||
console.log('图片上传成功:', json.data.url);
|
||||
} else {
|
||||
const errorMsg = json.msg || '上传失败';
|
||||
reject(errorMsg);
|
||||
console.error('图片上传失败:', errorMsg);
|
||||
}
|
||||
} catch (e) {
|
||||
reject('响应解析失败');
|
||||
console.error('图片上传失败:响应解析错误');
|
||||
}
|
||||
} else {
|
||||
reject('HTTP错误:' + xhr.status);
|
||||
console.error('图片上传失败:HTTP ' + xhr.status);
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
progress(10);
|
||||
const file = new File([blobInfo.blob()], blobInfo.filename(), {
|
||||
type: blobInfo.blob().type || 'image/jpeg'
|
||||
});
|
||||
const formData = buildAttachmentUploadFormData(file, {
|
||||
bizType: CRM_UPLOAD_BIZ_TYPE.QUOTE_ATTACHMENT
|
||||
});
|
||||
progress(40);
|
||||
const res = await fetchUploadAttachment(formData);
|
||||
const fileUrl = res?.fileUrl;
|
||||
if (!fileUrl) {
|
||||
reject('上传失败');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// 上传失败
|
||||
xhr.onerror = () => {
|
||||
reject('网络错误');
|
||||
console.error('图片上传失败:网络错误');
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
progress(100);
|
||||
const imageUrl = resolveCrmAttachmentUrl(fileUrl);
|
||||
resolve(imageUrl);
|
||||
console.log('图片上传成功:', imageUrl);
|
||||
} catch (error: any) {
|
||||
const errorMsg = error?.message || '上传失败';
|
||||
reject(errorMsg);
|
||||
console.error('图片上传失败:', errorMsg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -334,6 +331,20 @@ const editorInit = computed(() => {
|
||||
|
||||
// 初始化回调
|
||||
setup: (editor: any) => {
|
||||
const refreshFloatingLayers = () => {
|
||||
const z = resolveTinyMceBaseZIndex();
|
||||
syncTinyMceFloatingZIndex(z);
|
||||
};
|
||||
|
||||
editor.on('init', () => {
|
||||
refreshFloatingLayers();
|
||||
});
|
||||
|
||||
editor.on('OpenWindow', refreshFloatingLayers);
|
||||
editor.on('CloseWindow', refreshFloatingLayers);
|
||||
editor.on('ShowMenu', refreshFloatingLayers);
|
||||
editor.on('focus', refreshFloatingLayers);
|
||||
|
||||
// 编辑器初始化完成
|
||||
// 注意:水印已在 content_style 的 body::before 伪元素中设置,无需重复添加
|
||||
editor.on('init', () => {
|
||||
@@ -544,20 +555,29 @@ onMounted(() => {
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* TinyMCE 全局弹窗样式 - 不使用 scoped,确保对所有 TinyMCE 弹窗生效 */
|
||||
/* TinyMCE 全局浮层:高于 ElConfigProvider z-index(3000) 及嵌套 el-dialog */
|
||||
.tox-tinymce-aux {
|
||||
z-index: 3000 !important;
|
||||
z-index: 10000 !important;
|
||||
}
|
||||
|
||||
.tox-dialog-wrap {
|
||||
z-index: 3000 !important;
|
||||
z-index: 10000 !important;
|
||||
}
|
||||
|
||||
.tox-dialog {
|
||||
z-index: 3001 !important;
|
||||
z-index: 10001 !important;
|
||||
}
|
||||
|
||||
.tox-dialog__backdrop {
|
||||
z-index: 2999 !important;
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
.tox-menu,
|
||||
.tox-pop,
|
||||
.tox-toolbar__overflow,
|
||||
.tox-collection,
|
||||
.tox-swatches,
|
||||
.tox-selector {
|
||||
z-index: 10002 !important;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user