CustomersView.vue 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. <template>
  2. <div class="data-page">
  3. <div class="data-header">
  4. <div class="data-title">📋 客户池 - 共{{ customerStats.total }}家</div>
  5. <div style="display: flex; gap: 8px;">
  6. <button class="btn btn-secondary"><i class="fas fa-download"></i> 导出</button>
  7. <!-- <button class="btn btn-primary" @click="showAddModal = true"><i class="fas fa-plus"></i> 添加客户</button> -->
  8. </div>
  9. </div>
  10. <!-- Stats -->
  11. <div class="stat-row">
  12. <div class="stat-card-lg" @click="filterTab = 'all'">
  13. <div class="stat-icon-lg blue"><i class="fas fa-users"></i></div>
  14. <div class="stat-content-lg"><h3>{{ customerStats.total }}</h3><p>总客户数</p></div>
  15. </div>
  16. <div class="stat-card-lg" @click="filterTab = 'prospect'">
  17. <div class="stat-icon-lg purple"><i class="fas fa-user-clock"></i></div>
  18. <div class="stat-content-lg"><h3>{{ customerStats.prospect }}</h3><p>潜在客户</p></div>
  19. </div>
  20. <div class="stat-card-lg" @click="filterTab = 'intent'">
  21. <div class="stat-icon-lg orange"><i class="fas fa-star"></i></div>
  22. <div class="stat-content-lg"><h3>{{ customerStats.intent }}</h3><p>意向客户</p></div>
  23. </div>
  24. <div class="stat-card-lg" @click="filterTab = 'deal'">
  25. <div class="stat-icon-lg green"><i class="fas fa-handshake"></i></div>
  26. <div class="stat-content-lg"><h3>{{ customerStats.deal }}</h3><p>成交客户</p></div>
  27. </div>
  28. <div class="stat-card-lg" @click="filterTab = 'silent'">
  29. <div class="stat-icon-lg" style="background: rgba(148, 163, 184, 0.2); color: var(--text-secondary);"><i class="fas fa-moon"></i></div>
  30. <div class="stat-content-lg"><h3>{{ customerStats.silent }}</h3><p>沉默客户</p></div>
  31. </div>
  32. </div>
  33. <!-- Search & Filter -->
  34. <div style="display: flex; gap: 16px; margin-bottom: 20px;">
  35. <div class="search-box" style="flex: 1;">
  36. <i class="fas fa-search search-icon"></i>
  37. <input type="text" v-model="searchQuery" class="search-input" placeholder="搜索公司名称、联系人、邮箱..." @input="getCustomer">
  38. </div>
  39. <div style="display: flex; gap: 8px;">
  40. <el-cascader class="form-select" style="width: auto; padding: 0;" placeholder="区域名称" :options="region"
  41. v-model="regionValue" clearable filterable @change="getRegion" />
  42. <!-- <select v-model="industryFilter" class="form-select" style="width: auto; padding: 8px 16px;">
  43. <option value="">全部行业</option>
  44. <option value="固态硬盘">固态硬盘</option>
  45. <option value="电子分销">电子分销</option>
  46. <option value="企业级存储">企业级存储</option>
  47. </select>
  48. <button class="btn btn-secondary"><i class="fas fa-sliders-h"></i> 高级筛选</button> -->
  49. </div>
  50. </div>
  51. <!-- Tabs -->
  52. <div style="display: flex; gap: 4px; margin-bottom: 20px; border-bottom: 1px solid var(--border); padding-bottom: 12px;">
  53. <div v-for="tab in tabs" :key="tab.key"
  54. :style="{
  55. padding: '8px 16px',
  56. borderRadius: '8px 8px 0 0',
  57. cursor: 'pointer',
  58. fontSize: '13px',
  59. background: filterTab === tab.key ? 'var(--bg-hover)' : 'transparent',
  60. color: filterTab === tab.key ? 'var(--text-primary)' : 'var(--text-secondary)',
  61. fontWeight: filterTab === tab.key ? '600' : '400'
  62. }"
  63. @click="filterTab = tab.key">
  64. {{ tab.label }} <span style="font-size: 11px; opacity: 0.7;">({{ tab.count }})</span>
  65. </div>
  66. </div>
  67. <!-- Customer List -->
  68. <!-- <div style="flex: 1; overflow-y: auto;"> -->
  69. <div v-for="customer in filteredCustomers" :key="customer.id"
  70. class="customer-card"
  71. @click="showCustomerDetail(customer)">
  72. <div class="customer-flag">{{ customer.flag }}</div>
  73. <div class="customer-info">
  74. <div class="customer-header">
  75. <span class="customer-name">{{ customer.customerName }}</span>
  76. <!-- 客户类型标识 -->
  77. <!-- <span class="customer-status" :style="{ background: getStatusBg(customer.status), color: getStatusColor(customer.status) }">
  78. {{ getStatusLabel(customer.status) }}
  79. </span> -->
  80. </div>
  81. <div class="customer-contact">
  82. <i class="fas fa-user"></i> {{ customer.liaison?.name }}<span v-if="customer.liaison?.departmentPosition"> · </span>{{ customer.liaison?.departmentPosition }}
  83. </div>
  84. <div class="customer-meta">
  85. <span><i class="fas fa-map-marker-alt"></i> {{ customer.continent }} · {{ customer.country }}</span>
  86. <span><i class="fas fa-industry"></i> {{ customer.customerCompany?.industry }}</span>
  87. <!-- <span><i class="fas fa-dollar-sign"></i> {{ customer.annualPurchase }}</span> -->
  88. </div>
  89. </div>
  90. <!-- <div class="customer-score">
  91. <div class="score-circle" :class="getScoreClass(customer.score)">{{ customer.score }}</div>
  92. <div class="score-label">意向分</div>
  93. </div> -->
  94. <div class="customer-actions" @click.stop>
  95. <button class="btn btn-secondary btn-sm" @click="showCustomerDetail(customer)"><i class="fas fa-eye"></i>更多详情</button>
  96. </div>
  97. </div>
  98. <div class="pagination-area">
  99. <el-pagination
  100. background
  101. :total="customerStats.total"
  102. style="text-align: right;"
  103. :current-page="params.pageIndex"
  104. :page-size="params.pageSize"
  105. layout="total, prev, pager, next"
  106. @current-change="pageCurrentChange">
  107. </el-pagination>
  108. </div>
  109. <!-- </div> -->
  110. <!-- 引入添加客户弹窗组件 -->
  111. <!-- <AddCustomerModal
  112. :show-modal="showAddModal"
  113. @close="showAddModal = false"
  114. @add="handleAddCustomer"
  115. /> -->
  116. <!-- Customer Detail Modal -->
  117. <div v-if="showDetailModal && selectedCustomer" class="modal-overlay" @click.self="showDetailModal = false">
  118. <div class="modal customer-detail-modal">
  119. <div class="modal-header">
  120. <div class="modal-title">
  121. <span class="detail-flag">{{ selectedCustomer.flag }}</span>
  122. <div>
  123. <h3>{{ selectedCustomer.name }}</h3>
  124. <p>{{ selectedCustomer.industry }} · {{ selectedCustomer.location }}</p>
  125. </div>
  126. </div>
  127. <button class="modal-close" @click="showDetailModal = false"><i class="fas fa-times"></i></button>
  128. </div>
  129. <div class="modal-body">
  130. <!-- Quick Stats -->
  131. <!-- <div class="detail-stats">
  132. <div class="detail-stat-card">
  133. <div class="detail-stat-value" :class="getScoreClass(selectedCustomer.score)">{{ selectedCustomer.score }}</div>
  134. <div class="detail-stat-label">意向分</div>
  135. </div>
  136. <div class="detail-stat-card">
  137. <div class="detail-stat-value">{{ selectedCustomer.totalDeals }}</div>
  138. <div class="detail-stat-label">累计订单</div>
  139. </div>
  140. <div class="detail-stat-card">
  141. <div class="detail-stat-value success">¥{{ formatNumber(selectedCustomer.totalAmount) }}</div>
  142. <div class="detail-stat-label">累计金额</div>
  143. </div>
  144. <div class="detail-stat-card">
  145. <div class="detail-stat-value">{{ selectedCustomer.lastContact }}</div>
  146. <div class="detail-stat-label">最后联系</div>
  147. </div>
  148. </div> -->
  149. <!-- Contact Info -->
  150. <div class="detail-section">
  151. <h4><i class="fas fa-address-card"></i> 联系信息</h4>
  152. <div class="contact-grid">
  153. <div class="contact-item">
  154. <i class="fas fa-user"></i>
  155. <div class="contact-content">
  156. <div class="contact-label">主要联系人</div>
  157. <div class="contact-value">{{ selectedCustomer.liaison?.name }}</div>
  158. <div class="contact-sub">{{ selectedCustomer.liaison?.departmentPosition }}</div>
  159. </div>
  160. </div>
  161. <div class="contact-item">
  162. <i class="fas fa-envelope"></i>
  163. <div class="contact-content">
  164. <div class="contact-label">邮箱</div>
  165. <div class="contact-value">{{ selectedCustomer.liaison?.email }}</div>
  166. </div>
  167. </div>
  168. <div class="contact-item">
  169. <i class="fas fa-phone"></i>
  170. <div class="contact-content">
  171. <div class="contact-label">电话</div>
  172. <div class="contact-value">{{ selectedCustomer.liaison?.telephone }}</div>
  173. </div>
  174. </div>
  175. <div class="contact-item">
  176. <i class="fas fa-globe"></i>
  177. <div class="contact-content">
  178. <div class="contact-label">官网</div>
  179. <div class="contact-value link">{{ selectedCustomer.customerCompany?.website }}</div>
  180. </div>
  181. </div>
  182. </div>
  183. </div>
  184. <!-- AI Insights -->
  185. <div class="detail-section">
  186. <h4><i class="fas fa-robot"></i> AI 洞察</h4>
  187. <div class="ai-insight-cards" v-if="selectedCustomer.aiInsights">
  188. <div class="ai-insight-card" v-for="insight in selectedCustomer.aiInsights" :key="insight.type">
  189. <div class="insight-icon" :class="insight.level">
  190. <i :class="insight.icon"></i>
  191. </div>
  192. <div class="insight-content">
  193. <div class="insight-title">{{ insight.title }}</div>
  194. <div class="insight-desc">{{ insight.description }}</div>
  195. </div>
  196. </div>
  197. </div>
  198. </div>
  199. <!-- Interaction Timeline -->
  200. <div class="detail-section">
  201. <h4><i class="fas fa-history"></i> 互动记录</h4>
  202. <div class="timeline" v-if="selectedCustomer.interactions">
  203. <div class="timeline-item" v-for="record in selectedCustomer.interactions" :key="record.id">
  204. <div class="timeline-dot" :class="record.type"></div>
  205. <div class="timeline-content">
  206. <div class="timeline-header">
  207. <span class="timeline-action">{{ record.action }}</span>
  208. <span class="timeline-time">{{ record.time }}</span>
  209. </div>
  210. <div class="timeline-detail">{{ record.detail }}</div>
  211. </div>
  212. </div>
  213. </div>
  214. </div>
  215. <!-- Related Opportunities -->
  216. <!-- <div class="detail-section">
  217. <h4><i class="fas fa-briefcase"></i> 关联商机</h4>
  218. <div class="related-opps">
  219. <div class="related-opp" v-for="opp in selectedCustomer.opportunities" :key="opp.id">
  220. <div class="opp-info">
  221. <div class="opp-name">{{ opp.name }}</div>
  222. <div class="opp-meta">¥{{ formatNumber(opp.amount) }} · {{ opp.stage }}</div>
  223. </div>
  224. <div class="opp-status" :class="opp.statusClass">{{ opp.statusLabel }}</div>
  225. </div>
  226. </div>
  227. </div> -->
  228. </div>
  229. <div class="modal-footer">
  230. <button class="btn btn-secondary" @click="showDetailModal = false">关闭</button>
  231. <!-- <button class="btn btn-secondary"><i class="fas fa-edit"></i> 编辑</button> -->
  232. <button class="btn btn-primary" @click="">
  233. <i class="fas fa-rocket"></i> 客户详情
  234. </button>
  235. </div>
  236. </div>
  237. </div>
  238. <!-- Toast -->
  239. <transition name="toast">
  240. <div v-if="showToast" class="toast" :class="toastType">
  241. <i :class="toastType === 'success' ? 'fas fa-check-circle' : 'fas fa-info-circle'"></i>
  242. {{ toastMessage }}
  243. </div>
  244. </transition>
  245. </div>
  246. </template>
  247. <script setup>
  248. import { ref, computed } from 'vue'
  249. import { getCustomerList } from '../api/customer'
  250. import {region} from '@/utils/region';
  251. // 引入添加客户组件
  252. import AddCustomerModal from '../components/addCustomer.vue'
  253. const showAddModal = ref(false)
  254. const showDetailModal = ref(false)
  255. const selectedCustomer = ref(null)
  256. const filterTab = ref('all')
  257. const searchQuery = ref('')
  258. const regionFilter = ref('')
  259. const industryFilter = ref('')
  260. const showToast = ref(false)
  261. const toastMessage = ref('')
  262. const toastType = ref('success')
  263. const customerStats = ref({
  264. total: 0,
  265. prospect: 0,
  266. intent: 0,
  267. deal: 0,
  268. silent: 0
  269. })
  270. const tabs = ref([
  271. { key: 'all', label: '全部客户', count: 0 },
  272. // { key: 'prospect', label: '潜在客户', count: 812 },
  273. // { key: 'intent', label: '意向客户', count: 224 },
  274. // { key: 'deal', label: '成交客户', count: 156 },
  275. // { key: 'silent', label: '沉默客户', count: 55 }
  276. ])
  277. const customers = ref([])
  278. const customerTotal = ref()
  279. const params = ref({
  280. pageIndex: 1,
  281. pageSize: 10,
  282. blurry:null,
  283. country:null,
  284. })
  285. const getCustomer = () => {
  286. params.value.blurry = searchQuery.value
  287. getCustomerList(params.value).then(res => {
  288. customerStats.value.total = tabs.value[0].count = res.result.total
  289. customers.value = res.result.records
  290. })
  291. }
  292. getCustomer()
  293. const pageCurrentChange = (page) => {
  294. params.value.pageIndex = page
  295. getCustomer()
  296. }
  297. const regionValue = ref([])
  298. const getRegion = (value) => {
  299. params.value.country = value?value[1]:''
  300. getCustomer()
  301. }
  302. // const customers = ref([
  303. // {
  304. // id: 1, flag: '🇸🇦', name: 'Riyadh Tech Distribution', status: 'prospect', contact: 'Ahmed Al-Rashid',
  305. // role: '采购总监', location: '沙特·利雅得', industry: '固态硬盘进口商', annualPurchase: '年采购$2M+', score: 92,
  306. // email: 'ahmed@riyahtech.sa', phone: '+966 50 123 4567', website: 'www.riyahtech.sa',
  307. // totalDeals: 3, totalAmount: 4560000, lastContact: '3天前',
  308. // aiInsights: [
  309. // { type: 'purchase', level: 'high', icon: 'fas fa-shopping-cart', title: '采购意愿强烈', description: '近30天内访问官网5次,下载产品手册,建议重点跟进' },
  310. // { type: 'timing', level: 'medium', icon: 'fas fa-clock', title: '最佳联系时间', description: '根据历史数据分析,周二上午10点联系成功率最高' },
  311. // { type: 'decision', level: 'high', icon: 'fas fa-user-tie', title: '决策人已识别', description: 'Ahmed Al-Rashid 为采购决策人,已验证邮箱有效性' }
  312. // ],
  313. // interactions: [
  314. // { id: 1, type: 'email', action: '发送报价单', detail: 'SSD采购报价已发送,等待回复', time: '3天前' },
  315. // { id: 2, type: 'visit', action: '官网访问', detail: '浏览了企业级SSD产品页面,停留8分钟', time: '5天前' },
  316. // { id: 3, type: 'call', action: '电话沟通', detail: '确认了采购需求,数量约5000片', time: '1周前' },
  317. // { id: 4, type: 'email', action: '首次联系', detail: '通过LinkedIn找到联系人,发送介绍邮件', time: '2周前' }
  318. // ],
  319. // opportunities: [
  320. // { id: 1, name: 'SSD批量采购', amount: 850000, stage: '谈判中', statusClass: 'negotiation', statusLabel: '跟进中' }
  321. // ]
  322. // },
  323. // {
  324. // id: 2, flag: '🇦🇪', name: 'Dubai Storage Solutions', status: 'prospect', contact: 'Mohammed Khan',
  325. // role: 'CEO', location: '阿联酋·迪拜', industry: '电子分销商', annualPurchase: '年采购$1.5M', score: 88,
  326. // email: 'mohammed@dubaistorage.ae', phone: '+971 50 987 6543', website: 'www.dubaistorage.ae',
  327. // totalDeals: 2, totalAmount: 2800000, lastContact: '1天前',
  328. // aiInsights: [
  329. // { type: 'purchase', level: 'medium', icon: 'fas fa-chart-line', title: '采购潜力', description: '公司规模扩张中,预计明年需求增长30%' }
  330. // ],
  331. // interactions: [
  332. // { id: 1, type: 'meeting', action: '视频会议', detail: '讨论Q2采购计划,对NVMe SSD感兴趣', time: '1天前' }
  333. // ],
  334. // opportunities: []
  335. // },
  336. // {
  337. // id: 3, flag: '🇰🇼', name: 'Kuwait Data Systems', status: 'intent', contact: 'Faisal Al-Mutairi',
  338. // role: 'IT总监', location: '科威特', industry: '数据中心集成商', annualPurchase: '年采购$2.5M', score: 85,
  339. // email: 'faisal@kwdata.kw', phone: '+965 99 888 777', website: 'www.kwdata.kw',
  340. // totalDeals: 5, totalAmount: 6800000, lastContact: '今天',
  341. // aiInsights: [
  342. // { type: 'timing', level: 'high', icon: 'fas fa-fire', title: '紧急需求', description: '数据中心扩容项目急需采购,建议立即跟进' }
  343. // ],
  344. // interactions: [
  345. // { id: 1, type: 'email', action: '回复询价', detail: '确认了技术规格,要求提供样品', time: '今天' }
  346. // ],
  347. // opportunities: [
  348. // { id: 1, name: '数据中心扩容项目', amount: 1200000, stage: '方案报价', statusClass: 'proposal', statusLabel: '待报价' }
  349. // ]
  350. // },
  351. // {
  352. // id: 4, flag: '🇩🇪', name: 'Berlin Tech GmbH', status: 'deal', contact: 'Hans Mueller',
  353. // role: '采购经理', location: '德国·柏林', industry: '企业级存储', annualPurchase: '年采购$3M+', score: 95,
  354. // email: 'hans@berlintech.de', phone: '+49 30 12345678', website: 'www.berlintech.de',
  355. // totalDeals: 8, totalAmount: 12500000, lastContact: '2天前',
  356. // aiInsights: [],
  357. // interactions: [
  358. // { id: 1, type: 'order', action: '订单确认', detail: 'PO-2026-0315 已确认,金额¥680,000', time: '2天前' }
  359. // ],
  360. // opportunities: [
  361. // { id: 1, name: 'NVMe SSD批量采购', amount: 680000, stage: '已成交', statusClass: 'won', statusLabel: '已成交' }
  362. // ]
  363. // }
  364. // ])
  365. const filteredCustomers = computed(() => {
  366. let result = customers.value
  367. if (filterTab.value !== 'all') {
  368. result = result.filter(c => c.status === filterTab.value)
  369. }
  370. if (searchQuery.value) {
  371. const query = searchQuery.value.toLowerCase()
  372. result = result.filter(c =>
  373. c.name.toLowerCase().includes(query) ||
  374. c.contact.toLowerCase().includes(query) ||
  375. c.email?.toLowerCase().includes(query)
  376. )
  377. }
  378. return result
  379. })
  380. const formatNumber = (num) => num.toLocaleString()
  381. const getScoreClass = (score) => score >= 85 ? 'high' : score >= 70 ? 'medium' : 'low'
  382. const getStatusBg = (status) => ({
  383. prospect: 'rgba(139, 92, 246, 0.2)',
  384. intent: 'rgba(245, 158, 11, 0.2)',
  385. deal: 'rgba(16, 185, 129, 0.2)',
  386. silent: 'rgba(148, 163, 184, 0.2)'
  387. }[status] || 'rgba(148, 163, 184, 0.2)')
  388. const getStatusColor = (status) => ({
  389. prospect: 'var(--secondary)',
  390. intent: 'var(--warning)',
  391. deal: 'var(--success)',
  392. silent: 'var(--text-secondary)'
  393. }[status] || 'var(--text-secondary)')
  394. const getStatusLabel = (status) => ({
  395. prospect: '潜客',
  396. intent: '意向',
  397. deal: '成交',
  398. silent: '沉默'
  399. }[status] || status)
  400. const showCustomerDetail = (customer) => {
  401. selectedCustomer.value = customer
  402. showDetailModal.value = true
  403. }
  404. const sendEmail = (customer) => {
  405. showToastMessage(`正在发送邮件给 ${customer.contact}...`, 'success')
  406. }
  407. const createOpportunity = (customer) => {
  408. showDetailModal.value = false
  409. showToastMessage(`正在为 ${customer.name} 创建商机...`, 'success')
  410. }
  411. // 辅助函数:根据国家代码获取国旗
  412. const getFlagByCountry = (countryCode) => {
  413. const flagMap = {
  414. 'SA': '🇸🇦',
  415. 'AE': '🇦🇪',
  416. 'DE': '🇩🇪',
  417. 'FR': '🇫🇷',
  418. '': '🌐'
  419. }
  420. return flagMap[countryCode] || '🌐'
  421. }
  422. // 辅助函数:根据国家代码获取地区
  423. const getLocationByCountry = (countryCode) => {
  424. const locationMap = {
  425. 'SA': '沙特阿拉伯',
  426. 'AE': '阿联酋',
  427. 'DE': '德国',
  428. 'FR': '法国',
  429. '': '未填写'
  430. }
  431. return locationMap[countryCode] || '未填写'
  432. }
  433. const showToastMessage = (message, type = 'success') => {
  434. toastMessage.value = message
  435. toastType.value = type
  436. showToast.value = true
  437. setTimeout(() => showToast.value = false, 3000)
  438. }
  439. // ---------------- 暂时无用 ---------------------
  440. // 处理添加客户逻辑
  441. const handleAddCustomer = (customerData) => {
  442. // 模拟生成客户ID(实际项目中由后端生成)
  443. const newId = customers.value.length + 1
  444. // 构建完整的客户数据(补充默认值)
  445. const newCustomer = {
  446. id: newId,
  447. flag: getFlagByCountry(customerData.country),
  448. name: customerData.name,
  449. status: customerData.type,
  450. contact: customerData.contact,
  451. role: '未填写',
  452. location: getLocationByCountry(customerData.country),
  453. industry: '未填写',
  454. annualPurchase: '未填写',
  455. score: 80,
  456. email: customerData.email || '未填写',
  457. phone: customerData.phone || '未填写',
  458. website: '未填写',
  459. totalDeals: 0,
  460. totalAmount: 0,
  461. lastContact: '刚刚',
  462. aiInsights: [],
  463. interactions: [],
  464. opportunities: []
  465. }
  466. // 添加到客户列表
  467. customers.value.push(newCustomer)
  468. // 更新统计数据
  469. customerStats.total += 1
  470. if (customerData.type === 'prospect') customerStats.prospect += 1
  471. else if (customerData.type === 'intent') customerStats.intent += 1
  472. else if (customerData.type === 'deal') customerStats.deal += 1
  473. // 更新tabs计数
  474. tabs.forEach(tab => {
  475. if (tab.key === 'all') tab.count += 1
  476. else if (tab.key === customerData.type) tab.count += 1
  477. })
  478. // 显示提示
  479. showToastMessage('客户添加成功', 'success')
  480. }
  481. </script>
  482. <style scoped>
  483. .data-page { flex: 1; display: flex; flex-direction: column; padding: 24px; overflow-y: auto; background: var(--bg-dark); }
  484. .data-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
  485. .data-title { font-size: 20px; font-weight: 600; }
  486. .stat-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; margin-bottom: 24px; }
  487. .stat-card-lg { display: flex; align-items: center; gap: 14px; padding: 18px; background: var(--bg-card); border-radius: 14px; border: 1px solid var(--border); cursor: pointer; transition: all 0.2s; }
  488. .stat-card-lg:hover { border-color: var(--primary); }
  489. .stat-icon-lg { width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 20px; background: rgba(99, 102, 241, 0.2); color: var(--primary); }
  490. .stat-icon-lg.green { background: rgba(16, 185, 129, 0.2); color: var(--success); }
  491. .stat-icon-lg.purple { background: rgba(139, 92, 246, 0.2); color: var(--secondary); }
  492. .stat-icon-lg.orange { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
  493. .stat-content-lg h3 { font-size: 24px; font-weight: 700; }
  494. .stat-content-lg p { font-size: 12px; color: var(--text-secondary); }
  495. .search-box { position: relative; }
  496. .search-icon { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(--text-muted); }
  497. .search-input { width: 100%; padding: 10px 14px 10px 40px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px; color: var(--text-primary); font-size: 13px; }
  498. .search-input:focus { outline: none; border-color: var(--primary); }
  499. .form-select { background: var(--bg-card); border: 1px solid var(--border); color: var(--text-primary); }
  500. .form-select :deep(.el-input__wrapper){ background: var(--bg-card);}
  501. .customer-card { display: flex; align-items: center; gap: 16px; padding: 16px; background: var(--bg-card); border-radius: 12px; margin-bottom: 12px; border: 1px solid var(--border); cursor: pointer; transition: all 0.2s; }
  502. .customer-card:hover { border-color: var(--primary); }
  503. .customer-flag { font-size: 32px; }
  504. .customer-info { flex: 1; }
  505. .customer-header { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
  506. .customer-name { font-size: 15px; font-weight: 600; }
  507. .customer-status { font-size: 10px; padding: 2px 8px; border-radius: 10px; }
  508. .customer-contact { font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; }
  509. .customer-meta { display: flex; gap: 16px; font-size: 11px; color: var(--text-muted); }
  510. .customer-score { text-align: center; margin-right: 20px; }
  511. .score-circle { width: 50px; height: 50px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: 700; }
  512. .score-circle.high { background: rgba(16, 185, 129, 0.2); color: var(--success); }
  513. .score-circle.medium { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
  514. .score-circle.low { background: rgba(148, 163, 184, 0.2); color: var(--text-secondary); }
  515. .score-label { font-size: 10px; color: var(--text-muted); margin-top: 4px; }
  516. .customer-actions { display: flex; gap: 8px; }
  517. .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.7); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 20px; }
  518. .modal { background: var(--bg-card); border-radius: 16px; width: 100%; max-width: 600px; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column; }
  519. .customer-detail-modal { max-width: 800px; }
  520. .modal-header { display: flex; justify-content: space-between; align-items: center; padding: 20px; border-bottom: 1px solid var(--border); }
  521. .modal-title { display: flex; align-items: center; gap: 12px; font-size: 16px; font-weight: 600; }
  522. .modal-title h3 { font-size: 16px; font-weight: 600; margin: 0; }
  523. .modal-title p { font-size: 12px; color: var(--text-secondary); margin: 4px 0 0 0; }
  524. .detail-flag { font-size: 32px; }
  525. .modal-close { width: 32px; height: 32px; background: var(--bg-hover); border: none; border-radius: 8px; color: var(--text-secondary); cursor: pointer; display: flex; align-items: center; justify-content: center; }
  526. .modal-close:hover { background: var(--border); color: var(--text-primary); }
  527. .modal-body { flex: 1; overflow-y: auto; padding: 20px; }
  528. .modal-footer { display: flex; justify-content: flex-end; gap: 12px; padding: 20px; border-top: 1px solid var(--border); }
  529. .detail-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 24px; }
  530. .detail-stat-card { background: var(--bg-hover); border-radius: 12px; padding: 16px; text-align: center; }
  531. .detail-stat-value { font-size: 24px; font-weight: 700; margin-bottom: 4px; }
  532. .detail-stat-value.high { color: var(--success); }
  533. .detail-stat-value.medium { color: var(--warning); }
  534. .detail-stat-value.success { color: var(--success); }
  535. .detail-stat-label { font-size: 12px; color: var(--text-secondary); }
  536. .detail-section { margin-bottom: 24px; }
  537. .detail-section h4 { font-size: 14px; font-weight: 600; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
  538. .detail-section h4 i { color: var(--primary); }
  539. .contact-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
  540. .contact-item { display: flex; gap: 12px; padding: 12px; background: var(--bg-hover); border-radius: 10px; }
  541. .contact-item i { font-size: 16px; color: var(--primary); width: 20px; }
  542. .contact-label { font-size: 11px; color: var(--text-muted); margin-bottom: 2px; }
  543. .contact-value { font-size: 13px; font-weight: 500; }
  544. .contact-value.link { color: var(--primary); cursor: pointer; }
  545. .contact-sub { font-size: 11px; color: var(--text-secondary); }
  546. .ai-insight-cards { display: flex; flex-direction: column; gap: 10px; }
  547. .ai-insight-card { display: flex; gap: 12px; padding: 14px; background: var(--bg-hover); border-radius: 12px; border-left: 3px solid var(--primary); }
  548. .insight-icon { width: 36px; height: 36px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 14px; }
  549. .insight-icon.high { background: rgba(16, 185, 129, 0.2); color: var(--success); }
  550. .insight-icon.medium { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
  551. .insight-title { font-size: 13px; font-weight: 600; margin-bottom: 4px; }
  552. .insight-desc { font-size: 11px; color: var(--text-secondary); line-height: 1.5; }
  553. .timeline { display: flex; flex-direction: column; gap: 12px; position: relative; padding-left: 24px; }
  554. .timeline::before { content: ''; position: absolute; left: 6px; top: 0; bottom: 0; width: 2px; background: var(--border); }
  555. .timeline-item { position: relative; }
  556. .timeline-dot { position: absolute; left: -20px; top: 4px; width: 12px; height: 12px; border-radius: 50%; background: var(--primary); border: 2px solid var(--bg-card); }
  557. .timeline-dot.email { background: var(--primary); }
  558. .timeline-dot.call { background: var(--success); }
  559. .timeline-dot.meeting { background: var(--warning); }
  560. .timeline-dot.visit { background: var(--secondary); }
  561. .timeline-dot.order { background: var(--success); }
  562. .timeline-content { padding: 12px; background: var(--bg-hover); border-radius: 10px; }
  563. .timeline-header { display: flex; justify-content: space-between; margin-bottom: 4px; }
  564. .timeline-action { font-size: 13px; font-weight: 500; }
  565. .timeline-time { font-size: 11px; color: var(--text-muted); }
  566. .timeline-detail { font-size: 12px; color: var(--text-secondary); }
  567. .related-opps { display: flex; flex-direction: column; gap: 10px; }
  568. .related-opp { display: flex; justify-content: space-between; align-items: center; padding: 12px; background: var(--bg-hover); border-radius: 10px; }
  569. .opp-name { font-size: 13px; font-weight: 500; margin-bottom: 2px; }
  570. .opp-meta { font-size: 11px; color: var(--text-secondary); }
  571. .opp-status { font-size: 10px; padding: 3px 8px; border-radius: 10px; }
  572. .opp-status.negotiation { background: rgba(99, 102, 241, 0.2); color: var(--primary); }
  573. .opp-status.proposal { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
  574. .opp-status.won { background: rgba(16, 185, 129, 0.2); color: var(--success); }
  575. .toast { position: fixed; bottom: 24px; right: 24px; padding: 12px 20px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px; display: flex; align-items: center; gap: 10px; font-size: 13px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); z-index: 2000; }
  576. .toast.success { border-color: var(--success); }
  577. .toast.success i { color: var(--success); }
  578. .toast-enter-active, .toast-leave-active { transition: all 0.3s ease; }
  579. .toast-enter-from, .toast-leave-to { opacity: 0; transform: translateY(20px); }
  580. </style>