feat(wecom): 集成企业微信JSSDK并重构客户信息获取逻辑
refactor(admin): 优化文件上传处理及表单字段管理 fix(user): 修复企微SDK初始化及客户ID获取问题
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vicons/ionicons5": "^0.13.0",
|
"@vicons/ionicons5": "^0.13.0",
|
||||||
|
"@wecom/jssdk": "^2.3.4",
|
||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
"dev": "^0.1.3",
|
"dev": "^0.1.3",
|
||||||
"naive-ui": "^2.44.1",
|
"naive-ui": "^2.44.1",
|
||||||
|
|||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
|||||||
'@vicons/ionicons5':
|
'@vicons/ionicons5':
|
||||||
specifier: ^0.13.0
|
specifier: ^0.13.0
|
||||||
version: 0.13.0
|
version: 0.13.0
|
||||||
|
'@wecom/jssdk':
|
||||||
|
specifier: ^2.3.4
|
||||||
|
version: 2.3.4
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.13.6
|
specifier: ^1.13.6
|
||||||
version: 1.13.6
|
version: 1.13.6
|
||||||
@@ -650,6 +653,9 @@ packages:
|
|||||||
vue:
|
vue:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@wecom/jssdk@2.3.4':
|
||||||
|
resolution: {integrity: sha512-oLfuvwMBZCRRMowVi/JkKx/dLNGHCmmUbQfCYG7XGmHYbgkQJlAlack4jKBk+NVG4G0S24fIrAF+XwQuXXxgAw==}
|
||||||
|
|
||||||
acorn@8.16.0:
|
acorn@8.16.0:
|
||||||
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
@@ -1890,6 +1896,8 @@ snapshots:
|
|||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
vue: 3.5.30(typescript@5.9.3)
|
vue: 3.5.30(typescript@5.9.3)
|
||||||
|
|
||||||
|
'@wecom/jssdk@2.3.4': {}
|
||||||
|
|
||||||
acorn@8.16.0: {}
|
acorn@8.16.0: {}
|
||||||
|
|
||||||
alien-signals@3.1.2: {}
|
alien-signals@3.1.2: {}
|
||||||
|
|||||||
@@ -101,9 +101,9 @@
|
|||||||
</n-card>
|
</n-card>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- 详情抽屉 - 视觉重构 -->
|
<!-- 详情抽屉 -->
|
||||||
<n-drawer v-model:show="showDrawer" :width="650" placement="right" resizable>
|
<n-drawer v-model:show="showDrawer" :width="650" placement="right" resizable>
|
||||||
<n-drawer-content title="原始提报资料详情" closable class="modern-drawer">
|
<n-drawer-content title="原始提报资料详情与分配" closable class="modern-drawer">
|
||||||
<div class="drawer-body" v-if="showDrawer && editingRow.wecom_id">
|
<div class="drawer-body" v-if="showDrawer && editingRow.wecom_id">
|
||||||
<n-space vertical :size="24">
|
<n-space vertical :size="24">
|
||||||
<!-- 分配任务卡片 -->
|
<!-- 分配任务卡片 -->
|
||||||
@@ -136,14 +136,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<n-form label-placement="left" label-width="90" size="small" :model="editingRow">
|
<n-form label-placement="left" label-width="90" size="small" :model="editingRow">
|
||||||
<n-grid :x-gap="20" :cols="2">
|
<n-grid :x-gap="20" :cols="2">
|
||||||
<n-grid-item v-for="field in [
|
<!-- 修复了之前的 v-for 报错,将数组抽离为了 baseInfoFields -->
|
||||||
{label:'主管', key:'analyst_supervisor'},
|
<n-grid-item v-for="field in baseInfoFields" :key="field.key">
|
||||||
{label:'部门', key:'analyst_department'},
|
|
||||||
{label:'分析师', key:'analyst_name'},
|
|
||||||
{label:'家长姓名', key:'parent_name'},
|
|
||||||
{label:'家长电话', key:'parent_phone'},
|
|
||||||
{label:'身份证', key:'parent_id_card'},
|
|
||||||
]" :key="field.key">
|
|
||||||
<n-form-item :label="field.label">
|
<n-form-item :label="field.label">
|
||||||
<n-input v-model:value="editingRow[field.key]" />
|
<n-input v-model:value="editingRow[field.key]" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
@@ -162,27 +156,43 @@
|
|||||||
</n-form>
|
</n-form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 附件区 -->
|
<!-- 附件/素材区 -->
|
||||||
<div class="file-grid">
|
<div class="file-grid">
|
||||||
|
<!-- 1. 附件文档 -->
|
||||||
<div class="file-item">
|
<div class="file-item">
|
||||||
<div class="file-label"><n-icon><FileTrayOutline /></n-icon> 附件文档</div>
|
<div class="file-label"><n-icon><FileTrayOutline /></n-icon> 附件文档</div>
|
||||||
<n-upload
|
<n-upload
|
||||||
v-model:file-list="editingRow.attachmentFileList"
|
v-model:file-list="editingRow.attachmentFileList"
|
||||||
:max="1"
|
:max="1"
|
||||||
:custom-request="({ file, onFinish, onError, onProgress }) => handleCustomUpload({ file, onFinish, onError, onProgress }, 'attachmentFileList')"
|
:custom-request="handleCustomUpload"
|
||||||
>
|
>
|
||||||
<n-button dashed block>上传新附件</n-button>
|
<n-button dashed block>更换附件文档</n-button>
|
||||||
</n-upload>
|
</n-upload>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. 电子签名 -->
|
||||||
<div class="file-item">
|
<div class="file-item">
|
||||||
<div class="file-label"><n-icon><ImagesOutline /></n-icon> 付款截图</div>
|
<div class="file-label"><n-icon><CreateOutline /></n-icon> 电子签名</div>
|
||||||
|
<n-upload
|
||||||
|
v-model:file-list="editingRow.signatureFileList"
|
||||||
|
list-type="image-card"
|
||||||
|
:max="1"
|
||||||
|
@preview="handlePreview"
|
||||||
|
:custom-request="handleCustomUpload"
|
||||||
|
>
|
||||||
|
点击上传
|
||||||
|
</n-upload>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. 付款截图 -->
|
||||||
|
<div class="file-item payment-item">
|
||||||
|
<div class="file-label"><n-icon><ImagesOutline /></n-icon> 付款截图凭证 (多选)</div>
|
||||||
<n-upload
|
<n-upload
|
||||||
v-model:file-list="editingRow.paymentFileList"
|
v-model:file-list="editingRow.paymentFileList"
|
||||||
list-type="image-card"
|
list-type="image-card"
|
||||||
multiple
|
multiple
|
||||||
@preview="handlePreview"
|
@preview="handlePreview"
|
||||||
:custom-request="({ file, onFinish, onError, onProgress }) => handleCustomUpload({ file, onFinish, onError, onProgress }, 'paymentFileList')"
|
:custom-request="handleCustomUpload"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -208,21 +218,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
/**
|
|
||||||
* Script 部分保持原样,无需修改逻辑
|
|
||||||
* 这里省略重复的逻辑代码以减少篇幅,请直接沿用你原文件中的 script 内容
|
|
||||||
*/
|
|
||||||
import { ref, h, reactive, onMounted } from 'vue'
|
import { ref, h, reactive, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
useMessage, zhCN, dateZhCN, NConfigProvider, NCard, NDataTable, NButton, NIcon, NInput,
|
useMessage, zhCN, dateZhCN, NConfigProvider, NCard, NDataTable, NButton, NIcon, NInput,
|
||||||
NDrawer, NDrawerContent, NSpace, NForm, NFormItem, NGrid, NGridItem, NDatePicker, NSelect,
|
NDrawer, NDrawerContent, NSpace, NForm, NFormItem, NGrid, NGridItem, NDatePicker, NSelect,
|
||||||
NTag, NUpload, NModal, NText, NBadge
|
NTag, NUpload, NModal, NBadge
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SearchOutline, LogOutOutline, DownloadOutline, PersonOutline, FileTrayOutline,
|
SearchOutline, LogOutOutline, DownloadOutline, PersonOutline, FileTrayOutline,
|
||||||
ImagesOutline, BrushOutline, ReaderOutline, EyeOutline
|
ImagesOutline, BrushOutline, ReaderOutline, EyeOutline, CreateOutline
|
||||||
} from '@vicons/ionicons5'
|
} from '@vicons/ionicons5'
|
||||||
import http from '@/utils/http'
|
import http from '@/utils/http'
|
||||||
|
|
||||||
@@ -231,6 +237,16 @@ const message = useMessage()
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const submitLoading = ref(false)
|
const submitLoading = ref(false)
|
||||||
|
|
||||||
|
// 基础信息表单配置(解决模板中 v-for 太长和语法报错的问题)
|
||||||
|
const baseInfoFields =[
|
||||||
|
{ label: '主管', key: 'analyst_supervisor' },
|
||||||
|
{ label: '部门', key: 'analyst_department' },
|
||||||
|
{ label: '分析师', key: 'analyst_name' },
|
||||||
|
{ label: '家长姓名', key: 'parent_name' },
|
||||||
|
{ label: '家长电话', key: 'parent_phone' },
|
||||||
|
{ label: '身份证', key: 'parent_id_card' }
|
||||||
|
]
|
||||||
|
|
||||||
const searchParams = reactive({
|
const searchParams = reactive({
|
||||||
wecom_id: '',
|
wecom_id: '',
|
||||||
parentInfo: '',
|
parentInfo: '',
|
||||||
@@ -262,6 +278,9 @@ const pagination = reactive({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 核心逻辑:数据请求与文件对象标准化
|
||||||
|
*/
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -278,18 +297,22 @@ const fetchData = async () => {
|
|||||||
query.append('start_date', startDate)
|
query.append('start_date', startDate)
|
||||||
query.append('end_date', endDate)
|
query.append('end_date', endDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await http.get(`/v1/customer/list?${query.toString()}`)
|
const res = await http.get(`/v1/customer/list?${query.toString()}`)
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
displayData.value = res.data.map(item => {
|
displayData.value = res.data.map(item => {
|
||||||
|
// 列表回显处理:为了让 n-upload 正常显示缩略图,仍然必须组装包含 url 字段的对象
|
||||||
const p_names = Array.isArray(item.payment_object_names) ? item.payment_object_names :[]
|
const p_names = Array.isArray(item.payment_object_names) ? item.payment_object_names :[]
|
||||||
const p_urls = item.payment_image_url ||[]
|
const p_urls = item.payment_image_url ||[]
|
||||||
const paymentFileList = p_urls.map((url, i) => ({
|
const paymentFileList = p_urls.map((url, i) => ({
|
||||||
id: p_names[i] || `pay_${i}`,
|
id: p_names[i] || `pay_${i}`,
|
||||||
name: `付款截图_${i + 1}.png`,
|
name: `付款截图_${i + 1}.png`,
|
||||||
status: 'finished',
|
status: 'finished',
|
||||||
url: url,
|
url: url, // 映射到组件的 url 以展示缩略图
|
||||||
object_name: p_names[i]
|
object_name: p_names[i] // 保留真实的 object_name
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// 处理签名
|
||||||
const signatureFileList = item.signature_image_url ?[{
|
const signatureFileList = item.signature_image_url ?[{
|
||||||
id: item.signature_object_name || 'sig',
|
id: item.signature_object_name || 'sig',
|
||||||
name: '电子签名.png',
|
name: '电子签名.png',
|
||||||
@@ -297,6 +320,8 @@ const fetchData = async () => {
|
|||||||
url: item.signature_image_url,
|
url: item.signature_image_url,
|
||||||
object_name: item.signature_object_name
|
object_name: item.signature_object_name
|
||||||
}] :[]
|
}] :[]
|
||||||
|
|
||||||
|
// 处理附件文档
|
||||||
const attachmentFileList = item.attachment_file_url ?[{
|
const attachmentFileList = item.attachment_file_url ?[{
|
||||||
id: item.attachment_object_name || 'att',
|
id: item.attachment_object_name || 'att',
|
||||||
name: '原始附件文档',
|
name: '原始附件文档',
|
||||||
@@ -304,6 +329,7 @@ const fetchData = async () => {
|
|||||||
url: item.attachment_file_url,
|
url: item.attachment_file_url,
|
||||||
object_name: item.attachment_object_name
|
object_name: item.attachment_object_name
|
||||||
}] :[]
|
}] :[]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
transaction_amount: item.transaction_amount || '0',
|
transaction_amount: item.transaction_amount || '0',
|
||||||
@@ -318,54 +344,85 @@ const fetchData = async () => {
|
|||||||
pagination.itemCount = res.pagination?.total || 0
|
pagination.itemCount = res.pagination?.total || 0
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error('Fetch error:', error)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCustomUpload = async ({ file, onFinish, onError, onProgress }, type) => {
|
/**
|
||||||
|
* 核心上传逻辑:基于引用的响应式更新 (已适配最新接口格式)
|
||||||
|
*/
|
||||||
|
const handleCustomUpload = async ({ file, onFinish, onError, onProgress }) => {
|
||||||
try {
|
try {
|
||||||
const uploadData = new FormData()
|
const uploadData = new FormData()
|
||||||
uploadData.append('file', file.file)
|
uploadData.append('file', file.file)
|
||||||
|
|
||||||
const response = await http.post('/v1/material/upload', uploadData, {
|
const response = await http.post('/v1/material/upload', uploadData, {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
onUploadProgress: (p) => onProgress({ percent: Math.ceil((p.loaded / p.total) * 100) })
|
onUploadProgress: (p) => onProgress({ percent: Math.ceil((p.loaded / p.total) * 100) })
|
||||||
})
|
})
|
||||||
if (response && response.success) {
|
|
||||||
file.rawResponse = response
|
// 提取接口返回的真实数据对象
|
||||||
file.url = response.url
|
const resData = response.data
|
||||||
|
console.log(24536,resData)
|
||||||
|
// 判断是否成功获取到了关键字段 object_name
|
||||||
|
if ( resData && resData.object_name) {
|
||||||
|
|
||||||
|
// 1. 核心业务字段赋值:供后续 submitAssignment 提取上传
|
||||||
|
file.name = resData.object_name
|
||||||
|
|
||||||
|
// 2. 组件渲染依赖字段映射:Naive UI 组件强依赖 url 属性显示图片预览,所以把 preview_url 给它
|
||||||
|
file.url = resData.preview_url
|
||||||
|
|
||||||
|
// 3. 存储其余接口返回数据:备用
|
||||||
|
file.download_url = resData.download_url
|
||||||
|
file.upload_time = resData.upload_time
|
||||||
|
|
||||||
|
// 更新组件内部状态为完成
|
||||||
file.status = 'finished'
|
file.status = 'finished'
|
||||||
|
|
||||||
message.success('上传成功')
|
message.success('上传成功')
|
||||||
onFinish()
|
onFinish()
|
||||||
} else {
|
} else {
|
||||||
|
message.error(response.message || '上传失败,接口返回数据格式异常')
|
||||||
onError()
|
onError()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Upload error:', error)
|
||||||
|
message.error('网络错误,上传失败')
|
||||||
onError()
|
onError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getObjectName = (fileItem) => {
|
/**
|
||||||
if (fileItem.rawResponse) return fileItem.rawResponse.data?.object_name || fileItem.rawResponse.object_name || null
|
* 提交分配任务 (包含文件标识符提取)
|
||||||
return fileItem.object_name || null
|
*/
|
||||||
}
|
|
||||||
|
|
||||||
const submitAssignment = async (row) => {
|
const submitAssignment = async (row) => {
|
||||||
if (!row.assignee_name || !row.assignee_phone) {
|
if (!row.assignee_name || !row.assignee_phone) {
|
||||||
message.warning('请填写指导师姓名和电话')
|
message.warning('请填写指导师姓名和电话')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查上传状态
|
||||||
|
const allFiles = [...row.paymentFileList, ...row.signatureFileList, ...row.attachmentFileList]
|
||||||
|
if (allFiles.some(f => f.status === 'uploading')) {
|
||||||
|
message.warning('文件正在上传中,请稍后提交')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
row.isSubmitting = true
|
row.isSubmitting = true
|
||||||
try {
|
try {
|
||||||
const payment_object_names = row.paymentFileList.map(getObjectName).filter(Boolean)
|
// 从当前数组提取最新的 object_name
|
||||||
const signature_object_name = row.signatureFileList.length > 0 ? getObjectName(row.signatureFileList[0]) : ''
|
const payment_object_names = row.paymentFileList.map(f => f.name || f?.object_name || f.name).filter(Boolean)
|
||||||
const attachment_object_name = row.attachmentFileList.length > 0 ? getObjectName(row.attachmentFileList[0]) : ''
|
const signature_object_name = row.signatureFileList[0]?.name || row.signatureFileList[0]?.object_name || row.signatureFileList[0]?.name
|
||||||
|
const attachment_object_name = row.attachmentFileList[0]?.name || row.attachmentFileList[0]?.object_name || row.attachmentFileList[0]?.name
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
wecom_id: String(row.wecom_id),
|
wecom_id: String(row.wecom_id),
|
||||||
payment_object_names: payment_object_names,
|
payment_object_names,
|
||||||
signature_object_name: signature_object_name,
|
signature_object_name,
|
||||||
attachment_object_name: attachment_object_name,
|
attachment_object_name,
|
||||||
analyst_supervisor: row.analyst_supervisor,
|
analyst_supervisor: row.analyst_supervisor,
|
||||||
analyst_department: row.analyst_department,
|
analyst_department: row.analyst_department,
|
||||||
analyst_name: row.analyst_name,
|
analyst_name: row.analyst_name,
|
||||||
@@ -378,14 +435,18 @@ const submitAssignment = async (row) => {
|
|||||||
assignee_name: row.assignee_name,
|
assignee_name: row.assignee_name,
|
||||||
assignee_phone: row.assignee_phone
|
assignee_phone: row.assignee_phone
|
||||||
}
|
}
|
||||||
|
console.log(24531221126,row)
|
||||||
|
console.log('提交的数据:', payload)
|
||||||
|
|
||||||
const res = await http.post('/v1/material/submit', payload)
|
const res = await http.post('/v1/material/submit', payload)
|
||||||
if (res && (res.success || res.code === 200)) {
|
if (res && (res.success || res.code === 200)) {
|
||||||
message.success('分配成功')
|
message.success('分配并同步成功')
|
||||||
row.status = 'processed'
|
row.status = 'processed'
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Submit error:', error)
|
||||||
return false
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
row.isSubmitting = false
|
row.isSubmitting = false
|
||||||
@@ -397,12 +458,14 @@ const editingRow = ref({})
|
|||||||
const showPreview = ref(false)
|
const showPreview = ref(false)
|
||||||
const previewImageUrl = ref('')
|
const previewImageUrl = ref('')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开详情并执行深拷贝隔离
|
||||||
|
*/
|
||||||
const openDetails = (row) => {
|
const openDetails = (row) => {
|
||||||
const rowCopy = JSON.parse(JSON.stringify(row))
|
editingRow.value = JSON.parse(JSON.stringify(row))
|
||||||
if (!rowCopy.transaction_date) {
|
if (!editingRow.value.transaction_date) {
|
||||||
rowCopy.transaction_date = null
|
editingRow.value.transaction_date = null
|
||||||
}
|
}
|
||||||
editingRow.value = rowCopy
|
|
||||||
showDrawer.value = true
|
showDrawer.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,6 +479,9 @@ const handleDrawerSubmit = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表格列配置
|
||||||
|
*/
|
||||||
const columns =[
|
const columns =[
|
||||||
{ title: '分析师', key: 'analyst_name', width: 100, fixed: 'left' },
|
{ title: '分析师', key: 'analyst_name', width: 100, fixed: 'left' },
|
||||||
{ title: '家长姓名', key: 'parent_name', width: 100 },
|
{ title: '家长姓名', key: 'parent_name', width: 100 },
|
||||||
@@ -485,6 +551,7 @@ const columns =[
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 预览依然取 url,因为 handleCustomUpload 已将接口真实的 preview_url 映射给了 url
|
||||||
const handlePreview = (file) => {
|
const handlePreview = (file) => {
|
||||||
previewImageUrl.value = file.url || (file.file && URL.createObjectURL(file.file))
|
previewImageUrl.value = file.url || (file.file && URL.createObjectURL(file.file))
|
||||||
showPreview.value = true
|
showPreview.value = true
|
||||||
@@ -668,6 +735,7 @@ const logout = () => {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 文件网格布局 */
|
||||||
.file-grid {
|
.file-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
@@ -679,6 +747,13 @@ const logout = () => {
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px dashed #cbd5e1;
|
border: 1px dashed #cbd5e1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 付款截图独占一行 */
|
||||||
|
.payment-item {
|
||||||
|
grid-column: span 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-label {
|
.file-label {
|
||||||
@@ -688,6 +763,7 @@ const logout = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
color: #475569;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-footer {
|
.drawer-footer {
|
||||||
@@ -713,6 +789,10 @@ const logout = () => {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.payment-item {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,17 +186,26 @@ import {
|
|||||||
NForm, NFormItem, NInput, NInputNumber, NDatePicker, NUpload, NUploadDragger,
|
NForm, NFormItem, NInput, NInputNumber, NDatePicker, NUpload, NUploadDragger,
|
||||||
NButton, NIcon, NText, NP
|
NButton, NIcon, NText, NP
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AnalyticsOutline, LogOutOutline, IdCardOutline, DocumentAttachOutline,
|
AnalyticsOutline, LogOutOutline, IdCardOutline, DocumentAttachOutline,
|
||||||
ImagesOutline, BrushOutline, CloudUploadOutline, CameraOutline,
|
ImagesOutline, BrushOutline, CloudUploadOutline, CameraOutline,
|
||||||
CreateOutline, CloseOutline, CheckmarkCircleOutline
|
CreateOutline, CloseOutline, CheckmarkCircleOutline
|
||||||
} from '@vicons/ionicons5'
|
} from '@vicons/ionicons5'
|
||||||
|
|
||||||
import http from '@/utils/http'
|
import http from '@/utils/http'
|
||||||
|
import * as ww from '@wecom/jssdk'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const router = useRouter()
|
||||||
|
const formRef = ref(null)
|
||||||
|
|
||||||
|
// 【修复 1】添加 isWWReady 响应式变量的声明
|
||||||
|
const isWWReady = ref(false)
|
||||||
|
|
||||||
|
// 【修复 2 & 3】使用 let 声明 wecomId,并且不在顶层作用域立即调用 API
|
||||||
|
let wecomId = ''
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主题定制:优化圆角和品牌色
|
* 主题定制
|
||||||
*/
|
*/
|
||||||
const themeOverrides = {
|
const themeOverrides = {
|
||||||
common: {
|
common: {
|
||||||
@@ -207,15 +216,64 @@ const themeOverrides = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = useMessage()
|
/**
|
||||||
const route = useRoute()
|
* 初始化企业微信 JSSDK
|
||||||
const router = useRouter()
|
*/
|
||||||
const formRef = ref(null)
|
async function getConfigSignature(url) {
|
||||||
|
const response = await fetch('https://superdata.nycjy.cn/api/v1/wecom/agent-config-signature', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ url })
|
||||||
|
})
|
||||||
|
const ConfigSignature = await response.json()
|
||||||
|
console.log('基础签名响应:', ConfigSignature)
|
||||||
|
if (ConfigSignature.code === 200 && ConfigSignature.data) {
|
||||||
|
return {
|
||||||
|
timestamp: ConfigSignature.data.timestamp,
|
||||||
|
nonceStr: ConfigSignature.data.nonceStr,
|
||||||
|
signature: ConfigSignature.data.corpSignature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('基础签名获取失败')
|
||||||
|
}
|
||||||
|
|
||||||
// 保持原逻辑:获取 wecom_id
|
async function getAgentConfigSignature(url) {
|
||||||
const wecomId = route.query.wecom_id || 'wmcr-ECwAAzKclEfIKNcVgOdxD-TcqLg'
|
const response = await fetch('https://superdata.nycjy.cn/api/v1/wecom/agent-config-signature', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ url })
|
||||||
|
})
|
||||||
|
const AgentConfigSignature = await response.json()
|
||||||
|
console.log('应用签名响应:', AgentConfigSignature)
|
||||||
|
if (AgentConfigSignature.code === 200 && AgentConfigSignature.data) {
|
||||||
|
return {
|
||||||
|
timestamp: AgentConfigSignature.data.timestamp,
|
||||||
|
nonceStr: AgentConfigSignature.data.nonceStr,
|
||||||
|
signature: AgentConfigSignature.data.signature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('应用签名获取失败')
|
||||||
|
}
|
||||||
|
const initSDK = async () => {
|
||||||
|
try {
|
||||||
|
await ww.register({
|
||||||
|
corpId: 'wwf72acc5a681dca93',
|
||||||
|
agentId: 1000135,
|
||||||
|
jsApiList: ['getCurExternalContact'],
|
||||||
|
getAgentConfigSignature,
|
||||||
|
getConfigSignature
|
||||||
|
})
|
||||||
|
isWWReady.value = true
|
||||||
|
console.log('企微JSSDK初始化成功')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('SDK初始化失败', e)
|
||||||
|
message.error('企业微信SDK初始化失败,请检查配置或环境')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 保持原逻辑:响应式表单数据
|
/**
|
||||||
|
* 响应式表单数据
|
||||||
|
*/
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
analystSupervisor: '',
|
analystSupervisor: '',
|
||||||
analystDepartment: '',
|
analystDepartment: '',
|
||||||
@@ -232,12 +290,16 @@ const formData = reactive({
|
|||||||
signatureFileList: []
|
signatureFileList: []
|
||||||
})
|
})
|
||||||
|
|
||||||
// 保持原逻辑:文件预览列表
|
/**
|
||||||
|
* 文件预览列表
|
||||||
|
*/
|
||||||
const documentFileListLast = ref([])
|
const documentFileListLast = ref([])
|
||||||
const paymentFileListLast = ref([])
|
const paymentFileListLast = ref([])
|
||||||
const signatureFileListLast = ref([])
|
const signatureFileListLast = ref([])
|
||||||
|
|
||||||
// 保持原逻辑:校验规则
|
/**
|
||||||
|
* 表单校验规则
|
||||||
|
*/
|
||||||
const rules = {
|
const rules = {
|
||||||
analystSupervisor: { required: true, message: '请输入主管姓名', trigger: 'blur' },
|
analystSupervisor: { required: true, message: '请输入主管姓名', trigger: 'blur' },
|
||||||
parentName: { required: true, message: '请输入家长姓名', trigger: 'blur' },
|
parentName: { required: true, message: '请输入家长姓名', trigger: 'blur' },
|
||||||
@@ -245,7 +307,7 @@ const rules = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保持原逻辑:格式化日期
|
* 格式化日期
|
||||||
*/
|
*/
|
||||||
const formatDate = (timestamp) => {
|
const formatDate = (timestamp) => {
|
||||||
if (!timestamp) return null;
|
if (!timestamp) return null;
|
||||||
@@ -257,7 +319,7 @@ const formatDate = (timestamp) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保持原逻辑:通用文件上传逻辑
|
* 通用文件上传逻辑
|
||||||
*/
|
*/
|
||||||
const handleCustomUpload = async ({ file, onFinish, onError, onProgress }, type) => {
|
const handleCustomUpload = async ({ file, onFinish, onError, onProgress }, type) => {
|
||||||
try {
|
try {
|
||||||
@@ -294,60 +356,84 @@ const handleCustomUpload = async ({ file, onFinish, onError, onProgress }, type)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保持原逻辑:初始化数据回显
|
* 初始化数据回显
|
||||||
*/
|
*/
|
||||||
const initData = async () => {
|
const initData = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await http.get('/v1/customer/get_customers_info', { wecom_id: wecomId })
|
// 【修复 3】确保在 SDK ready 后再调用 API
|
||||||
|
if (isWWReady.value) {
|
||||||
|
const contactRes = await ww.getCurExternalContact()
|
||||||
|
wecomId = contactRes?.userId || ''
|
||||||
|
|
||||||
formData.analystName = res.analystName
|
if (!wecomId) {
|
||||||
formData.parentName = res.customerName
|
message.warning('未获取到当前客户ID,请确认在企微侧边栏打开');
|
||||||
formData.parentPhone = res.customerPhone
|
return;
|
||||||
formData.analystNotes = res.notes
|
}
|
||||||
formData.analystSupervisor = res.analystDepartmentLeader?.[0] || ''
|
|
||||||
formData.analystDepartment = res.analystDepartmentName?.[0] || ''
|
|
||||||
if (res.dealAmount) formData.transactionAmount = Number(res.dealAmount)
|
|
||||||
if (res.dealDate) formData.transactionDate = new Date(res.dealDate).getTime()
|
|
||||||
|
|
||||||
if (res.proofOfPayment && res.proofOfPayment_urls) {
|
console.log('当前客户 wecom_id:', wecomId);
|
||||||
const paymentFiles = res.proofOfPayment.map((objName, index) => ({
|
const res = await http.get('/v1/customer/get_customers_info', { wecom_id: wecomId })
|
||||||
id: objName,
|
|
||||||
name: `凭证-${index + 1}`,
|
formData.analystName = res.analystName
|
||||||
status: 'finished',
|
formData.parentName = res.customerName
|
||||||
url: res.proofOfPayment_urls[index],
|
formData.parentPhone = res.customerPhone
|
||||||
rawResponse: {
|
formData.analystNotes = res.notes
|
||||||
data: {
|
formData.analystSupervisor = res.analystDepartmentLeader?.[0] || ''
|
||||||
object_name: objName,
|
formData.analystDepartment = res.analystDepartmentName?.[0] || ''
|
||||||
url: res.proofOfPayment_urls[index],
|
if (res.dealAmount) formData.transactionAmount = Number(res.dealAmount)
|
||||||
},
|
if (res.dealDate) formData.transactionDate = new Date(res.dealDate).getTime()
|
||||||
success: true
|
|
||||||
}
|
if (res.proofOfPayment && res.proofOfPayment_urls) {
|
||||||
}))
|
const paymentFiles = res.proofOfPayment.map((objName, index) => ({
|
||||||
formData.paymentFileList = paymentFiles
|
id: objName,
|
||||||
paymentFileListLast.value = paymentFiles
|
name: `凭证-${index + 1}`,
|
||||||
|
status: 'finished',
|
||||||
|
url: res.proofOfPayment_urls[index],
|
||||||
|
rawResponse: {
|
||||||
|
data: {
|
||||||
|
object_name: objName,
|
||||||
|
url: res.proofOfPayment_urls[index],
|
||||||
|
},
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
formData.paymentFileList = paymentFiles
|
||||||
|
paymentFileListLast.value = paymentFiles
|
||||||
|
}
|
||||||
|
message.success('客户数据加载成功')
|
||||||
}
|
}
|
||||||
message.success('数据加载成功')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载错误:', error)
|
console.error('加载客户数据错误:', error)
|
||||||
message.error('客户信息加载失败')
|
message.error('客户信息加载失败,请刷新重试')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
/**
|
||||||
initData()
|
* 生命周期钩子:组件挂载后执行初始化
|
||||||
|
*/
|
||||||
|
onMounted(async () => {
|
||||||
|
await initSDK() // 等待 SDK 初始化完成
|
||||||
|
initData() // 然后再去获取业务数据
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件预览
|
||||||
|
*/
|
||||||
const handlePreview = (file) => {
|
const handlePreview = (file) => {
|
||||||
const url = file.url || file.thumbnailUrl
|
const url = file.url || file.thumbnailUrl
|
||||||
if (url) window.open(url)
|
if (url) window.open(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保持原逻辑:更新提交逻辑
|
* 提交表单
|
||||||
*/
|
*/
|
||||||
const handleSubmit = () => {
|
const handleSubmit = async () => {
|
||||||
formRef.value?.validate(async (errors) => {
|
formRef.value?.validate(async (errors) => {
|
||||||
if (!errors) {
|
if (!errors) {
|
||||||
|
if (!wecomId) {
|
||||||
|
message.error('未获取到客户ID,无法提交');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const submitPayload = {
|
const submitPayload = {
|
||||||
wecom_id: wecomId,
|
wecom_id: wecomId,
|
||||||
analyst_supervisor: formData.analystSupervisor,
|
analyst_supervisor: formData.analystSupervisor,
|
||||||
@@ -371,7 +457,7 @@ const handleSubmit = () => {
|
|||||||
message.error('提交失败,请联系管理员');
|
message.error('提交失败,请联系管理员');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
message.error('请完善必填信息');
|
message.error('请检查并完善必填信息');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user