feat(wecom): 集成企业微信JSSDK并重构客户信息获取逻辑

refactor(admin): 优化文件上传处理及表单字段管理

fix(user): 修复企微SDK初始化及客户ID获取问题
This commit is contained in:
2026-03-23 16:46:25 +08:00
parent 40c720fdd7
commit 55695da2fd
4 changed files with 267 additions and 92 deletions

View File

@@ -13,6 +13,7 @@
},
"dependencies": {
"@vicons/ionicons5": "^0.13.0",
"@wecom/jssdk": "^2.3.4",
"axios": "^1.13.6",
"dev": "^0.1.3",
"naive-ui": "^2.44.1",

8
pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@vicons/ionicons5':
specifier: ^0.13.0
version: 0.13.0
'@wecom/jssdk':
specifier: ^2.3.4
version: 2.3.4
axios:
specifier: ^1.13.6
version: 1.13.6
@@ -650,6 +653,9 @@ packages:
vue:
optional: true
'@wecom/jssdk@2.3.4':
resolution: {integrity: sha512-oLfuvwMBZCRRMowVi/JkKx/dLNGHCmmUbQfCYG7XGmHYbgkQJlAlack4jKBk+NVG4G0S24fIrAF+XwQuXXxgAw==}
acorn@8.16.0:
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
engines: {node: '>=0.4.0'}
@@ -1890,6 +1896,8 @@ snapshots:
typescript: 5.9.3
vue: 3.5.30(typescript@5.9.3)
'@wecom/jssdk@2.3.4': {}
acorn@8.16.0: {}
alien-signals@3.1.2: {}

View File

@@ -101,9 +101,9 @@
</n-card>
</main>
<!-- 详情抽屉 - 视觉重构 -->
<!-- 详情抽屉 -->
<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">
<n-space vertical :size="24">
<!-- 分配任务卡片 -->
@@ -136,14 +136,8 @@
</div>
<n-form label-placement="left" label-width="90" size="small" :model="editingRow">
<n-grid :x-gap="20" :cols="2">
<n-grid-item v-for="field in [
{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'},
]" :key="field.key">
<!-- 修复了之前的 v-for 报错,将数组抽离为了 baseInfoFields -->
<n-grid-item v-for="field in baseInfoFields" :key="field.key">
<n-form-item :label="field.label">
<n-input v-model:value="editingRow[field.key]" />
</n-form-item>
@@ -162,27 +156,43 @@
</n-form>
</div>
<!-- 附件区 -->
<!-- 附件/素材区 -->
<div class="file-grid">
<!-- 1. 附件文档 -->
<div class="file-item">
<div class="file-label"><n-icon><FileTrayOutline /></n-icon> 附件文档</div>
<n-upload
v-model:file-list="editingRow.attachmentFileList"
: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>
</div>
<!-- 2. 电子签名 -->
<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
v-model:file-list="editingRow.paymentFileList"
list-type="image-card"
multiple
@preview="handlePreview"
:custom-request="({ file, onFinish, onError, onProgress }) => handleCustomUpload({ file, onFinish, onError, onProgress }, 'paymentFileList')"
:custom-request="handleCustomUpload"
/>
</div>
</div>
@@ -208,21 +218,17 @@
</template>
<script setup>
/**
* Script 部分保持原样,无需修改逻辑
* 这里省略重复的逻辑代码以减少篇幅,请直接沿用你原文件中的 script 内容
*/
import { ref, h, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import {
useMessage, zhCN, dateZhCN, NConfigProvider, NCard, NDataTable, NButton, NIcon, NInput,
NDrawer, NDrawerContent, NSpace, NForm, NFormItem, NGrid, NGridItem, NDatePicker, NSelect,
NTag, NUpload, NModal, NText, NBadge
NTag, NUpload, NModal, NBadge
} from 'naive-ui'
import {
SearchOutline, LogOutOutline, DownloadOutline, PersonOutline, FileTrayOutline,
ImagesOutline, BrushOutline, ReaderOutline, EyeOutline
ImagesOutline, BrushOutline, ReaderOutline, EyeOutline, CreateOutline
} from '@vicons/ionicons5'
import http from '@/utils/http'
@@ -231,6 +237,16 @@ const message = useMessage()
const loading = 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({
wecom_id: '',
parentInfo: '',
@@ -262,6 +278,9 @@ const pagination = reactive({
}
})
/**
* 核心逻辑:数据请求与文件对象标准化
*/
const fetchData = async () => {
loading.value = true
try {
@@ -278,18 +297,22 @@ const fetchData = async () => {
query.append('start_date', startDate)
query.append('end_date', endDate)
}
const res = await http.get(`/v1/customer/list?${query.toString()}`)
if (res && res.success) {
displayData.value = res.data.map(item => {
// 列表回显处理:为了让 n-upload 正常显示缩略图,仍然必须组装包含 url 字段的对象
const p_names = Array.isArray(item.payment_object_names) ? item.payment_object_names :[]
const p_urls = item.payment_image_url ||[]
const paymentFileList = p_urls.map((url, i) => ({
id: p_names[i] || `pay_${i}`,
name: `付款截图_${i + 1}.png`,
status: 'finished',
url: url,
object_name: p_names[i]
url: url, // 映射到组件的 url 以展示缩略图
object_name: p_names[i] // 保留真实的 object_name
}))
// 处理签名
const signatureFileList = item.signature_image_url ?[{
id: item.signature_object_name || 'sig',
name: '电子签名.png',
@@ -297,6 +320,8 @@ const fetchData = async () => {
url: item.signature_image_url,
object_name: item.signature_object_name
}] :[]
// 处理附件文档
const attachmentFileList = item.attachment_file_url ?[{
id: item.attachment_object_name || 'att',
name: '原始附件文档',
@@ -304,6 +329,7 @@ const fetchData = async () => {
url: item.attachment_file_url,
object_name: item.attachment_object_name
}] :[]
return {
...item,
transaction_amount: item.transaction_amount || '0',
@@ -318,54 +344,85 @@ const fetchData = async () => {
pagination.itemCount = res.pagination?.total || 0
}
} catch (error) {
console.error(error)
console.error('Fetch error:', error)
} finally {
loading.value = false
}
}
const handleCustomUpload = async ({ file, onFinish, onError, onProgress }, type) => {
/**
* 核心上传逻辑:基于引用的响应式更新 (已适配最新接口格式)
*/
const handleCustomUpload = async ({ file, onFinish, onError, onProgress }) => {
try {
const uploadData = new FormData()
uploadData.append('file', file.file)
const response = await http.post('/v1/material/upload', uploadData, {
headers: { 'Content-Type': 'multipart/form-data' },
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'
message.success('上传成功')
onFinish()
} else {
message.error(response.message || '上传失败,接口返回数据格式异常')
onError()
}
} catch (error) {
console.error('Upload error:', error)
message.error('网络错误,上传失败')
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) => {
if (!row.assignee_name || !row.assignee_phone) {
message.warning('请填写指导师姓名和电话')
return false
}
// 检查上传状态
const allFiles = [...row.paymentFileList, ...row.signatureFileList, ...row.attachmentFileList]
if (allFiles.some(f => f.status === 'uploading')) {
message.warning('文件正在上传中,请稍后提交')
return false
}
row.isSubmitting = true
try {
const payment_object_names = row.paymentFileList.map(getObjectName).filter(Boolean)
const signature_object_name = row.signatureFileList.length > 0 ? getObjectName(row.signatureFileList[0]) : ''
const attachment_object_name = row.attachmentFileList.length > 0 ? getObjectName(row.attachmentFileList[0]) : ''
// 从当前数组提取最新的 object_name
const payment_object_names = row.paymentFileList.map(f => f.name || f?.object_name || f.name).filter(Boolean)
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 = {
wecom_id: String(row.wecom_id),
payment_object_names: payment_object_names,
signature_object_name: signature_object_name,
attachment_object_name: attachment_object_name,
payment_object_names,
signature_object_name,
attachment_object_name,
analyst_supervisor: row.analyst_supervisor,
analyst_department: row.analyst_department,
analyst_name: row.analyst_name,
@@ -378,14 +435,18 @@ const submitAssignment = async (row) => {
assignee_name: row.assignee_name,
assignee_phone: row.assignee_phone
}
console.log(24531221126,row)
console.log('提交的数据:', payload)
const res = await http.post('/v1/material/submit', payload)
if (res && (res.success || res.code === 200)) {
message.success('分配成功')
message.success('分配并同步成功')
row.status = 'processed'
return true
}
return false
} catch (error) {
console.error('Submit error:', error)
return false
} finally {
row.isSubmitting = false
@@ -397,12 +458,14 @@ const editingRow = ref({})
const showPreview = ref(false)
const previewImageUrl = ref('')
/**
* 打开详情并执行深拷贝隔离
*/
const openDetails = (row) => {
const rowCopy = JSON.parse(JSON.stringify(row))
if (!rowCopy.transaction_date) {
rowCopy.transaction_date = null
editingRow.value = JSON.parse(JSON.stringify(row))
if (!editingRow.value.transaction_date) {
editingRow.value.transaction_date = null
}
editingRow.value = rowCopy
showDrawer.value = true
}
@@ -416,6 +479,9 @@ const handleDrawerSubmit = async () => {
}
}
/**
* 表格列配置
*/
const columns =[
{ title: '分析师', key: 'analyst_name', width: 100, fixed: 'left' },
{ title: '家长姓名', key: 'parent_name', width: 100 },
@@ -485,6 +551,7 @@ const columns =[
}
]
// 预览依然取 url因为 handleCustomUpload 已将接口真实的 preview_url 映射给了 url
const handlePreview = (file) => {
previewImageUrl.value = file.url || (file.file && URL.createObjectURL(file.file))
showPreview.value = true
@@ -668,6 +735,7 @@ const logout = () => {
font-size: 16px;
}
/* 文件网格布局 */
.file-grid {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -679,6 +747,13 @@ const logout = () => {
padding: 16px;
border-radius: 12px;
border: 1px dashed #cbd5e1;
display: flex;
flex-direction: column;
}
/* 付款截图独占一行 */
.payment-item {
grid-column: span 2;
}
.file-label {
@@ -688,6 +763,7 @@ const logout = () => {
display: flex;
align-items: center;
gap: 6px;
color: #475569;
}
.drawer-footer {
@@ -713,6 +789,10 @@ const logout = () => {
grid-template-columns: 1fr;
}
.payment-item {
grid-column: span 1;
}
.content-wrapper {
padding: 12px;
}

View File

@@ -186,17 +186,26 @@ import {
NForm, NFormItem, NInput, NInputNumber, NDatePicker, NUpload, NUploadDragger,
NButton, NIcon, NText, NP
} from 'naive-ui'
import {
AnalyticsOutline, LogOutOutline, IdCardOutline, DocumentAttachOutline,
ImagesOutline, BrushOutline, CloudUploadOutline, CameraOutline,
CreateOutline, CloseOutline, CheckmarkCircleOutline
} from '@vicons/ionicons5'
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 = {
common: {
@@ -207,15 +216,64 @@ const themeOverrides = {
}
}
const message = useMessage()
const route = useRoute()
const router = useRouter()
const formRef = ref(null)
/**
* 初始化企业微信 JSSDK
*/
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
const wecomId = route.query.wecom_id || 'wmcr-ECwAAzKclEfIKNcVgOdxD-TcqLg'
async function getAgentConfigSignature(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 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({
analystSupervisor: '',
analystDepartment: '',
@@ -232,12 +290,16 @@ const formData = reactive({
signatureFileList: []
})
// 保持原逻辑:文件预览列表
/**
* 文件预览列表
*/
const documentFileListLast = ref([])
const paymentFileListLast = ref([])
const signatureFileListLast = ref([])
// 保持原逻辑:校验规则
/**
* 表单校验规则
*/
const rules = {
analystSupervisor: { required: true, message: '请输入主管姓名', trigger: 'blur' },
parentName: { required: true, message: '请输入家长姓名', trigger: 'blur' },
@@ -245,7 +307,7 @@ const rules = {
}
/**
* 保持原逻辑:格式化日期
* 格式化日期
*/
const formatDate = (timestamp) => {
if (!timestamp) return null;
@@ -257,7 +319,7 @@ const formatDate = (timestamp) => {
};
/**
* 保持原逻辑:通用文件上传逻辑
* 通用文件上传逻辑
*/
const handleCustomUpload = async ({ file, onFinish, onError, onProgress }, type) => {
try {
@@ -294,10 +356,21 @@ const handleCustomUpload = async ({ file, onFinish, onError, onProgress }, type)
}
/**
* 保持原逻辑:初始化数据回显
* 初始化数据回显
*/
const initData = async () => {
try {
// 【修复 3】确保在 SDK ready 后再调用 API
if (isWWReady.value) {
const contactRes = await ww.getCurExternalContact()
wecomId = contactRes?.userId || ''
if (!wecomId) {
message.warning('未获取到当前客户ID请确认在企微侧边栏打开');
return;
}
console.log('当前客户 wecom_id:', wecomId);
const res = await http.get('/v1/customer/get_customers_info', { wecom_id: wecomId })
formData.analystName = res.analystName
@@ -326,28 +399,41 @@ const initData = async () => {
formData.paymentFileList = paymentFiles
paymentFileListLast.value = paymentFiles
}
message.success('数据加载成功')
message.success('客户数据加载成功')
}
} catch (error) {
console.error('加载错误:', error)
message.error('客户信息加载失败')
console.error('加载客户数据错误:', error)
message.error('客户信息加载失败,请刷新重试')
}
}
onMounted(() => {
initData()
/**
* 生命周期钩子:组件挂载后执行初始化
*/
onMounted(async () => {
await initSDK() // 等待 SDK 初始化完成
initData() // 然后再去获取业务数据
})
/**
* 文件预览
*/
const handlePreview = (file) => {
const url = file.url || file.thumbnailUrl
if (url) window.open(url)
}
/**
* 保持原逻辑:更新提交逻辑
* 提交表单
*/
const handleSubmit = () => {
const handleSubmit = async () => {
formRef.value?.validate(async (errors) => {
if (!errors) {
if (!wecomId) {
message.error('未获取到客户ID无法提交');
return;
}
const submitPayload = {
wecom_id: wecomId,
analyst_supervisor: formData.analystSupervisor,
@@ -371,7 +457,7 @@ const handleSubmit = () => {
message.error('提交失败,请联系管理员');
}
} else {
message.error('请完善必填信息');
message.error('请检查并完善必填信息');
}
});
};