detail.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. <template>
  2. <el-drawer :model-value="modelValue" @update:model-value="val => $emit('update:modelValue', val)" size="80%" direction="rtl" destroy-on-close class="plan-detail-drawer" :with-header="false">
  3. <div class="drawer-header">
  4. <span class="header-title">拜访计划明细</span>
  5. <el-icon class="close-icon" @click="$emit('update:modelValue', false)"><Close /></el-icon>
  6. </div>
  7. <div class="drawer-body" v-loading="loading">
  8. <!-- 左侧主体内容 -->
  9. <div class="main-content">
  10. <div class="content-tabs">
  11. <div class="tab-item active">资料</div>
  12. </div>
  13. <div class="content-scroll">
  14. <!-- 基本信息 -->
  15. <div class="section-container">
  16. <div class="section-header-grey">
  17. <span class="title">基本信息</span>
  18. </div>
  19. <div class="info-grid-2">
  20. <div class="info-item"><span class="label">计划编号</span><span class="value">{{ detailData.planNo }}</span></div>
  21. <div class="info-item"><span class="label">拜访人</span><span class="value">{{ detailData.visitorName }}</span></div>
  22. <div class="info-item"><span class="label">开始时间</span><span class="value">{{ parseTime(detailData.startTime, '{y}-{m}-{d}') }}</span></div>
  23. <div class="info-item"><span class="label">结束时间</span><span class="value">{{ parseTime(detailData.endTime, '{y}-{m}-{d}') }}</span></div>
  24. <div class="info-item"><span class="label">状态</span>
  25. <span class="value">
  26. <template v-if="detailData.status === '0'">待审核</template>
  27. <template v-else-if="detailData.status === '1'">已审核</template>
  28. <template v-else-if="detailData.status === '2'">已驳回</template>
  29. </span>
  30. </div>
  31. </div>
  32. </div>
  33. <!-- 日程信息 -->
  34. <div class="section-container" style="margin-top: 20px;">
  35. <div class="section-header-grey" style="display: block; visibility: visible;">
  36. <span class="title" style="display: inline-block;">日程信息</span>
  37. </div>
  38. <el-table :data="scheduleData" border class="custom-table" style="margin-top: 15px;">
  39. <el-table-column label="拜访日期" align="center" prop="callDate" width="120" sortable>
  40. <template #default="scope">
  41. <span>{{ parseTime(scope.row.callDate, '{y}-{m}-{d}') }}</span>
  42. </template>
  43. </el-table-column>
  44. <el-table-column label="客户名称" align="center" prop="customerName" min-width="200" />
  45. <el-table-column label="关联对象" align="center" prop="objectName" min-width="120" />
  46. <el-table-column label="关联类型" align="center" prop="relevanceType" width="100">
  47. <template #default="scope">
  48. {{ scope.row.relevanceType === 0 ? '客户' : (scope.row.relevanceType === 1 ? '项目商机' : '年度入围') }}
  49. </template>
  50. </el-table-column>
  51. <el-table-column label="紧要程度" align="center" prop="importantLevel" width="100">
  52. <template #default="scope">
  53. {{ getUrgencyLabel(scope.row.importantLevel) }}
  54. </template>
  55. </el-table-column>
  56. <el-table-column label="拜访目的" align="center" prop="purposeVisit" min-width="150" show-overflow-tooltip />
  57. <el-table-column label="操作" align="center" width="100">
  58. <template #default="scope">
  59. <el-button link type="primary" @click="handleViewSchedule(scope.row)">查看</el-button>
  60. </template>
  61. </el-table-column>
  62. </el-table>
  63. </div>
  64. </div>
  65. </div>
  66. <!-- 右侧侧边栏 (通用业务活动组件) -->
  67. <div class="side-panel">
  68. <BusinessActivity
  69. :business-id="props.id"
  70. business-type="plan"
  71. :info-data="detailData"
  72. >
  73. <template #management>
  74. <div class="admin-grid">
  75. <div class="admin-cell">
  76. <span class="label">创建日期</span>
  77. <span class="value">{{ parseTime(detailData.createTime || detailData.createDate, '{y}-{m}-{d}') || '--' }}</span>
  78. </div>
  79. <div class="admin-cell">
  80. <span class="label">创建人</span>
  81. <span class="value">{{ detailData.createByName || detailData.createUserName || detailData.nickName || detailData.createBy || '--' }}</span>
  82. </div>
  83. <div class="admin-cell">
  84. <span class="label">修改日期</span>
  85. <span class="value">{{ parseTime(detailData.updateTime || detailData.updateDate, '{y}-{m}-{d}') || '--' }}</span>
  86. </div>
  87. <div class="admin-cell">
  88. <span class="label">最后修改人</span>
  89. <span class="value">{{ detailData.updateByName || detailData.updateUserName || detailData.updateBy || '--' }}</span>
  90. </div>
  91. <div class="admin-cell" v-if="detailData.auditByName || detailData.auditBy">
  92. <span class="label">审核人</span>
  93. <span class="value">{{ detailData.auditByName || detailData.auditBy || '--' }}</span>
  94. </div>
  95. <div class="admin-cell" v-if="detailData.auditTime">
  96. <span class="label">审核时间</span>
  97. <span class="value">{{ parseTime(detailData.auditTime, '{y}-{m}-{d}') || '--' }}</span>
  98. </div>
  99. <div class="admin-cell" v-if="detailData.deptName">
  100. <span class="label">所属部门</span>
  101. <span class="value">{{ detailData.deptName || '--' }}</span>
  102. </div>
  103. </div>
  104. </template>
  105. </BusinessActivity>
  106. </div>
  107. </div>
  108. <!-- 查看日程对话框 -->
  109. <el-dialog v-if="viewScheduleOpen" v-model="viewScheduleOpen" width="750px" append-to-body destroy-on-close class="view-schedule-dialog">
  110. <template #header>
  111. <span class="custom-dialog-header">查看日程</span>
  112. </template>
  113. <el-form :model="currentSchedule" label-width="110px" disabled class="view-form">
  114. <el-form-item label="关联类型:">
  115. <el-radio-group v-model="currentSchedule.relevanceType">
  116. <el-radio :label="0">客户</el-radio>
  117. <el-radio :label="1">项目商机</el-radio>
  118. <el-radio :label="2">年度入围</el-radio>
  119. </el-radio-group>
  120. </el-form-item>
  121. <el-form-item label="客户:">
  122. <el-input v-model="currentSchedule.customerName" />
  123. </el-form-item>
  124. <el-row :gutter="20">
  125. <el-col :span="12">
  126. <el-form-item label="拜访日期:">
  127. <el-date-picker v-model="currentSchedule.callDate" type="date" style="width: 100%" disabled format="YYYY-MM-DD" value-format="YYYY-MM-DD" />
  128. </el-form-item>
  129. </el-col>
  130. <el-col :span="12">
  131. <el-form-item label="紧要程度:">
  132. <el-select v-model="currentSchedule.importantLevel" placeholder="请选择" disabled style="width: 100%">
  133. <el-option v-for="dict in urgencyOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
  134. </el-select>
  135. </el-form-item>
  136. </el-col>
  137. </el-row>
  138. <el-form-item label="随访人:">
  139. <el-input v-model="currentSchedule.followPeopleName" />
  140. </el-form-item>
  141. <el-form-item label="拜访目的:">
  142. <el-input v-model="currentSchedule.purposeVisit" type="textarea" :rows="5" disabled />
  143. </el-form-item>
  144. </el-form>
  145. <template #footer>
  146. <div class="dialog-footer">
  147. <el-button @click="viewScheduleOpen = false">确 定</el-button>
  148. </div>
  149. </template>
  150. </el-dialog>
  151. </el-drawer>
  152. </template>
  153. <script setup>
  154. import { ref, reactive, computed, watch, getCurrentInstance, onMounted, toRefs } from 'vue';
  155. import { Close, Search, Plus, UserFilled } from '@element-plus/icons-vue';
  156. import { getPlan } from "@/api/visit/plan";
  157. import { listOperateLog } from "@/api/visit/operateLog";
  158. import BusinessActivity from '../../common/businessActivity.vue';
  159. const props = defineProps({
  160. modelValue: Boolean,
  161. id: [String, Number]
  162. });
  163. const emit = defineEmits(['update:modelValue']);
  164. const proxy = getCurrentInstance().proxy;
  165. const { importance_level: urgencyOptions } = toRefs(reactive(proxy.useDict("importance_level")));
  166. const loading = ref(false);
  167. const detailData = ref({});
  168. const scheduleData = ref([]);
  169. const activeSideTab = ref('team');
  170. // 日程详情相关
  171. const viewScheduleOpen = ref(false);
  172. const currentSchedule = ref({
  173. relatedType: '客户',
  174. customerName: '',
  175. visitDate: '',
  176. urgency: '',
  177. accompanyPersons: '',
  178. purpose: ''
  179. });
  180. watch(() => props.modelValue, (val) => {
  181. if (val && props.id) {
  182. getDetail();
  183. }
  184. });
  185. const getDetail = () => {
  186. loading.value = true;
  187. getPlan(props.id).then(response => {
  188. detailData.value = response.data;
  189. scheduleData.value = response.data.detailList || [];
  190. loading.value = false;
  191. }).catch(() => {
  192. loading.value = false;
  193. });
  194. };
  195. const handleViewSchedule = (row) => {
  196. const data = JSON.parse(JSON.stringify(row));
  197. if (data.importantLevel !== undefined && data.importantLevel !== null) {
  198. data.importantLevel = String(data.importantLevel);
  199. }
  200. currentSchedule.value = data;
  201. viewScheduleOpen.value = true;
  202. };
  203. const getUrgencyLabel = (val) => {
  204. const found = urgencyOptions.value.find(item => String(item.value) === String(val));
  205. return found ? found.label : (val || '--');
  206. };
  207. const getOperateTypeText = (type) => {
  208. const map = { 1: '添加', 2: '编辑', 3: '删除', 4: '审核', 5: '转移', 6: '分析' };
  209. return map[String(type)] || '操作';
  210. };
  211. const translateLogTarget = (details) => {
  212. if (!details) return '拜访计划';
  213. if (details.includes('日程')) return '日程';
  214. return '拜访计划';
  215. };
  216. onMounted(() => {
  217. });
  218. </script>
  219. <style scoped lang="scss">
  220. /* 此处应包含原 index.vue 中 .plan-detail-drawer, .drawer-header, .drawer-body, .side-panel 等样式 */
  221. .plan-detail-drawer {
  222. :deep(.el-drawer__body) { padding: 0; overflow: hidden; }
  223. }
  224. .drawer-header {
  225. height: 50px; display: flex; justify-content: space-between; align-items: center;
  226. padding: 0 20px; border-bottom: 1px solid #f0f0f0; background-color: #fff;
  227. .header-title { font-size: 16px; font-weight: normal; color: #333; }
  228. .close-icon { cursor: pointer; color: #999; font-size: 18px; &:hover { color: #409eff; } }
  229. }
  230. .drawer-body { height: calc(100vh - 50px); display: flex; }
  231. .main-content {
  232. flex: 1; display: flex; flex-direction: column; background-color: #fff; border-right: 1px solid #f0f0f0;
  233. min-width: 0;
  234. .content-tabs {
  235. padding: 0 20px; border-bottom: 1px solid #f2f2f2; height: 48px;
  236. .tab-item {
  237. display: inline-block; height: 48px; line-height: 48px; margin-right: 30px; color: #409eff; font-size: 14px; cursor: pointer;
  238. &.active { color: #409eff; font-weight: normal; position: relative;
  239. &::after { content: ""; position: absolute; bottom: 0; left: 0; width: 100%; height: 2px; background-color: #409eff; }
  240. }
  241. }
  242. }
  243. .content-scroll { flex: 1; overflow: auto; padding: 20px; }
  244. }
  245. .side-panel {
  246. width: 30%; flex-shrink: 0; background-color: #fff; display: flex; flex-direction: column;
  247. :deep(.activity-container) {
  248. .el-tabs__header {
  249. margin-top: 0;
  250. margin-bottom: 0;
  251. height: 48px;
  252. .el-tabs__nav-wrap {
  253. height: 100%;
  254. display: flex;
  255. align-items: center;
  256. }
  257. .el-tabs__item {
  258. font-weight: normal !important;
  259. }
  260. }
  261. }
  262. .side-tabs-header {
  263. display: flex; border-bottom: 1px solid #f1f5f9;
  264. .side-tab-item {
  265. flex: 1; text-align: center; padding: 16px 8px; font-size: 13px; color: #1d2129; cursor: pointer;
  266. &.active { color: #409eff; font-weight: normal; position: relative;
  267. &::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 32px; height: 3px; background: #409eff; }
  268. }
  269. }
  270. }
  271. .side-tab-content { flex: 1; overflow-y: auto; padding: 16px; }
  272. }
  273. .section-header-grey { background-color: #f5f7fa; padding: 10px 15px; margin-bottom: 5px; .title { font-size: 14px; font-weight: normal; color: #409eff; } }
  274. .info-grid-2 {
  275. display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px 40px; padding: 15px;
  276. .info-item { display: flex; font-size: 13px; .label { color: #8c8c8c; width: 80px; flex-shrink: 0; } .value { color: #262626; } }
  277. }
  278. .admin-grid {
  279. display: grid;
  280. grid-template-columns: repeat(2, 1fr);
  281. gap: 12px 8px;
  282. padding: 15px 5px;
  283. .admin-cell {
  284. display: flex;
  285. align-items: center;
  286. font-size: 13px;
  287. .label { color: #86909c; margin-right: 6px; flex-shrink: 0; }
  288. .value { color: #1d2129; word-break: break-all; }
  289. }
  290. }
  291. </style>
  292. <!-- 非作用域样式,用于覆盖 append-to-body 的弹窗 -->
  293. <style lang="scss">
  294. .view-schedule-dialog {
  295. .el-dialog__header {
  296. background-color: #fff !important;
  297. border-bottom: 1px solid #f0f0f0;
  298. .custom-dialog-header {
  299. height: 45px;
  300. line-height: 45px;
  301. padding-left: 20px;
  302. color: #333 !important;
  303. font-size: 16px;
  304. display: block;
  305. font-weight: normal !important;
  306. }
  307. .el-dialog__headerbtn {
  308. .el-dialog__close {
  309. color: #909399 !important;
  310. &:hover { color: #409eff !important; }
  311. }
  312. }
  313. }
  314. .el-dialog__body {
  315. padding: 25px 20px 10px;
  316. }
  317. .el-form-item__label, .el-input__inner, .el-textarea__inner, .el-radio__label {
  318. font-weight: normal !important;
  319. color: #333 !important;
  320. }
  321. /* 禁用状态文字颜色增强 */
  322. .el-input.is-disabled .el-input__inner,
  323. .el-textarea.is-disabled .el-textarea__inner {
  324. color: #606266 !important;
  325. -webkit-text-fill-color: #606266 !important;
  326. }
  327. }
  328. </style>