feat(lead-detail): 在用户详情页添加表单数据展示区域
- 在 API 类型定义中新增 `UserFormItem` 和 `UserFormAnswer` 类型 - 添加 `getUserForms` API 函数用于获取指定用户的表单数据 - 在用户详情面板中新增表单数据展示区域,支持加载状态、错误处理和空状态 - 添加 `getQuestionLabel` 和 `getAnswerText` 工具函数来格式化表单答案的显示 - 当选中不同用户时,自动获取并展示对应的表单数据 - 为现有 `LeadItem` 类型的部分字段添加中文注释以提升代码可读性
This commit is contained in:
@@ -12,19 +12,19 @@ export type LeadItem = {
|
||||
hook_user_id: string
|
||||
id: number
|
||||
avatar_url: string
|
||||
sex: number
|
||||
follow_up_user_id: string
|
||||
wechat_add_time: string
|
||||
sex: number // 性别 0: 未知, 1: 男, 2: 女
|
||||
follow_up_user_id: string // 跟进人ID
|
||||
wechat_add_time: string // 微信添加时间
|
||||
auto_status: number
|
||||
is_synced: number
|
||||
is_synced: number // 是否同步到服务器 0: 未同步, 1: 已同步
|
||||
created_at: string
|
||||
nickname: string
|
||||
phone: string
|
||||
follow_up_name: string
|
||||
source_type: number
|
||||
status: number
|
||||
wechat_status: number
|
||||
extended_info: Record<string, unknown>
|
||||
source_type: number //内外联系人 1: 外部联系人, 0: 内部联系人
|
||||
status: number // 状态 0: 正常, 1: 禁用
|
||||
wechat_status: number // 微信状态 0: 正常, 1: 已删除
|
||||
extended_info: Record<string, unknown> //
|
||||
updated_at: string
|
||||
last_active_at: string | null
|
||||
}
|
||||
@@ -36,6 +36,29 @@ export type LeadListResponse = {
|
||||
items: LeadItem[]
|
||||
}
|
||||
|
||||
export type UserFormAnswer = {
|
||||
question_label?: string
|
||||
answer?: unknown
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type UserFormItem = {
|
||||
form_id: string
|
||||
form_title: string
|
||||
answers: UserFormAnswer[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export const getUserForms = (userId: string) => {
|
||||
return http.get<UserFormItem[], UserFormItem[]>('/api/v1/admin/user/form', {
|
||||
params: {
|
||||
user_id: userId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const getAdminUserStatus = (params: AdminUserStatusParams = {}) => {
|
||||
return http.get<LeadListResponse, LeadListResponse>('/api/v1/admin/user/status', {
|
||||
params: {
|
||||
|
||||
@@ -134,6 +134,42 @@
|
||||
<span class="text-slate-300 font-mono">{{ formatTime(selectedLead.updated_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表单数据区域 -->
|
||||
<div class="pt-4 border-t border-slate-700/50">
|
||||
<div class="text-xs font-bold text-slate-300 mb-3 flex items-center justify-between">
|
||||
<span>表单数据</span>
|
||||
<span v-if="userForms.length" class="badge badge-xs badge-neutral">{{ userForms.length }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="formsLoading" class="flex justify-center py-4">
|
||||
<span class="loading loading-spinner loading-xs text-slate-500"></span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="formsError" class="text-xs text-red-400 bg-red-900/10 p-2 rounded">
|
||||
{{ formsError }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="userForms.length === 0" class="text-center text-slate-600 text-xs py-4">
|
||||
暂无表单记录
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3 max-h-[400px] overflow-y-auto pr-1 custom-scrollbar">
|
||||
<div v-for="form in userForms" :key="form.form_id" class="bg-slate-950/40 p-3 rounded-xl border border-slate-800/60 hover:border-slate-700 transition-colors">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="font-medium text-slate-200 text-xs">{{ form.form_title || '未命名表单' }}</div>
|
||||
<div class="text-[10px] text-slate-500 font-mono">{{ formatTime(form.created_at).split(' ')[0] }}</div>
|
||||
</div>
|
||||
<div v-if="form.answers && form.answers.length" class="mt-3 grid grid-cols-1 gap-2">
|
||||
<div v-for="(answer, index) in form.answers" :key="`${form.form_id}-${index}`" class="flex items-start gap-2 rounded-lg bg-slate-900/60 px-2.5 py-2">
|
||||
<div class="text-[10px] text-slate-500 w-24 shrink-0">{{ getQuestionLabel(answer) }}</div>
|
||||
<div class="text-[10px] text-slate-200 break-words flex-1 text-right">{{ getAnswerText(answer) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-[10px] text-slate-600 mt-2">暂无答案</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,8 +192,8 @@ import SidebarNav from './components/SidebarNav.vue';
|
||||
import DashboardView from './components/DashboardView.vue';
|
||||
import StrategyView from './components/StrategyView.vue';
|
||||
import MonitorView from './components/MonitorView.vue';
|
||||
import { getAdminUserStatus } from '@/api';
|
||||
import type { LeadListResponse, LeadItem } from '@/api';
|
||||
import { getAdminUserStatus, getUserForms } from '@/api';
|
||||
import type { LeadListResponse, LeadItem, UserFormAnswer, UserFormItem } from '@/api';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
@@ -211,6 +247,20 @@ const formatTime = (value: string | null) => {
|
||||
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
||||
};
|
||||
|
||||
const getQuestionLabel = (answer: UserFormAnswer) => {
|
||||
const label = answer.question_label;
|
||||
if (typeof label === 'string' && label.trim()) return label;
|
||||
return '问题';
|
||||
};
|
||||
|
||||
const getAnswerText = (answer: UserFormAnswer) => {
|
||||
const value = answer.answer;
|
||||
if (Array.isArray(value)) return value.join('、');
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const fetchLeads = async () => {
|
||||
if (leadsLoading.value) return;
|
||||
leadsLoading.value = true;
|
||||
@@ -227,6 +277,40 @@ const fetchLeads = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const userForms = ref<UserFormItem[]>([]);
|
||||
const formsLoading = ref(false);
|
||||
const formsError = ref('');
|
||||
|
||||
const fetchUserForms = async (userId: string) => {
|
||||
formsLoading.value = true;
|
||||
formsError.value = '';
|
||||
userForms.value = [];
|
||||
try {
|
||||
const res = await getUserForms(userId);
|
||||
console.log('getUserForms response:', res);
|
||||
if (Array.isArray(res.data)) {
|
||||
userForms.value = res.data;
|
||||
} else {
|
||||
userForms.value = [];
|
||||
formsError.value = '表单数据格式错误';
|
||||
}
|
||||
console.log('userForms:', userForms.value);
|
||||
} catch (error) {
|
||||
formsError.value = error instanceof Error ? error.message : '获取表单失败';
|
||||
console.error('Fetch forms error:', error);
|
||||
} finally {
|
||||
formsLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(selectedLead, (newLead) => {
|
||||
if (newLead) {
|
||||
fetchUserForms(newLead.hook_user_id);
|
||||
} else {
|
||||
userForms.value = [];
|
||||
}
|
||||
});
|
||||
|
||||
// 1. 销售阶段 (SOP)
|
||||
const salesStages = [
|
||||
{ id: 'connect', name: '1. 浅建联 (加微/破冰)', desc: '初步接触,建立信任,发送欢迎语' },
|
||||
|
||||
Reference in New Issue
Block a user