lusa 1 місяць тому
батько
коміт
8174efd4dc
100 змінених файлів з 8470 додано та 197 видалено
  1. 71 0
      ui/sp-user-center/docs/otr-migration-gap-and-qa.md
  2. 45 0
      ui/sp-user-center/docs/sp-tems-ui-migration-plan.md
  3. 12 0
      ui/sp-user-center/src/api/emails/index.ts
  4. 5 0
      ui/sp-user-center/src/api/otr/ai/core.ts
  5. 21 0
      ui/sp-user-center/src/api/otr/award/core.ts
  6. 57 0
      ui/sp-user-center/src/api/otr/effect/core.ts
  7. 5 0
      ui/sp-user-center/src/api/otr/notice/core.ts
  8. 5 0
      ui/sp-user-center/src/api/otr/report/core.ts
  9. 37 0
      ui/sp-user-center/src/api/otr/summary/core.ts
  10. 61 0
      ui/sp-user-center/src/api/otr/task/core.ts
  11. 72 0
      ui/sp-user-center/src/api/portalMenu.ts
  12. BIN
      ui/sp-user-center/src/assets/images/home/home1.png
  13. BIN
      ui/sp-user-center/src/assets/images/home/home2.png
  14. BIN
      ui/sp-user-center/src/assets/images/home/home3.png
  15. BIN
      ui/sp-user-center/src/assets/images/home/home4.png
  16. BIN
      ui/sp-user-center/src/assets/images/home/home5.png
  17. 7 2
      ui/sp-user-center/src/assets/styles/sidebar.scss
  18. 2 2
      ui/sp-user-center/src/components/common/createTask.vue
  19. 1 1
      ui/sp-user-center/src/components/common/quotationDetail.vue
  20. 84 0
      ui/sp-user-center/src/enums/ModuleEnum.ts
  21. 11 2
      ui/sp-user-center/src/layout/components/AppMain.vue
  22. 120 0
      ui/sp-user-center/src/layout/components/ModuleTabs.vue
  23. 44 3
      ui/sp-user-center/src/layout/components/Sidebar/index.vue
  24. 1 1
      ui/sp-user-center/src/layout/components/notice/SetMsgTools.vue
  25. 54 186
      ui/sp-user-center/src/layout/index.vue
  26. 677 0
      ui/sp-user-center/src/mock/httpMock.ts
  27. 55 0
      ui/sp-user-center/src/modules/_shared/components/ModuleStub.vue
  28. 56 0
      ui/sp-user-center/src/modules/otr/_shared/components/OtrSimpleFormScene.vue
  29. 117 0
      ui/sp-user-center/src/modules/otr/_shared/components/OtrSimpleTableWorkspace.vue
  30. 13 0
      ui/sp-user-center/src/modules/otr/_shared/views/RoutePlaceholder.vue
  31. 24 0
      ui/sp-user-center/src/modules/otr/ai/views/AddAi.vue
  32. 256 0
      ui/sp-user-center/src/modules/otr/award/components/AwardWorkspace.vue
  33. 7 0
      ui/sp-user-center/src/modules/otr/award/views/List.vue
  34. 7 0
      ui/sp-user-center/src/modules/otr/award/views/Pool.vue
  35. 7 0
      ui/sp-user-center/src/modules/otr/award/views/Trends.vue
  36. 19 0
      ui/sp-user-center/src/modules/otr/constants/targetMeta.ts
  37. 225 0
      ui/sp-user-center/src/modules/otr/effect/components/EffectWorkspace.vue
  38. 7 0
      ui/sp-user-center/src/modules/otr/effect/views/Backlog.vue
  39. 7 0
      ui/sp-user-center/src/modules/otr/effect/views/Board.vue
  40. 7 0
      ui/sp-user-center/src/modules/otr/effect/views/ContentForm.vue
  41. 14 0
      ui/sp-user-center/src/modules/otr/effect/views/Dept.vue
  42. 15 0
      ui/sp-user-center/src/modules/otr/effect/views/Detail.vue
  43. 14 0
      ui/sp-user-center/src/modules/otr/effect/views/Evaluated.vue
  44. 15 0
      ui/sp-user-center/src/modules/otr/effect/views/Index.vue
  45. 7 0
      ui/sp-user-center/src/modules/otr/effect/views/Manage.vue
  46. 7 0
      ui/sp-user-center/src/modules/otr/effect/views/Mine.vue
  47. 14 0
      ui/sp-user-center/src/modules/otr/effect/views/Objection.vue
  48. 15 0
      ui/sp-user-center/src/modules/otr/effect/views/Person.vue
  49. 14 0
      ui/sp-user-center/src/modules/otr/effect/views/Review.vue
  50. 14 0
      ui/sp-user-center/src/modules/otr/effect/views/Setting.vue
  51. 7 0
      ui/sp-user-center/src/modules/otr/effect/views/Template.vue
  52. 7 0
      ui/sp-user-center/src/modules/otr/effect/views/TemplateForm.vue
  53. 14 0
      ui/sp-user-center/src/modules/otr/effect/views/Whitelist.vue
  54. 20 0
      ui/sp-user-center/src/modules/otr/notice/views/List.vue
  55. 21 0
      ui/sp-user-center/src/modules/otr/report/views/PersonReport.vue
  56. 96 0
      ui/sp-user-center/src/modules/otr/summary/components/SummaryFormScene.vue
  57. 205 0
      ui/sp-user-center/src/modules/otr/summary/components/SummaryWorkspace.vue
  58. 7 0
      ui/sp-user-center/src/modules/otr/summary/views/Announcement.vue
  59. 7 0
      ui/sp-user-center/src/modules/otr/summary/views/Create.vue
  60. 7 0
      ui/sp-user-center/src/modules/otr/summary/views/Draft.vue
  61. 7 0
      ui/sp-user-center/src/modules/otr/summary/views/Form.vue
  62. 7 0
      ui/sp-user-center/src/modules/otr/summary/views/List.vue
  63. 7 0
      ui/sp-user-center/src/modules/otr/summary/views/Member.vue
  64. 7 0
      ui/sp-user-center/src/modules/otr/summary/views/RemindTask.vue
  65. 7 0
      ui/sp-user-center/src/modules/otr/summary/views/Settings.vue
  66. 7 0
      ui/sp-user-center/src/modules/otr/summary/views/Share.vue
  67. 7 0
      ui/sp-user-center/src/modules/otr/summary/views/ShareDetail.vue
  68. 7 0
      ui/sp-user-center/src/modules/otr/summary/views/Statistics.vue
  69. 199 0
      ui/sp-user-center/src/modules/otr/task/components/TaskWorkspace.vue
  70. 7 0
      ui/sp-user-center/src/modules/otr/task/views/AtList.vue
  71. 137 0
      ui/sp-user-center/src/modules/otr/task/views/Detail.vue
  72. 85 0
      ui/sp-user-center/src/modules/otr/task/views/Form.vue
  73. 7 0
      ui/sp-user-center/src/modules/otr/task/views/List.vue
  74. 7 0
      ui/sp-user-center/src/modules/otr/task/views/Member.vue
  75. 7 0
      ui/sp-user-center/src/modules/otr/task/views/Table.vue
  76. 173 0
      ui/sp-user-center/src/modules/sales/views/bulletin/add.vue
  77. 278 0
      ui/sp-user-center/src/modules/sales/views/bulletin/comment.vue
  78. 421 0
      ui/sp-user-center/src/modules/sales/views/bulletin/detail.vue
  79. 355 0
      ui/sp-user-center/src/modules/sales/views/bulletin/detail2.vue
  80. 132 0
      ui/sp-user-center/src/modules/sales/views/bulletin/index.vue
  81. 10 0
      ui/sp-user-center/src/modules/sales/views/business/allBusiness/index.vue
  82. 285 0
      ui/sp-user-center/src/modules/sales/views/business/allBusinessDetailMenu.sql
  83. 313 0
      ui/sp-user-center/src/modules/sales/views/business/allBusinessMenu.sql
  84. 109 0
      ui/sp-user-center/src/modules/sales/views/business/component/Addcontact.vue
  85. 498 0
      ui/sp-user-center/src/modules/sales/views/business/component/CommonBusiness.vue
  86. 163 0
      ui/sp-user-center/src/modules/sales/views/business/component/CostList.vue
  87. 692 0
      ui/sp-user-center/src/modules/sales/views/business/component/GetProductPrice.vue
  88. 215 0
      ui/sp-user-center/src/modules/sales/views/business/component/Information.vue
  89. 277 0
      ui/sp-user-center/src/modules/sales/views/business/component/QuotationList.vue
  90. 90 0
      ui/sp-user-center/src/modules/sales/views/business/component/TurnBus.vue
  91. 403 0
      ui/sp-user-center/src/modules/sales/views/business/component/add.vue
  92. 540 0
      ui/sp-user-center/src/modules/sales/views/business/component/detail.vue
  93. 187 0
      ui/sp-user-center/src/modules/sales/views/business/component/skuDetail.vue
  94. 10 0
      ui/sp-user-center/src/modules/sales/views/business/lossBusiness/index.vue
  95. 43 0
      ui/sp-user-center/src/modules/sales/views/business/lossBusinessMenu.sql
  96. 10 0
      ui/sp-user-center/src/modules/sales/views/business/mineBusiness/index.vue
  97. 0 0
      ui/sp-user-center/src/modules/sales/views/business/mineBusinessDetailMenu.sql
  98. 32 0
      ui/sp-user-center/src/modules/sales/views/business/mineBusinessMenu.sql
  99. 10 0
      ui/sp-user-center/src/modules/sales/views/business/myCollaborateBusiness/index.vue
  100. 0 0
      ui/sp-user-center/src/modules/sales/views/business/myCollaborateBusinessDetailMenu.sql

+ 71 - 0
ui/sp-user-center/docs/otr-migration-gap-and-qa.md

@@ -0,0 +1,71 @@
+# OTR 迁移差异与回归清单
+
+## 一、与旧系统对照差异(当前阶段)
+
+说明:当前已完成“路由可达 + 核心筛选表格 + 基础表单动作 + mock 联调”;以下是仍需逐步对齐旧系统的能力。
+
+### 1) target
+- 已完成:列表、模板、报告、审批/草稿/看板等页面路由与基础交互。
+- 待对齐:
+  - 目标树(旧 `vue-okr-tree`)的完整结构与节点操作。
+  - 模板/目标的复杂弹窗与批量操作。
+  - 审批流节点级动作与历史日志联动。
+
+### 2) effect
+- 已完成:`manage/mine/board/backlog/template` 统一工作台,深层子页均可访问。
+- 待对齐:
+  - 绩效评审、异议管理、白名单业务规则(状态机)完整动作。
+  - 看板统计图表与导出能力。
+
+### 3) task
+- 已完成:四视图联动(我的/成员/@我/看板)、详情查询、新建提交。
+- 待对齐:
+  - 日/周/月日历视图与拖拽排期。
+  - 任务日志、评论、点赞、标签、附件、关联 KR 的细分动作。
+  - 批量删除、批量编辑权重等高级操作。
+
+### 4) summary
+- 已完成:工作台筛选(关键词/类型/状态/时间)、表单新增编辑、分享详情。
+- 待对齐:
+  - 审阅通过/驳回动作流、提醒与统计卡片。
+  - 评论/点赞/置顶等互动行为。
+
+### 5) award / notice / report / ai
+- 已完成:可用入口 + 列表联动,award 已有新增与详情弹窗。
+- 待对齐:
+  - 奖金池调整、奖励审批流细节。
+  - 通知动作与回调场景。
+  - 报表图表化和导出。
+
+## 二、回归清单(每次批次完成后执行)
+
+### A. 基础路由与菜单
+- OTR 左侧所有二级菜单可见、可点击、无 404。
+- 页面刷新后(深链)仍可访问,不跳错路由。
+
+### B. 列表页
+- 查询条件:关键词、状态、日期范围可生效。
+- 分页:页码/每页条数切换正常。
+- 空数据与异常兜底展示正常。
+
+### C. 表单页
+- 新建保存成功提示。
+- 编辑回填正确,保存后状态更新。
+- 必填校验可阻止空提交。
+
+### D. 详情页
+- 携带 `id` 跳转可加载详情。
+- 无 `id` 时不崩溃,有兜底提示/默认值。
+
+### E. Mock 与真实接口切换
+- mock 开启时页面可完整联调。
+- 接口可达后字段兼容,无控制台报错。
+
+### F. 稳定性
+- `ReadLints` 无新增错误。
+- 控制台无明显运行时异常(红色报错)。
+
+## 三、建议下一批优先级
+1. `task` 日历与高级操作(评论/日志/附件/标签)。
+2. `summary` 审阅与互动链路。
+3. `effect` 深层状态流(review/objection/whitelist)。

+ 45 - 0
ui/sp-user-center/docs/sp-tems-ui-migration-plan.md

@@ -0,0 +1,45 @@
+# sp-tems-ui 迁移升级计划(Vue3 + TS + Element Plus)
+
+## 目标与边界
+- 迁移源仅使用 `D:/workerspace/sp-tems/ui/sp-tems-ui`。
+- 不再继续使用旧 `sp-user-center` 业务页面作为迁移来源。
+- 统一落地到当前新项目(Vite + Vue3 + TS + Element Plus)中的 OTR 模块。
+- API 路径保持不变;接口不可达时继续走 mock/fallback。
+
+## 模块拆分与优先级
+1. P0:`target`、`effect`、`task`(核心业务)
+2. P1:`summary`、`notice`、`award`、`report`、`ai`
+3. P2:跨模块公共能力(utils、widgets、样式、权限点)
+
+## 迁移方法
+1. 路由对齐:按 `sp-tems-ui/src/router/router.js` 建立 OTR 路由映射。
+2. 页面迁移:Vue2 Options API -> Vue3 `<script setup lang="ts">`。
+3. 组件替换:
+   - `element-ui` / `view-design` -> `element-plus`
+   - 事件、插槽、`v-model`、表单校验规则按 Vue3 写法改造
+4. 状态改造:`vuex` 依赖迁到 Pinia store(按业务切 store)。
+5. 公共依赖替换:移除 `sp-common`,在新项目复用或重写等价工具。
+
+## 分批执行清单
+### Batch A(已开始)
+- 移除 OTR 中旧 `sp-user-center` 系统管理路由与页面(避免混源)。
+- OTR 默认入口改为 `target`(`/otr/target/mine`)。
+
+### Batch B(进行中)
+- 迁移 `target` 核心页面:`company/depart/mine/targetTemplate/report`。
+- 先保证路由可达 + 页面可渲染,再逐页恢复交互。
+
+### Batch C
+- 迁移 `effect` 核心页面:`manage/mine/board/backlog/template`。
+
+### Batch D
+- 迁移 `task` 与 `summary` 主页面,补齐 widgets 依赖。
+
+### Batch E
+- 迁移 `notice/award/report/ai`,统一样式与权限。
+
+## 验收标准
+- 模块路由全部可访问,无 404/白屏。
+- 主流程可跑通(查询、分页、详情、保存)。
+- 无新增 lint 报错;关键页面控制台无运行时异常。
+- 关键 API 路径与旧系统一致。

+ 12 - 0
ui/sp-user-center/src/api/emails/index.ts

@@ -336,6 +336,18 @@ export const fileDownload = (query: any, type?: string) => {
     });
 };
 
+/**
+ * 预览附件URL(给 window.open 使用)
+ * 注意:保持与旧 sales 项目一致的 /sales 前缀,走 vite proxy。
+ */
+export const filePreviewUrl = (id: string | number, type?: string) => {
+    return (
+        '/sales/mail/' +
+        ((type == 'draft' || type == 'senting') ? 'draft_download' : 'download') +
+        '?attachmentId=' + id
+    );
+};
+
 /**
  * 打包下载附件
  * @param query

+ 5 - 0
ui/sp-user-center/src/api/otr/ai/core.ts

@@ -0,0 +1,5 @@
+import request from '@/utils/request'
+
+export function aiToolList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/ai/tool/list', params })
+}

+ 21 - 0
ui/sp-user-center/src/api/otr/award/core.ts

@@ -0,0 +1,21 @@
+import request from '@/utils/request'
+
+export function awardList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/award/list', params })
+}
+
+export function awardPoolList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/award/pool/list', params })
+}
+
+export function awardTrendsList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/award/trends/list', params })
+}
+
+export function awardGet(id: string | number) {
+  return request({ method: 'get', url: '/award/get', params: { id } })
+}
+
+export function awardAdd(data: Record<string, any>) {
+  return request({ method: 'post', url: '/award/add', data })
+}

+ 57 - 0
ui/sp-user-center/src/api/otr/effect/core.ts

@@ -0,0 +1,57 @@
+import request from '@/utils/request'
+
+export function effectManageList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/effect/manage/list', params })
+}
+
+export function effectMineList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/effect/mine/list', params })
+}
+
+export function effectBoardList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/effect/board/list', params })
+}
+
+export function effectBacklogList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/effect/backlog/list', params })
+}
+
+export function effectTemplateList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/effect/template/list', params })
+}
+
+export function effectSettingList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/effect/setting/list', params })
+}
+
+export function effectDetailList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/effect/detail/list', params })
+}
+
+export function effectIndexList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/effect/index/list', params })
+}
+
+export function effectDeptList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/effect/dept/list', params })
+}
+
+export function effectPersonList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/effect/person/list', params })
+}
+
+export function effectEvaluatedList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/effect/evaluated/list', params })
+}
+
+export function effectReviewList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/effect/review/list', params })
+}
+
+export function effectObjectionList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/effect/objection/list', params })
+}
+
+export function effectWhitelistList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/effect/whitelist/list', params })
+}

+ 5 - 0
ui/sp-user-center/src/api/otr/notice/core.ts

@@ -0,0 +1,5 @@
+import request from '@/utils/request'
+
+export function noticeList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/notice/list', params })
+}

+ 5 - 0
ui/sp-user-center/src/api/otr/report/core.ts

@@ -0,0 +1,5 @@
+import request from '@/utils/request'
+
+export function personReportList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/report/person/list', params })
+}

+ 37 - 0
ui/sp-user-center/src/api/otr/summary/core.ts

@@ -0,0 +1,37 @@
+import request from '@/utils/request'
+
+export function summaryList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/summary/list', params })
+}
+
+export function summaryMemberList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/summary/member/list', params })
+}
+
+export function summaryStatisticsList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/summary/statistics/list', params })
+}
+
+export function summaryDraftList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/summary/draft/list', params })
+}
+
+export function summaryShareList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/summary/share/list', params })
+}
+
+export function summaryAnnouncementList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/summary/announcement/list', params })
+}
+
+export function summaryGet(id: string | number) {
+  return request({ method: 'get', url: '/summary/get', params: { id } })
+}
+
+export function summaryAdd(data: Record<string, any>) {
+  return request({ method: 'post', url: '/summary/add', data })
+}
+
+export function summaryEdit(data: Record<string, any>) {
+  return request({ method: 'put', url: '/summary/edit', data })
+}

+ 61 - 0
ui/sp-user-center/src/api/otr/task/core.ts

@@ -0,0 +1,61 @@
+import request from '@/utils/request'
+
+export function taskList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/task/list', params })
+}
+
+export function taskMemberList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/task/member/list', params })
+}
+
+export function taskBoardList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/task/board/list', params })
+}
+
+export function taskDetailList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/task/detail/list', params })
+}
+
+export function taskAtList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/task/at/list', params })
+}
+
+export function taskListByTable(params: Record<string, any>) {
+  return request({ method: 'get', url: '/task/listByTable', params })
+}
+
+export function taskListMineGantt(params: Record<string, any>) {
+  return request({ method: 'get', url: '/task/list-mine-gantt', params })
+}
+
+export function taskListSubordinateGantt(params: Record<string, any>) {
+  return request({ method: 'get', url: '/task/list-subordinate-gantt', params })
+}
+
+export function taskListAtMine(params: Record<string, any>) {
+  return request({ method: 'get', url: '/task/at-mine-list', params })
+}
+
+export function taskGet(taskDateId: string | number) {
+  return request({ method: 'get', url: '/task/get', params: { taskDateId } })
+}
+
+export function taskAdd(data: Record<string, any>) {
+  return request({ method: 'post', url: '/task/add', data })
+}
+
+export function taskLogList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/task/log/list', params })
+}
+
+export function taskCommentList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/task/comment/list', params })
+}
+
+export function taskCommentAdd(data: Record<string, any>) {
+  return request({ method: 'post', url: '/task/comment/add', data })
+}
+
+export function taskResourceList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/task/resource/list', params })
+}

+ 72 - 0
ui/sp-user-center/src/api/portalMenu.ts

@@ -0,0 +1,72 @@
+import request from '@/utils/request'
+import { ModuleKey } from '@/enums/ModuleEnum'
+
+export interface PortalMenuItem {
+  id: string
+  title: string
+  path: string
+  icon?: string
+  children?: PortalMenuItem[]
+}
+
+export interface PortalModuleMenu {
+  module: ModuleKey
+  commonMenus: PortalMenuItem[]
+  moduleMenus: PortalMenuItem[]
+}
+
+/**
+ * 新门户菜单接口(后端待提供)
+ * 约定:返回“模块 + 菜单树”聚合结构,替代旧查询菜单接口
+ */
+export const fetchPortalMenus = () => {
+  return request({
+    url: '/sys/portal/menu/tree',
+    method: 'post',
+  })
+}
+
+/**
+ * 接口未就绪时的本地 mock,方便先跑通导航壳。
+ */
+export const getPortalMenusMock = (): PortalModuleMenu[] => ([
+  {
+    module: ModuleKey.HOME,
+    commonMenus: [
+      { id: 'home-dashboard', title: '首页', path: '/home/dashboard', icon: 'dashboard' },
+      { id: 'home-otr-role', title: '角色管理', path: '/otr/system/role', icon: 'peoples' },
+      { id: 'home-sales-index', title: '销售首页', path: '/index', icon: 'chart' },
+    ],
+    moduleMenus: [],
+  },
+  {
+    module: ModuleKey.OTR,
+    commonMenus: [],
+    moduleMenus: [
+      {
+        id: 'otr-system',
+        title: '系统管理',
+        path: '/otr/system',
+        icon: 'system',
+        children: [
+          { id: 'otr-system-user', title: '员工管理', path: '/otr/system/user', icon: 'user' },
+          { id: 'otr-system-role', title: '角色管理', path: '/otr/system/role', icon: 'peoples' },
+          { id: 'otr-system-menu', title: '菜单管理', path: '/otr/system/menu', icon: 'tree-table' },
+        ],
+      },
+    ],
+  },
+  {
+    module: ModuleKey.SALES,
+    commonMenus: [],
+    moduleMenus: [
+      { id: 'sales-index', title: '销售首页', path: '/index', icon: 'dashboard' },
+      { id: 'sales-clue-add', title: '新增线索', path: '/clue/add', icon: 'edit' },
+      { id: 'sales-customer-add', title: '创建客户', path: '/customer/addCustomer', icon: 'user' },
+    ],
+  },
+  { module: ModuleKey.PROJECT, commonMenus: [], moduleMenus: [] },
+  { module: ModuleKey.SALARY, commonMenus: [], moduleMenus: [] },
+  { module: ModuleKey.WEBSITE, commonMenus: [], moduleMenus: [] },
+])
+

BIN
ui/sp-user-center/src/assets/images/home/home1.png


BIN
ui/sp-user-center/src/assets/images/home/home2.png


BIN
ui/sp-user-center/src/assets/images/home/home3.png


BIN
ui/sp-user-center/src/assets/images/home/home4.png


BIN
ui/sp-user-center/src/assets/images/home/home5.png


+ 7 - 2
ui/sp-user-center/src/assets/styles/sidebar.scss

@@ -4,10 +4,13 @@
     transition: margin-left 0.28s;
     margin-left: $base-sidebar-width;
     position: relative;
+    width: calc(100% - #{$base-sidebar-width});
+    min-width: 0;
   }
 
   .sidebarHide {
     margin-left: 0 !important;
+    width: 100% !important;
   }
 
   .sidebar-container {
@@ -15,10 +18,10 @@
     transition: width 0.28s;
     width: $base-sidebar-width !important;
     background-color: $base-menu-background;
-    height: 100%;
+    height: calc(100% - 50px);
     position: fixed;
     font-size: 0;
-    top: 0;
+    top: 50px;
     bottom: 0;
     left: 0;
     z-index: 1001;
@@ -139,6 +142,7 @@
 
     .main-container {
       margin-left: 54px;
+      width: calc(100% - 54px);
     }
 
     .sub-menu-title-noDropdown {
@@ -196,6 +200,7 @@
   .mobile {
     .main-container {
       margin-left: 0px;
+      width: 100%;
     }
 
     .sidebar-container {

+ 2 - 2
ui/sp-user-center/src/components/common/createTask.vue

@@ -124,7 +124,7 @@
     const buttonLoading = ref(false)
 
     const taskFormRef = ref<ElFormInstance>();
-    const { userInfo } = useUserStore();
+    const userStore = useUserStore();
     const initFormData: TaskForm = {
         id: undefined,
         taskBeginTime: undefined,
@@ -142,7 +142,7 @@
         remindType: '30',
         remindModels: undefined,
         remindTime: undefined,
-        ownerBy: userInfo.id,
+        ownerBy: (userStore.userId as any) || (userStore.userInfo as any)?.id,
         isDelete: undefined,
         sort: undefined,
         enabled: undefined,

+ 1 - 1
ui/sp-user-center/src/components/common/quotationDetail.vue

@@ -7,7 +7,7 @@
 </template>
 
 <script setup lang="ts">
-import QuotationList from '@/views/business/component/QuotationList.vue'
+import QuotationList from '@/modules/sales/views/business/component/QuotationList.vue'
     const visible = ref()
     const quotationListRef = ref()
     const paramsData = ref()

+ 84 - 0
ui/sp-user-center/src/enums/ModuleEnum.ts

@@ -0,0 +1,84 @@
+/**
+ * 顶部大模块定义
+ * 与路由 meta.module、菜单 module 字段、useAppStore.activeModule 联动
+ */
+export enum ModuleKey {
+  HOME = 'home',
+  OTR = 'otr',
+  SALES = 'sales',
+  PROJECT = 'project',
+  SALARY = 'salary',
+  WEBSITE = 'website',
+}
+
+export interface ModuleDefinition {
+  key: ModuleKey
+  /** 顶部 tab 显示文案 */
+  title: string
+  /** 用于路径反查的根前缀(如 /home /otr /salary) */
+  path: string
+  /** 点击 tab 时实际跳转的入口路径(默认为 path,可被 entry 覆盖) */
+  entry?: string
+  /** 是否本期已实现,false 走占位页 */
+  implemented: boolean
+  /** 顶部 tab 图标(element plus icon name 或 svg name),可选 */
+  icon?: string
+  /** 该模块名下的所有路径前缀(用于路径反查 module,仅 sales 需要因为它没有统一前缀)*/
+  pathPrefixes?: string[]
+}
+
+export const MODULE_LIST: ModuleDefinition[] = [
+  { key: ModuleKey.HOME,    title: '首页', path: '/home',    entry: '/home/dashboard',  implemented: true  },
+  { key: ModuleKey.OTR,     title: 'OTR',  path: '/otr',     entry: '/otr/target/mine', implemented: true  },
+  // sales 模块迁自 sp-sales-management,保留原始路径(/index, /clue/*, /customer/* 等),
+  // 因此用 pathPrefixes 显式列出销售业务下所有顶级路径,用于反查 module。
+  {
+    key: ModuleKey.SALES,
+    title: '销售',
+    path: '/index',
+    entry: '/index',
+    implemented: true,
+    pathPrefixes: [
+      '/index',
+      '/clue',
+      '/customer',
+      '/business',
+      '/order',
+      '/orderForm',
+      '/email',
+      '/finance',
+      '/refundRecord',
+      '/statistics',
+      '/bulletin',
+      '/task',
+      '/target',
+      '/layout/notice',
+      '/system/updatePwd',
+      '/user',
+    ],
+  },
+  { key: ModuleKey.PROJECT, title: '项目', path: '/project', implemented: false },
+  { key: ModuleKey.SALARY,  title: '工资', path: '/salary',  implemented: false },
+  { key: ModuleKey.WEBSITE, title: '官网', path: '/website', implemented: false },
+]
+
+/** 通过路径匹配模块(用于路由切换时反查所属模块;仅作 fallback,优先用 route.meta.module) */
+export function resolveModuleByPath(path: string): ModuleKey {
+  if (!path) return ModuleKey.HOME
+  // 优先精确/前缀匹配(包含 pathPrefixes,按长度倒序避免短前缀误匹配)
+  const candidates: Array<{ key: ModuleKey; prefix: string }> = []
+  for (const m of MODULE_LIST) {
+    candidates.push({ key: m.key, prefix: m.path })
+    if (m.pathPrefixes) {
+      for (const p of m.pathPrefixes) candidates.push({ key: m.key, prefix: p })
+    }
+  }
+  candidates.sort((a, b) => b.prefix.length - a.prefix.length)
+  const hit = candidates.find(c => path === c.prefix || path.startsWith(c.prefix + '/'))
+  return hit?.key ?? ModuleKey.HOME
+}
+
+/** 通过 key 取定义 */
+export function getModuleDefinition(key: ModuleKey): ModuleDefinition | undefined {
+  return MODULE_LIST.find(m => m.key === key)
+}

+ 11 - 2
ui/sp-user-center/src/layout/components/AppMain.vue

@@ -34,10 +34,13 @@ watch(()=> useSettingsStore().animationEnable, (val) => {
 
 <style lang="scss" scoped>
 .app-main {
-  /* 50= navbar  50  */
   width: 100%;
+  height: 100%;
   position: relative;
-  overflow: hidden;
+  overflow: auto;
+  box-sizing: border-box;
+  padding: 16px;
+  background-color: var(--el-bg-color-page);
 }
 
 .fixed-header+.app-main {
@@ -57,6 +60,12 @@ watch(()=> useSettingsStore().animationEnable, (val) => {
     padding-top: 84px;
   }
 }
+
+@media (max-width: 992px) {
+  .app-main {
+    padding: 12px;
+  }
+}
 </style>
 <style lang="scss">
 // fix css style bug in open el-dialog

+ 120 - 0
ui/sp-user-center/src/layout/components/ModuleTabs.vue

@@ -0,0 +1,120 @@
+<template>
+    <div class="module-tabs">
+        <div class="module-tabs__logo">
+            <img src="@/assets/images/home/logo@2x.png" alt="logo" />
+        </div>
+        <ul class="module-tabs__list">
+            <li
+                v-for="m in MODULE_LIST"
+                :key="m.key"
+                class="module-tabs__item"
+                :class="{
+                    'is-active': activeKey === m.key,
+                    'is-disabled': !m.implemented,
+                }"
+                @click="onClick(m)"
+            >
+                {{ m.title }}
+            </li>
+        </ul>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { storeToRefs } from 'pinia'
+import useAppStore from '@/store/modules/app'
+import {
+    MODULE_LIST,
+    ModuleKey,
+    type ModuleDefinition,
+    resolveModuleByPath,
+} from '@/enums/ModuleEnum'
+
+const appStore = useAppStore()
+const router = useRouter()
+const route = useRoute()
+
+const { activeModule } = storeToRefs(appStore)
+
+// 当前激活:优先以 store 中的 activeModule 为准;store 未初始化时按当前路径反查
+const activeKey = computed<ModuleKey>(() => {
+    return activeModule.value || resolveModuleByPath(route.path)
+})
+
+const onClick = (m: ModuleDefinition) => {
+    appStore.setActiveModule(m.key)
+    router.push(m.entry || m.path)
+}
+</script>
+
+<style lang="scss" scoped>
+.module-tabs {
+    display: flex;
+    align-items: center;
+    height: 50px;
+    padding: 0 20px;
+    background: linear-gradient(90deg, #1e88e5 0%, #42a5f5 100%);
+    color: #fff;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+    user-select: none;
+
+    &__logo {
+        display: flex;
+        align-items: center;
+        margin-right: 32px;
+
+        img {
+            height: 28px;
+            display: block;
+        }
+    }
+
+    &__list {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        list-style: none;
+        margin: 0;
+        padding: 0;
+        height: 100%;
+    }
+
+    &__item {
+        height: 100%;
+        padding: 0 18px;
+        display: flex;
+        align-items: center;
+        font-size: 14px;
+        cursor: pointer;
+        position: relative;
+        opacity: 0.85;
+        transition: opacity 0.15s;
+
+        &:hover {
+            opacity: 1;
+        }
+
+        &.is-active {
+            opacity: 1;
+            font-weight: 600;
+
+            &::after {
+                content: '';
+                position: absolute;
+                left: 18px;
+                right: 18px;
+                bottom: 0;
+                height: 3px;
+                background: #fff;
+                border-radius: 2px;
+            }
+        }
+
+        &.is-disabled {
+            opacity: 0.55;
+        }
+    }
+}
+</style>

+ 44 - 3
ui/sp-user-center/src/layout/components/Sidebar/index.vue

@@ -24,16 +24,57 @@ import SidebarItem from './SidebarItem.vue'
 import variables from '@/assets/styles/variables.module.scss'
 import useAppStore from '@/store/modules/app'
 import useSettingsStore from '@/store/modules/settings'
-import usePermissionStore from '@/store/modules/permission'
+import usePortalMenuStore from '@/store/modules/portalMenu'
 import { RouteOption } from "vue-router";
+import { ModuleKey, resolveModuleByPath } from '@/enums/ModuleEnum'
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 const route = useRoute();
 const appStore = useAppStore()
 const settingsStore = useSettingsStore()
-const permissionStore = usePermissionStore()
+const portalMenuStore = usePortalMenuStore()
 
-const sidebarRouters =  computed<RouteOption[]>(() => permissionStore.sidebarRouters);
+// 当前模块:优先看 store,没有则按当前路由路径反查
+const currentModule = computed<ModuleKey>(() => appStore.activeModule || resolveModuleByPath(route.path));
+
+const normalizeMenuPath = (rawPath: string, parentPath = ''): string => {
+    if (!rawPath) return ''
+    if (/^https?:\/\//.test(rawPath)) return rawPath
+    if (!parentPath) return rawPath
+    // 侧边栏组件会基于 basePath 拼接子 path,若这里保留绝对路径会被重复拼接导致 404。
+    if (rawPath.startsWith(parentPath + '/')) {
+        return rawPath.slice(parentPath.length + 1)
+    }
+    if (rawPath.startsWith('/')) {
+        return rawPath.replace(/^\/+/, '')
+    }
+    return rawPath
+}
+
+const toRouteOption = (menus: any[], parentPath = ''): RouteOption[] => {
+    return menus.map((item: any) => {
+        const currentPath = normalizeMenuPath(item.path, parentPath)
+        const fullPath = parentPath ? `${parentPath.replace(/\/$/, '')}/${currentPath}` : item.path
+        return ({
+        path: currentPath,
+        meta: {
+            title: item.title,
+            icon: item.icon || 'menu',
+            module: currentModule.value,
+        },
+        children: item.children?.length ? toRouteOption(item.children, fullPath) : undefined,
+    })}) as RouteOption[]
+}
+
+// 菜单来源从“新门户聚合接口”读取:
+// - home: 默认展示常用菜单
+// - 其他模块: 展示对应模块菜单
+const sidebarRouters = computed<RouteOption[]>(() => {
+    if (currentModule.value === ModuleKey.HOME) {
+        return toRouteOption(portalMenuStore.getCommonMenus)
+    }
+    return toRouteOption(portalMenuStore.getModuleMenus(currentModule.value))
+});
 
 const showLogo = computed(() => settingsStore.sidebarLogo);
 const sideTheme = computed(() => settingsStore.sideTheme);

+ 1 - 1
ui/sp-user-center/src/layout/components/notice/SetMsgTools.vue

@@ -40,7 +40,7 @@ import auth from '@/plugins/auth'
 import Table from '@/components/Table/index.vue'
 import { tableTypes } from '@/components/Table/types'
 import { useDictCache } from '@/hooks/web/useDict'
-import MessageDetail from '@/views/system/messageTemplate/detail.vue'
+import MessageDetail from '@/modules/sales/views/system/messageTemplate/detail.vue'
 
 const { queryDictDetailByCodes } = useDictCache(['query_work_data_group'])
 queryDictDetailByCodes()

+ 54 - 186
ui/sp-user-center/src/layout/index.vue

@@ -1,126 +1,61 @@
 <template>
-	<div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
-		<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
-		<side-bar v-if="!sidebar.hide" class="sidebar-container" />
-		<div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
-			<div :class="{ 'fixed-header': fixedHeader }">
-				<navbar ref="navbarRef" @setLayout="setLayout" />
-				<tags-view v-if="needTagsView" />
-			</div>
-			<app-main />
-			<settings ref="settingRef" />
-		</div>
-	</div>
-	<Transition name="slide-fade">
-		<div v-if="showEmailDialog" class="email-dialog-container">
-			<template v-for="(item, index) in emailData">
-				<div class="email-dialog flex-center" @click="goEmail(item, index)">
-					<div class="flex" style="position: relative">
-						<img src="../assets/images/email-img.png" />
-						<span class="badge">{{ unReadCount }}</span>
-					</div>
-					<div class="email-detail">
-						<p class="bold">{{ item.fromName || item.from }}</p>
-						<p>{{ item.subject }}</p>
-						<!-- <p class="content" v-html="item.content"></p> -->
-					</div>
-					<el-icon :size="18" @click.stop="closeEmailDialog(index)"><Close /></el-icon>
-				</div>
-			</template>
-		</div>
-	</Transition>
+    <div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
+        <module-tabs class="module-tabs-bar" />
+        <div class="app-body">
+            <div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
+            <side-bar v-if="!sidebar.hide" class="sidebar-container" />
+            <div :class="{ sidebarHide: sidebar.hide }" class="main-container">
+                <app-main />
+            </div>
+        </div>
+    </div>
 </template>
 
 <script setup lang="ts">
 import SideBar from './components/Sidebar/index.vue'
-import { AppMain, Navbar, Settings, TagsView } from './components'
+import ModuleTabs from './components/ModuleTabs.vue'
+import { AppMain } from './components'
 import useAppStore from '@/store/modules/app'
 import useSettingsStore from '@/store/modules/settings'
-import useNoticeStore from '@/store/modules/notice';
+import usePortalMenuStore from '@/store/modules/portalMenu'
 
-const noticeStore = storeToRefs(useNoticeStore());
+const appStore = useAppStore()
+const portalMenuStore = usePortalMenuStore()
 const settingsStore = useSettingsStore()
-const theme = computed(() => settingsStore.theme);
-const sidebar = computed(() => useAppStore().sidebar);
-const device = computed(() => useAppStore().device);
-const needTagsView = computed(() => settingsStore.tagsView);
-const fixedHeader = computed(() => settingsStore.fixedHeader);
-const emailObj = computed(() => noticeStore.state.value.emailNoReadObj);
-
-const showEmailDialog = ref(false)
+const theme = computed(() => settingsStore.theme)
+const sidebar = computed(() => appStore.sidebar)
+const device = computed(() => appStore.device)
 
 const classObj = computed(() => ({
-	hideSidebar: !sidebar.value.opened,
-	openSidebar: sidebar.value.opened,
-	withoutAnimation: sidebar.value.withoutAnimation,
-	mobile: device.value === 'mobile'
+    hideSidebar: !sidebar.value.opened,
+    openSidebar: sidebar.value.opened,
+    withoutAnimation: sidebar.value.withoutAnimation,
+    mobile: device.value === 'mobile'
 }))
 
-const { width } = useWindowSize();
-const WIDTH = 992; // refer to Bootstrap's responsive design
+const { width } = useWindowSize()
+const WIDTH = 992
 
 watchEffect(() => {
-	if (device.value === 'mobile' && sidebar.value.opened) {
-		useAppStore().closeSideBar({ withoutAnimation: false })
-	}
-	if (width.value - 1 < WIDTH) {
-		useAppStore().toggleDevice('mobile')
-		useAppStore().closeSideBar({ withoutAnimation: true })
-	} else {
-		useAppStore().toggleDevice('desktop')
-	}
+    if (device.value === 'mobile' && sidebar.value.opened) {
+        appStore.closeSideBar({ withoutAnimation: false })
+    }
+    if (width.value - 1 < WIDTH) {
+        appStore.toggleDevice('mobile')
+        appStore.closeSideBar({ withoutAnimation: true })
+    } else {
+        appStore.toggleDevice('desktop')
+    }
 })
 
-const emailData = ref([])
-const unReadCount = ref(0)
-watch(emailObj, (newVal, oldVal) => {
-	if(newVal.maills && newVal.maills.length > 0) {
-		unReadCount.value = newVal.unReadCount
-		emailData.value.push(...newVal.maills)
-		emailData.value = emailData.value.slice(-3)
-		showEmailDialog.value = true
-	}
-}, { deep: true })
-
-const closeEmailDialog = (index: number) => {
-	emailData.value.splice(index, 1)
-	if(emailData.value.length == 0) {
-		showEmailDialog.value = false
-	}
-}
-
-const router = useRouter();
-const goEmail = (item: any, index: number) => {
-	const { href } = router.resolve({
-		path: '/customer/followPanel/detail',
-		query: {
-			id: item.id,
-			resourceType: item.customerForm
-		}
-	})
-    window.open(href)
-	closeEmailDialog(index)
-}
-
-const navbarRef = ref(Navbar);
-const settingRef = ref(Settings);
-
-const channel = new BroadcastChannel('email_channel');
-channel.onmessage = (event) => {
-	useNoticeStore().getCount()
-};
-
 onMounted(() => {
-	nextTick(() => {
-	})
+    if (!portalMenuStore.loaded) {
+        portalMenuStore.loadPortalMenus()
+    }
 })
 
 const handleClickOutside = () => {
-	useAppStore().closeSideBar({ withoutAnimation: false })
-}
-
-const setLayout = () => {
-	settingRef.value.openSetting();
+    appStore.closeSideBar({ withoutAnimation: false })
 }
 </script>
 
@@ -133,6 +68,8 @@ const setLayout = () => {
 	position: relative;
 	height: 100%;
 	width: 100%;
+	display: flex;
+	flex-direction: column;
 
 	&.mobile.openSidebar {
 		position: fixed;
@@ -140,98 +77,29 @@ const setLayout = () => {
 	}
 }
 
-.drawer-bg {
-	background: #000;
-	opacity: 0.3;
+.module-tabs-bar {
+	flex: 0 0 50px;
+	height: 50px;
 	width: 100%;
-	top: 0;
-	height: 100%;
-	position: absolute;
-	z-index: 999;
+	z-index: 1001;
 }
 
-.fixed-header {
-	position: fixed;
-	top: 0;
-	right: 0;
-	z-index: 9;
-	width: calc(100% - #{$base-sidebar-width});
-	transition: width 0.28s;
-	background: $fixed-header-bg;
-}
-
-.hideSidebar .fixed-header {
-	width: calc(100% - 54px);
-}
-
-.sidebarHide .fixed-header {
+.app-body {
+	flex: 1 1 auto;
+	position: relative;
+	display: flex;
 	width: 100%;
+	min-height: 0;
 }
 
-.mobile .fixed-header {
+.drawer-bg {
+	background: #000;
+	opacity: 0.3;
 	width: 100%;
-}
-
-.email-dialog-container{
+	top: 0;
+	height: 100%;
 	position: absolute;
-	bottom: 25px;
-	right: 25px;
-	z-index: 99999;
-}
-.email-dialog {
-	width: 400px;
-	height: 80px;
-	box-shadow: 0px 0px 7px 0px rgba(0,0,0,0.1);
-	border-radius: 2px;
-	background-color: #ECF5FF;
-	padding: 10px;
-	cursor: pointer;
-	margin-top: 15px;
-    position: relative;
-	.badge{
-		position: absolute;
-		top: 2px;
-		right: 8px;
-		background: #F40003;
-		color: #FFF;
-		border-radius: 50%;
-		font-size: 12px;
-		width: 18px;
-		height: 18px;
-		text-align: center;
-	}
-	.email-detail{
-		flex: 1;
-		width: 0;
-		margin-left: 8px;
-		color: #333;
-		p{
-			overflow: hidden;
-			text-overflow: ellipsis;
-			white-space: nowrap;
-		}
-		.content{
-			color: #999
-		}
-	}
-	.el-icon{
-		position: absolute;
-		top: 7px;
-		right: 7px;
-		cursor: pointer;
-	}
-}
-.slide-fade-enter-active {
-  transition: all 0.5s ease-out;
-}
-
-.slide-fade-leave-active {
-  transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);
+	z-index: 999;
 }
 
-.slide-fade-enter-from,
-.slide-fade-leave-to {
-  transform: translateX(20px);
-  opacity: 0;
-}
 </style>

+ 677 - 0
ui/sp-user-center/src/mock/httpMock.ts

@@ -0,0 +1,677 @@
+import { ModuleKey } from '@/enums/ModuleEnum'
+
+interface MockResult {
+  success?: boolean
+  code: number
+  message: string
+  result: any
+}
+
+const ok = (result: any, message = 'mock data'): MockResult => ({
+  success: true,
+  code: 200,
+  message,
+  result,
+})
+
+const parseBody = (data: any) => {
+  if (!data) return {}
+  if (typeof data === 'string') {
+    try {
+      return JSON.parse(data)
+    } catch {
+      return {}
+    }
+  }
+  return data
+}
+
+const portalMenuMock = [
+  {
+    module: ModuleKey.HOME,
+    commonMenus: [
+      { id: 'home-dashboard', title: '首页', path: '/home/dashboard', icon: 'dashboard' },
+      { id: 'home-otr-target', title: '我的目标', path: '/otr/target/mine', icon: 'guide' },
+      { id: 'home-sales-index', title: '销售首页', path: '/index', icon: 'chart' },
+    ],
+    moduleMenus: [],
+  },
+  {
+    module: ModuleKey.OTR,
+    commonMenus: [],
+    moduleMenus: [
+      {
+        id: 'otr-target',
+        title: '目标管理',
+        path: '/otr/target',
+        icon: 'guide',
+        children: [
+          { id: 'otr-target-mine', title: '我的目标', path: '/otr/target/mine' },
+          { id: 'otr-target-company', title: '公司目标', path: '/otr/target/company' },
+          { id: 'otr-target-depart', title: '部门目标', path: '/otr/target/depart' },
+          { id: 'otr-target-template', title: '目标模板', path: '/otr/target/template' },
+          { id: 'otr-target-report', title: '目标报告', path: '/otr/target/report' },
+        ],
+      },
+      {
+        id: 'otr-effect',
+        title: '绩效管理',
+        path: '/otr/effect',
+        icon: 'chart',
+        children: [
+          { id: 'otr-effect-manage', title: '绩效管理', path: '/otr/effect/manage' },
+          { id: 'otr-effect-mine', title: '我的绩效', path: '/otr/effect/mine' },
+          { id: 'otr-effect-board', title: '绩效看板', path: '/otr/effect/board' },
+          { id: 'otr-effect-backlog', title: '绩效待办', path: '/otr/effect/backlog' },
+        ],
+      },
+      {
+        id: 'otr-task',
+        title: '任务管理',
+        path: '/otr/task',
+        icon: 'edit',
+        children: [
+          { id: 'otr-task-list', title: '我的任务', path: '/otr/task/list' },
+          { id: 'otr-task-member', title: '成员任务', path: '/otr/task/member' },
+          { id: 'otr-task-at-list', title: '@我的任务', path: '/otr/task/at-list' },
+          { id: 'otr-task-table', title: '任务看板', path: '/otr/task/table' },
+        ],
+      },
+      {
+        id: 'otr-summary',
+        title: '工作总结',
+        path: '/otr/summary',
+        icon: 'documentation',
+        children: [
+          { id: 'otr-summary-list', title: '工作总结', path: '/otr/summary/list' },
+          { id: 'otr-summary-member', title: '成员简报', path: '/otr/summary/member' },
+          { id: 'otr-summary-statistics', title: '简报统计', path: '/otr/summary/statistics' },
+          { id: 'otr-summary-draft', title: '草稿箱', path: '/otr/summary/draft' },
+          { id: 'otr-summary-share', title: '心得分享', path: '/otr/summary/share' },
+          { id: 'otr-summary-announcement', title: '公告列表', path: '/otr/summary/announcement' },
+        ],
+      },
+      {
+        id: 'otr-notice',
+        title: '系统通知',
+        path: '/otr/notice',
+        icon: 'message',
+        children: [{ id: 'otr-notice-list', title: '系统通知', path: '/otr/notice/list' }],
+      },
+      {
+        id: 'otr-award',
+        title: '奖励管理',
+        path: '/otr/award',
+        icon: 'star',
+        children: [
+          { id: 'otr-award-list', title: '奖励管理', path: '/otr/award/list' },
+          { id: 'otr-award-pool', title: '奖金池管理', path: '/otr/award/pool' },
+          { id: 'otr-award-trends', title: '奖励动态', path: '/otr/award/trends' },
+        ],
+      },
+      {
+        id: 'otr-report',
+        title: '人员报告',
+        path: '/otr/report',
+        icon: 'data-analysis',
+        children: [{ id: 'otr-report-person', title: '人员报告', path: '/otr/report/person' }],
+      },
+      {
+        id: 'otr-ai',
+        title: 'AI 工具',
+        path: '/otr/ai',
+        icon: 'magic-stick',
+        children: [{ id: 'otr-ai-add', title: 'AI 工具', path: '/otr/ai/add' }],
+      },
+    ],
+  },
+  {
+    module: ModuleKey.SALES,
+    commonMenus: [],
+    moduleMenus: [
+      { id: 'sales-index', title: '销售首页', path: '/index', icon: 'dashboard' },
+      { id: 'sales-clue-add', title: '新增线索', path: '/clue/add', icon: 'edit' },
+      { id: 'sales-customer-add', title: '创建客户', path: '/customer/addCustomer', icon: 'user' },
+    ],
+  },
+  { module: ModuleKey.PROJECT, commonMenus: [], moduleMenus: [] },
+  { module: ModuleKey.SALARY, commonMenus: [], moduleMenus: [] },
+  { module: ModuleKey.WEBSITE, commonMenus: [], moduleMenus: [] },
+]
+
+const roleRows = [
+  { id: 1, name: 'admin', desc: '系统管理员', remark: 'mock', createTime: '2026-05-01 10:00:00', enabled: 1 },
+  { id: 2, name: 'sales-manage', desc: '销售主管', remark: 'mock', createTime: '2026-05-02 11:00:00', enabled: 1 },
+  { id: 3, name: 'staff', desc: '普通员工', remark: 'mock', createTime: '2026-05-03 12:00:00', enabled: 0 },
+]
+
+const salesHomeSummary = {
+  addCustomerNum: 0,
+  addClusNum: 0,
+  addBusinessNum: 0,
+  addOrderFormNum: 0,
+  addLiaisonNum: 0,
+  followUpNum: 0,
+  transactionsNum: 0,
+  transactionsBusiness: 0,
+  transactionsAmount: 0,
+}
+
+const salesHomeNewData = {
+  orderGrowthRate: 0,
+  followUpGrowthRate: 0,
+  totalBusinessCount: 0,
+  amountGrowthRate: 0,
+  totalOrderAmt: 0,
+  followUpCount: 0,
+  businessGrowthRate: 0,
+  totalOrderCount: 0,
+}
+
+const salesHomeAllProgress = { progress: '0.00' }
+const salesHomeAmountRink = { data: [] }
+const salesHomeUserProgress = { total: '0', progress: '0.00', target: '0' }
+const salesHomeTotalTask = {}
+
+const salesHomeFunnel = {
+  businessStageCountls: [
+    { stageNum: null, stageName: '初步洽谈', salesAmount: 0, stageSurnoverRatio: 0, businessNum: '0', businessNumRatio: 0, expectedSalesAmount: 0 },
+    { stageNum: null, stageName: '深入沟通', salesAmount: 0, stageSurnoverRatio: 0, businessNum: '0', businessNumRatio: 0, expectedSalesAmount: 0 },
+    { stageNum: null, stageName: '产品报价', salesAmount: 0, stageSurnoverRatio: 0, businessNum: '0', businessNumRatio: 0, expectedSalesAmount: 0 },
+    { stageNum: null, stageName: '成交商机', salesAmount: 0, stageSurnoverRatio: 0, businessNum: '0', businessNumRatio: 0, expectedSalesAmount: 0 },
+    { stageNum: null, stageName: '流失商机', salesAmount: 0, stageSurnoverRatio: 0, businessNum: '0', businessNumRatio: 0, expectedSalesAmount: 0 },
+  ],
+  salesAmountTotal: 0,
+  businessNumTotal: 0,
+  expectedSalesAmountTotal: 0,
+}
+
+const salesHomePredictiveTotal = {
+  businessMonthCountls: [
+    { month: '2025-06', salesAmount: 0, businessNum: '0', expectedSalesAmount: 0 },
+    { month: '2025-07', salesAmount: 0, businessNum: '0', expectedSalesAmount: 0 },
+    { month: '2025-08', salesAmount: 0, businessNum: '0', expectedSalesAmount: 0 },
+    { month: '2025-09', salesAmount: 0, businessNum: '0', expectedSalesAmount: 0 },
+    { month: '2025-10', salesAmount: 0, businessNum: '0', expectedSalesAmount: 0 },
+    { month: '2025-11', salesAmount: 0, businessNum: '0', expectedSalesAmount: 0 },
+    { month: '2025-12', salesAmount: 0, businessNum: '0', expectedSalesAmount: 0 },
+    { month: '2026-01', salesAmount: 0, businessNum: '0', expectedSalesAmount: 0 },
+    { month: '2026-02', salesAmount: 0, businessNum: '0', expectedSalesAmount: 0 },
+    { month: '2026-03', salesAmount: 0, businessNum: '0', expectedSalesAmount: 0 },
+    { month: '2026-04', salesAmount: 0, businessNum: '0', expectedSalesAmount: 0 },
+    { month: '2026-05', salesAmount: 0, businessNum: '0', expectedSalesAmount: 0 },
+  ],
+  salesAmountTotal: 0,
+  businessNumTotal: '0',
+  expectedSalesAmountTotal: 0,
+}
+
+const salesQuickMenus = [
+  { id: '172257396461570', menuName: '下属商机', path: 'business_opportunity/subordinateBusiness', icon: 'edit' },
+  { id: '774668423532546', menuName: '公共线索', path: 'clue/public', icon: 'wechat' },
+  { id: '968414528999425', menuName: '我的订单', path: 'order/mine', icon: 'list' },
+  { id: '968906923511809', menuName: '下属订单', path: 'order/subordinate', icon: 'form' },
+  { id: '3031967315419138', menuName: '联系人', path: 'customer/liaison', icon: 'peoples' },
+  { id: '3036259468427265', menuName: '客户跟进记录', path: 'customer/followUp', icon: 'nested' },
+  { id: '3059155905527859', menuName: '添加商机', path: 'business_opportunity/addbus', icon: 'list-check' },
+  { id: '1783031967315419289', menuName: '重点客户', path: 'customer/importantCustomer', icon: 'peoples' },
+  { id: '1783031967315419292', menuName: '跟进记录', path: '/quickMenu/addFlow', icon: 'date' },
+]
+
+const salesAuthMenus = [
+  {
+    id: '68',
+    menuName: '线索管理',
+    children: [
+      { id: '2305459139530754', menuName: '全部线索' },
+      { id: '3708552951508', menuName: '我的线索' },
+      { id: '791524534857729', menuName: '下属线索' },
+      { id: '774668423532546', menuName: '公共线索' },
+    ],
+  },
+  {
+    id: '66',
+    menuName: '客户管理',
+    children: [
+      { id: '885833816346625', menuName: '客户列表' },
+      { id: '3031967315419138', menuName: '联系人' },
+      { id: '3036259468427265', menuName: '客户跟进记录' },
+      { id: '1783031967315419289', menuName: '重点客户' },
+    ],
+  },
+  {
+    id: '69',
+    menuName: '商机管理',
+    children: [
+      { id: '342091493154818', menuName: '全部商机' },
+      { id: '3059155905527859', menuName: '添加商机' },
+      { id: '164953884266498', menuName: '我的商机' },
+      { id: '172257396461570', menuName: '下属商机' },
+    ],
+  },
+  {
+    id: '70',
+    menuName: '订单管理',
+    children: [
+      { id: '642294251134977', menuName: '全部订单' },
+      { id: '968414528999425', menuName: '我的订单' },
+      { id: '968906923511809', menuName: '下属订单' },
+    ],
+  },
+]
+
+/**
+ * 全局兜底 mock:接口失败时调用。
+ * 返回 null 表示该接口没有定制 mock,将走通用空数据。
+ */
+export const buildMockByRequest = (url = '', method = 'get', data?: any): MockResult | null => {
+  const body = parseBody(data)
+  const m = method.toLowerCase()
+
+  if (url.includes('/sys/portal/menu/tree')) {
+    return ok(portalMenuMock)
+  }
+
+  if (url.includes('/sys/auth/login')) {
+    return ok({ token: 'mock-token', access_token: 'mock-token' }, 'mock login success')
+  }
+
+  if (url.includes('/sys/auth/getCurrentUserInfo')) {
+    return ok({
+      id: 1,
+      userId: 1,
+      account: 'mock.admin',
+      username: 'mock.admin',
+      nickname: 'Mock管理员',
+      realName: 'Mock管理员',
+      avatar: '',
+      roles: ['admin'],
+    })
+  }
+
+  if (url.includes('/sys/permission/getUserAccessResource')) {
+    return ok([])
+  }
+
+  if (url.includes('/sys/role/manage/pagelist')) {
+    const pageNum = Number(body?.pageNum || 1)
+    const pageSize = Number(body?.pageSize || 10)
+    const start = (pageNum - 1) * pageSize
+    const records = roleRows.slice(start, start + pageSize)
+    return ok({ records, total: roleRows.length, pageNum, pageSize })
+  }
+
+  if (url.includes('/sys/role/manage/save')) {
+    return ok(true, 'mock save success')
+  }
+
+  if (url.includes('/sys/role/manage/delete')) {
+    return ok(true, 'mock delete success')
+  }
+
+  if (url.includes('/sys/role/manage/enable')) {
+    return ok(true, 'mock enable success')
+  }
+
+  if (url.includes('/customer/attribute/data-summary')) {
+    return ok(salesHomeSummary)
+  }
+
+  if (url.includes('/sys/task/total_task')) {
+    return ok(salesHomeTotalTask)
+  }
+
+  if (url.includes('/customer/attribute/new-data')) {
+    return ok(salesHomeNewData)
+  }
+
+  if (url.includes('/target-quota/uesr-all-progress')) {
+    return ok(salesHomeAllProgress)
+  }
+
+  if (url.includes('/sys/quick/menu/quickMenus')) {
+    return ok(salesQuickMenus)
+  }
+
+  if (url.includes('/sys/quick/menu/listMenus')) {
+    return ok(salesAuthMenus)
+  }
+
+  if (url.includes('/target-quota/amount-rink')) {
+    return ok(salesHomeAmountRink)
+  }
+
+  if (url.includes('/target-quota/uesr-progress')) {
+    return ok(salesHomeUserProgress)
+  }
+
+  if (url.includes('/process/analysis/get_analyze_sales_funnel')) {
+    return ok(salesHomeFunnel)
+  }
+
+  if (url.includes('/process/analysis/get_predictive_analytics_total')) {
+    return ok(salesHomePredictiveTotal)
+  }
+
+  /** OTR 目标列表 / 报告 / 分析(接口失败时的兜底结构) */
+  if (url.includes('/target/search')) {
+    return ok({ records: [], total: 0 })
+  }
+
+  if (url.includes('/report/company/count')) {
+    return ok({
+      gt: '10.00',
+      ct: '20.00',
+      dt: '30.00',
+      pt: '40.00',
+      p1: 1,
+      pr1: 5,
+      p2: 2,
+      pr2: 10,
+      p3: 3,
+      pr3: 15,
+      p4: 4,
+      pr4: 20,
+      p5: 5,
+      pr5: 25,
+      allCount: 15,
+      gtCount: 1,
+      ctCount: 2,
+      dtCount: 3,
+      ptCount: 4,
+    })
+  }
+
+  if (url.includes('/report/dept/all')) {
+    return ok({
+      deptReportDtos: [],
+      deptLeaderReportDtos: [],
+    })
+  }
+
+  if (url.includes('/report/staff/all')) {
+    return ok({
+      staffReportDtos: [],
+      staffTargetPercentDtos: [],
+    })
+  }
+
+  if (url.includes('/analysis/whole/target-task-analysis')) {
+    return ok({ targetNum: 10, taskNum: 25 })
+  }
+
+  if (url.includes('/analysis/whole/target-analysis')) {
+    return ok({
+      categories: ['一类', '二类', '三类'],
+      values: [12, 24, 36],
+    })
+  }
+
+  if (url.includes('/analysis/whole/staff-join')) {
+    return ok({ joined: 20, notJoined: 5 })
+  }
+
+  if (url.includes('/analysis/progress/trend')) {
+    return ok({
+      datas: [
+        { month: '2026-01', rate: 22 },
+        { month: '2026-02', rate: 38 },
+        { month: '2026-03', rate: 51 },
+      ],
+    })
+  }
+
+  if (url.includes('/analysis/progress/staff-rank')) {
+    return ok({ datas: [] })
+  }
+
+  if (url.includes('/analysis/progress/target-rank')) {
+    return ok({ datas: [] })
+  }
+
+  /** OTR 其余域(effect/task/summary/award/notice/report/ai) */
+  if (url.includes('/effect/manage/list')) {
+    return ok({
+      records: [{ id: 1, title: 'Q2 组织绩效考核', ownerName: '张三', cycleName: '季度', statusLabel: '进行中' }],
+      total: 1,
+    })
+  }
+  if (url.includes('/effect/mine/list')) {
+    return ok({
+      records: [{ id: 1, title: 'Q2 个人绩效', score: 88, rank: 6, statusLabel: '待确认' }],
+      total: 1,
+    })
+  }
+  if (url.includes('/effect/board/list')) {
+    return ok({
+      records: [{ id: 1, ownerName: '李四', deptName: '运营中心', targetCount: 8, avgScore: 86 }],
+      total: 1,
+    })
+  }
+  if (url.includes('/effect/backlog/list')) {
+    return ok({
+      records: [{ id: 1, title: '完成绩效自评', ownerName: '王五', deadline: '2026-05-31 18:00:00', statusLabel: '待处理' }],
+      total: 1,
+    })
+  }
+  if (url.includes('/effect/template/list')) {
+    return ok({
+      records: [{ id: 1, name: '销售经理季度模板', scene: '季度考核', updatedBy: '系统管理员', updatedAt: '2026-05-01 10:00:00' }],
+      total: 1,
+    })
+  }
+  if (url.includes('/task/list')) {
+    return ok({
+      records: [{ id: 1, name: '完成周目标复盘', ownerName: '赵六', priority: '高', deadline: '2026-05-10 17:30:00', statusLabel: '进行中' }],
+      total: 1,
+    })
+  }
+  if (url.includes('/task/listByTable') || url.includes('/task/list-mine-gantt')) {
+    return ok({
+      records: [{ id: 1, taskDateId: 1, name: '客户跟进计划', ownerName: '赵六', priority: '高', progress: '65%', deadline: '2026-05-10 17:30:00', statusLabel: '进行中' }],
+      total: 1,
+    })
+  }
+  if (url.includes('/task/list-subordinate-gantt')) {
+    return ok({
+      records: [{ id: 2, taskDateId: 2, memberName: '张三', name: '整理客户资料', deadline: '2026-05-11 12:00:00', statusLabel: '未开始' }],
+      total: 1,
+    })
+  }
+  if (url.includes('/task/at-mine-list')) {
+    return ok({
+      records: [{ id: 3, taskDateId: 3, name: '更新周报任务', fromName: '李四', createdAt: '2026-05-07 10:30:00', statusLabel: '未读' }],
+      total: 1,
+    })
+  }
+  if (url.includes('/task/get')) {
+    return ok({
+      id: 1,
+      title: '客户跟进计划',
+      leaderName: '赵六',
+      statusLabel: '进行中',
+      startTime: '2026-05-07 09:00:00',
+      endTime: '2026-05-10 17:30:00',
+      content: '分批联系客户并记录反馈',
+    })
+  }
+  if (url.includes('/task/log/list')) {
+    return ok({
+      records: [
+        { id: 1, createTime: '2026-05-07 09:10:00', content: '创建任务' },
+        { id: 2, createTime: '2026-05-07 10:00:00', content: '更新进度至 65%' },
+      ],
+      total: 2,
+    })
+  }
+  if (url.includes('/task/comment/list')) {
+    return ok({
+      records: [
+        { id: 1, staffName: '张三', createTime: '2026-05-07 10:20:00', content: '建议优先跟进 A 类客户。' },
+        { id: 2, staffName: '李四', createTime: '2026-05-07 10:40:00', content: '已补充本周执行计划。' },
+      ],
+      total: 2,
+    })
+  }
+  if (url.includes('/task/comment/add')) {
+    return ok(true, 'mock comment added')
+  }
+  if (url.includes('/task/resource/list')) {
+    return ok({
+      records: [{ id: 1, fileName: '任务说明.docx', fileSize: '120KB', createTime: '2026-05-07 09:12:00' }],
+      total: 1,
+    })
+  }
+  if (url.includes('/task/add')) {
+    return ok(true, 'mock task saved')
+  }
+  if (url.includes('/summary/list')) {
+    return ok({
+      records: [{ id: 1, title: '第19周工作总结', ownerName: '钱七', period: '周报', createdAt: '2026-05-06 18:20:00' }],
+      total: 1,
+    })
+  }
+  if (url.includes('/summary/get')) {
+    return ok({
+      id: 1,
+      title: '第19周工作总结',
+      summaryType: '2',
+      beginDate: '2026-05-01',
+      endDate: '2026-05-07',
+      content: '本周完成重点客户跟进与需求澄清。',
+    })
+  }
+  if (url.includes('/summary/add')) {
+    return ok(true, 'mock summary saved')
+  }
+  if (url.includes('/summary/edit')) {
+    return ok(true, 'mock summary updated')
+  }
+  if (url.includes('/award/list')) {
+    return ok({
+      records: [{ id: 1, title: '季度冲刺奖励', ownerName: '孙八', amount: 2000, statusLabel: '待审批' }],
+      total: 1,
+    })
+  }
+  if (url.includes('/award/get')) {
+    return ok({
+      id: 1,
+      title: '季度冲刺奖励',
+      ownerName: '孙八',
+      amount: 2000,
+      statusLabel: '待审批',
+      remark: '用于激励季度目标达成',
+    })
+  }
+  if (url.includes('/award/add')) {
+    return ok(true, 'mock award saved')
+  }
+  if (url.includes('/notice/list')) {
+    return ok({
+      records: [{ id: 1, title: 'OTR 目标更新提醒', type: '系统', senderName: '系统助手', createdAt: '2026-05-07 09:30:00' }],
+      total: 1,
+    })
+  }
+  if (url.includes('/report/person/list')) {
+    return ok({
+      records: [{ id: 1, staffName: '周九', deptName: '市场部', targetCount: 5, progress: '64%', effectScore: 83 }],
+      total: 1,
+    })
+  }
+  if (url.includes('/ai/tool/list')) {
+    return ok({
+      records: [{ id: 1, name: '目标分解助手', scene: '目标管理', ownerName: '产品团队', statusLabel: '已上线' }],
+      total: 1,
+    })
+  }
+
+  if (url.includes('/task/member/list')) {
+    return ok({ records: [{ memberName: '张三', taskName: '客户跟进复盘', deadline: '2026-05-12 18:00:00', statusLabel: '进行中' }], total: 1 })
+  }
+  if (url.includes('/task/board/list')) {
+    return ok({ records: [{ name: '合同回款跟进', ownerName: '李四', stage: '执行中', progress: '65%' }], total: 1 })
+  }
+  if (url.includes('/task/detail/list')) {
+    return ok({ records: [{ name: '任务明细示例', ownerName: '王五', content: '按周推进并同步状态', statusLabel: '进行中' }], total: 1 })
+  }
+  if (url.includes('/task/at/list')) {
+    return ok({ records: [{ name: '请更新进度', fromName: '赵六', createdAt: '2026-05-07 10:30:00', statusLabel: '未读' }], total: 1 })
+  }
+
+  if (url.includes('/summary/member/list')) {
+    return ok({ records: [{ memberName: '钱七', title: '本周总结', period: '周报', createdAt: '2026-05-06 18:30:00' }], total: 1 })
+  }
+  if (url.includes('/summary/statistics/list')) {
+    return ok({ records: [{ dimension: '周报提交率', value: 92, ratio: '92%' }], total: 1 })
+  }
+  if (url.includes('/summary/draft/list')) {
+    return ok({ records: [{ title: '周报草稿', updatedAt: '2026-05-07 09:18:00', ownerName: '孙八' }], total: 1 })
+  }
+  if (url.includes('/summary/share/list')) {
+    return ok({ records: [{ title: '客户洞察方法论', author: '周九', likes: 12, createdAt: '2026-05-01 09:00:00' }], total: 1 })
+  }
+  if (url.includes('/summary/announcement/list')) {
+    return ok({ records: [{ title: '周报提交通知', publisher: '系统助手', createdAt: '2026-05-07 09:00:00' }], total: 1 })
+  }
+
+  if (url.includes('/award/pool/list')) {
+    return ok({ records: [{ poolName: '季度激励池', amount: 50000, updatedAt: '2026-05-07 08:00:00' }], total: 1 })
+  }
+  if (url.includes('/award/trends/list')) {
+    return ok({ records: [{ title: '发放月度奖励', operator: '系统管理员', createdAt: '2026-05-07 08:30:00' }], total: 1 })
+  }
+
+  if (url.includes('/target/category/list')) {
+    return ok({ records: [{ name: '销售增长类', sort: 1, updatedAt: '2026-05-01 10:00:00' }], total: 1 })
+  }
+  if (url.includes('/target/tree/list')) {
+    return ok({ records: [{ title: '提升成交转化率', leaderName: '李四', statusLabel: '进行中', percent: 53 }], total: 1 })
+  }
+  if (url.includes('/target/draft/list')) {
+    return ok({ records: [{ title: 'Q3 目标草稿', leaderName: '王五', updatedAt: '2026-05-07 09:20:00' }], total: 1 })
+  }
+  if (url.includes('/target/approval/list')) {
+    return ok({ records: [{ title: '年度目标审批', applicantName: '赵六', statusLabel: '待审批', createdAt: '2026-05-07 09:40:00' }], total: 1 })
+  }
+
+  if (url.includes('/effect/setting/list')) {
+    return ok({ records: [{ name: '考核周期', value: '季度', updatedAt: '2026-05-01 10:00:00' }], total: 1 })
+  }
+  if (url.includes('/effect/detail/list')) {
+    return ok({ records: [{ title: 'Q2绩效考核', ownerName: '张三', score: 87, statusLabel: '进行中' }], total: 1 })
+  }
+  if (url.includes('/effect/index/list')) {
+    return ok({ records: [{ title: '组织绩效', ownerName: '李四', deptName: '销售部', statusLabel: '已发布' }], total: 1 })
+  }
+  if (url.includes('/effect/dept/list')) {
+    return ok({ records: [{ deptName: '运营中心', targetCount: 10, avgScore: 86 }], total: 1 })
+  }
+  if (url.includes('/effect/person/list')) {
+    return ok({ records: [{ staffName: '王五', deptName: '市场部', score: 89, rank: 5 }], total: 1 })
+  }
+  if (url.includes('/effect/evaluated/list')) {
+    return ok({ records: [{ title: 'Q2绩效待评', evaluateeName: '赵六', deadline: '2026-05-20 18:00:00' }], total: 1 })
+  }
+  if (url.includes('/effect/review/list')) {
+    return ok({ records: [{ title: '绩效待审单', applicantName: '周九', statusLabel: '待审核' }], total: 1 })
+  }
+  if (url.includes('/effect/objection/list')) {
+    return ok({ records: [{ title: '评分异议申请', ownerName: '钱七', statusLabel: '处理中' }], total: 1 })
+  }
+  if (url.includes('/effect/whitelist/list')) {
+    return ok({ records: [{ staffName: '孙八', deptName: '产品部', remark: '关键岗位白名单' }], total: 1 })
+  }
+
+  return null
+}
+
+/**
+ * 通用兜底:避免页面因接口失败崩溃
+ */
+export const buildDefaultMock = (url = ''): MockResult => {
+  if (url.includes('pagelist') || url.includes('list')) {
+    return ok({ records: [], total: 0 })
+  }
+  return ok({})
+}
+

+ 55 - 0
ui/sp-user-center/src/modules/_shared/components/ModuleStub.vue

@@ -0,0 +1,55 @@
+<template>
+    <div class="module-stub">
+        <div class="module-stub__inner">
+            <el-icon class="module-stub__icon" :size="56"><Tools /></el-icon>
+            <h3 class="module-stub__title">{{ title }}</h3>
+            <p class="module-stub__desc">{{ desc }}</p>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { Tools } from '@element-plus/icons-vue'
+
+defineProps<{
+    title: string
+    desc?: string
+}>()
+</script>
+
+<style lang="scss" scoped>
+.module-stub {
+    height: 100%;
+    min-height: calc(100vh - 200px);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: #fafbfc;
+}
+
+.module-stub__inner {
+    text-align: center;
+    padding: 40px 60px;
+    background: #fff;
+    border-radius: 8px;
+    box-shadow: 0 1px 6px rgba(0, 0, 0, 0.04);
+
+    .module-stub__icon {
+        color: #909399;
+        margin-bottom: 16px;
+    }
+
+    .module-stub__title {
+        font-size: 20px;
+        margin: 0 0 8px;
+        color: #303133;
+    }
+
+    .module-stub__desc {
+        font-size: 13px;
+        color: #909399;
+        margin: 0;
+        max-width: 480px;
+    }
+}
+</style>

+ 56 - 0
ui/sp-user-center/src/modules/otr/_shared/components/OtrSimpleFormScene.vue

@@ -0,0 +1,56 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <div class="header">{{ title }}</div>
+    </template>
+
+    <el-form label-width="100px" style="max-width: 720px">
+      <el-form-item label="标题">
+        <el-input v-model="form.title" placeholder="请输入标题" />
+      </el-form-item>
+      <el-form-item label="描述">
+        <el-input v-model="form.desc" type="textarea" :rows="4" placeholder="请输入内容说明" />
+      </el-form-item>
+      <el-form-item label="状态">
+        <el-select v-model="form.status" style="width: 180px">
+          <el-option label="草稿" value="draft" />
+          <el-option label="进行中" value="processing" />
+          <el-option label="已完成" value="done" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="submit">保存</el-button>
+        <el-button @click="reset">重置</el-button>
+      </el-form-item>
+    </el-form>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import { ElMessage } from 'element-plus'
+import { reactive } from 'vue'
+
+defineProps<{ title: string }>()
+
+const form = reactive({
+  title: '',
+  desc: '',
+  status: 'draft',
+})
+
+function submit() {
+  ElMessage.success('已保存(迁移阶段示例提交)')
+}
+
+function reset() {
+  form.title = ''
+  form.desc = ''
+  form.status = 'draft'
+}
+</script>
+
+<style scoped>
+.header {
+  font-weight: 600;
+}
+</style>

+ 117 - 0
ui/sp-user-center/src/modules/otr/_shared/components/OtrSimpleTableWorkspace.vue

@@ -0,0 +1,117 @@
+<template>
+  <div class="otr-simple-table-workspace">
+    <el-card shadow="never">
+      <template #header>
+        <div class="table-header">
+          <span>{{ title }}</span>
+          <slot name="header-extra" />
+        </div>
+      </template>
+
+      <el-form :inline="true" class="filter-form" @submit.prevent>
+        <el-form-item label="关键词">
+          <el-input v-model="query.keyword" clearable :placeholder="keywordPlaceholder" @keyup.enter="handleSearch" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleSearch">查询</el-button>
+          <el-button @click="reset">重置</el-button>
+        </el-form-item>
+      </el-form>
+
+      <el-table v-loading="loading" :data="rows" border stripe>
+        <el-table-column
+          v-for="col in columns"
+          :key="col.prop"
+          :prop="col.prop"
+          :label="col.label"
+          :min-width="col.minWidth || 120"
+          :width="col.width"
+          show-overflow-tooltip
+        />
+      </el-table>
+
+      <div class="pager">
+        <el-pagination
+          v-model:current-page="query.page"
+          v-model:page-size="query.pageSize"
+          :total="total"
+          :page-sizes="[10, 20, 50]"
+          layout="total, sizes, prev, pager, next"
+          @size-change="loadList"
+          @current-change="loadList"
+        />
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, reactive, ref } from 'vue'
+
+type TableColumn = { prop: string; label: string; minWidth?: number; width?: number }
+
+const props = withDefaults(
+  defineProps<{
+    title: string
+    keywordPlaceholder?: string
+    columns: TableColumn[]
+    fetcher: (params: Record<string, any>) => Promise<any>
+  }>(),
+  {
+    keywordPlaceholder: '请输入关键词',
+  },
+)
+
+const loading = ref(false)
+const total = ref(0)
+const rows = ref<any[]>([])
+const query = reactive({
+  page: 1,
+  pageSize: 10,
+  keyword: '',
+})
+
+async function loadList() {
+  loading.value = true
+  try {
+    const res: any = await props.fetcher({ ...query })
+    const result = res?.result ?? {}
+    rows.value = result.records ?? result.list ?? []
+    total.value = Number(result.total ?? rows.value.length ?? 0)
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleSearch() {
+  query.page = 1
+  loadList()
+}
+
+function reset() {
+  query.page = 1
+  query.pageSize = 10
+  query.keyword = ''
+  loadList()
+}
+
+onMounted(() => {
+  loadList()
+})
+</script>
+
+<style scoped lang="scss">
+.table-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.filter-form {
+  margin-bottom: 8px;
+}
+.pager {
+  margin-top: 12px;
+  display: flex;
+  justify-content: flex-end;
+}
+</style>

+ 13 - 0
ui/sp-user-center/src/modules/otr/_shared/views/RoutePlaceholder.vue

@@ -0,0 +1,13 @@
+<template>
+  <module-stub :title="pageTitle" :desc="`迁移自 sp-tems-ui:${route.path}`" />
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useRoute } from 'vue-router'
+import ModuleStub from '@/modules/_shared/components/ModuleStub.vue'
+
+const route = useRoute()
+const pageTitle = computed(() => (route.meta?.title as string) || '功能迁移中')
+</script>
+

+ 24 - 0
ui/sp-user-center/src/modules/otr/ai/views/AddAi.vue

@@ -0,0 +1,24 @@
+<template>
+  <otr-simple-table-workspace
+    title="AI 工具"
+    keyword-placeholder="请输入工具名称"
+    :columns="columns"
+    :fetcher="aiToolList"
+  >
+    <template #header-extra>
+      <el-tag type="success">已接入 mock 数据</el-tag>
+    </template>
+  </otr-simple-table-workspace>
+</template>
+
+<script setup lang="ts">
+import { aiToolList } from '@/api/otr/ai/core'
+import OtrSimpleTableWorkspace from '@/modules/otr/_shared/components/OtrSimpleTableWorkspace.vue'
+
+const columns = [
+  { prop: 'name', label: '工具名称', minWidth: 220 },
+  { prop: 'scene', label: '应用场景', width: 160 },
+  { prop: 'ownerName', label: '维护人', width: 120 },
+  { prop: 'statusLabel', label: '状态', width: 120 },
+]
+</script>

+ 256 - 0
ui/sp-user-center/src/modules/otr/award/components/AwardWorkspace.vue

@@ -0,0 +1,256 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <div class="header">
+        <span>{{ title }}</span>
+        <div class="right">
+          <el-button type="primary" @click="openCreate">新增奖励</el-button>
+        </div>
+      </div>
+    </template>
+
+    <el-form :inline="true" @submit.prevent>
+      <el-form-item label="关键词">
+        <el-input v-model="query.keyword" clearable placeholder="请输入关键词" @keyup.enter="search" />
+      </el-form-item>
+      <el-form-item label="状态">
+        <el-select v-model="query.status" clearable style="width: 140px">
+          <el-option label="待审批" value="pending" />
+          <el-option label="已通过" value="approved" />
+          <el-option label="已驳回" value="rejected" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="时间">
+        <el-date-picker
+          v-model="query.range"
+          type="daterange"
+          value-format="YYYY-MM-DD"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          style="width: 260px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="search">查询</el-button>
+        <el-button @click="reset">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-tabs v-model="active" @tab-change="onTabChange">
+      <el-tab-pane label="奖励管理" name="list" />
+      <el-tab-pane label="奖金池管理" name="pool" />
+      <el-tab-pane label="奖励动态" name="trends" />
+    </el-tabs>
+
+    <el-table v-loading="loading" :data="rows" border stripe>
+      <el-table-column v-for="col in columns" :key="col.prop" :prop="col.prop" :label="col.label" :min-width="col.minWidth || 120" show-overflow-tooltip />
+      <el-table-column label="操作" width="140" fixed="right">
+        <template #default="{ row }">
+          <el-button link type="primary" @click="openDetail(row)">详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <div class="pager">
+      <el-pagination
+        v-model:current-page="query.page"
+        v-model:page-size="query.pageSize"
+        :total="total"
+        :page-sizes="[10, 20, 50]"
+        layout="total, sizes, prev, pager, next"
+        @size-change="load"
+        @current-change="load"
+      />
+    </div>
+  </el-card>
+
+  <el-dialog v-model="detailVisible" title="奖励详情" width="560px">
+    <el-descriptions :column="2" border>
+      <el-descriptions-item label="奖励名称">{{ detail.title || '-' }}</el-descriptions-item>
+      <el-descriptions-item label="申请人">{{ detail.ownerName || '-' }}</el-descriptions-item>
+      <el-descriptions-item label="金额">{{ detail.amount || '-' }}</el-descriptions-item>
+      <el-descriptions-item label="状态">{{ detail.statusLabel || '-' }}</el-descriptions-item>
+      <el-descriptions-item label="备注" :span="2">{{ detail.remark || '-' }}</el-descriptions-item>
+    </el-descriptions>
+  </el-dialog>
+
+  <el-dialog v-model="createVisible" title="新增奖励" width="620px">
+    <el-form label-width="90px">
+      <el-form-item label="奖励名称">
+        <el-input v-model="createForm.title" />
+      </el-form-item>
+      <el-form-item label="申请人">
+        <el-input v-model="createForm.ownerName" />
+      </el-form-item>
+      <el-form-item label="金额">
+        <el-input-number v-model="createForm.amount" :min="0" :step="100" />
+      </el-form-item>
+      <el-form-item label="备注">
+        <el-input v-model="createForm.remark" type="textarea" :rows="3" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="createVisible = false">取消</el-button>
+      <el-button type="primary" :loading="creating" @click="submitCreate">保存</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ElMessage } from 'element-plus'
+import { computed, onMounted, reactive, ref } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { awardAdd, awardGet, awardList, awardPoolList, awardTrendsList } from '@/api/otr/award/core'
+
+type Scene = 'list' | 'pool' | 'trends'
+const props = defineProps<{ title: string; scene: Scene }>()
+const router = useRouter()
+const route = useRoute()
+
+const active = ref<Scene>(props.scene)
+const loading = ref(false)
+const rows = ref<any[]>([])
+const total = ref(0)
+const detailVisible = ref(false)
+const detail = reactive<Record<string, any>>({})
+const createVisible = ref(false)
+const creating = ref(false)
+const createForm = reactive({
+  title: '',
+  ownerName: '',
+  amount: 0,
+  remark: '',
+})
+
+const query = reactive({
+  page: 1,
+  pageSize: 10,
+  keyword: '',
+  status: '',
+  range: [] as string[],
+})
+
+const columns = computed(() => {
+  if (active.value === 'pool') {
+    return [
+      { prop: 'poolName', label: '奖金池', minWidth: 220 },
+      { prop: 'amount', label: '余额' },
+      { prop: 'updatedAt', label: '更新时间', minWidth: 160 },
+    ]
+  }
+  if (active.value === 'trends') {
+    return [
+      { prop: 'title', label: '动态标题', minWidth: 240 },
+      { prop: 'operator', label: '操作人' },
+      { prop: 'createdAt', label: '时间', minWidth: 160 },
+    ]
+  }
+  return [
+    { prop: 'title', label: '奖励名称', minWidth: 220 },
+    { prop: 'ownerName', label: '申请人' },
+    { prop: 'amount', label: '奖励金额' },
+    { prop: 'statusLabel', label: '状态' },
+  ]
+})
+
+function buildParams() {
+  return {
+    page: query.page,
+    pageSize: query.pageSize,
+    keyword: query.keyword,
+    status: query.status,
+    start: query.range?.[0] || '',
+    end: query.range?.[1] || '',
+  }
+}
+
+async function load() {
+  loading.value = true
+  try {
+    const params = buildParams()
+    let res: any
+    if (active.value === 'pool') res = await awardPoolList(params)
+    else if (active.value === 'trends') res = await awardTrendsList(params)
+    else res = await awardList(params)
+    const rt = res?.result ?? {}
+    rows.value = rt.records ?? rt.list ?? []
+    total.value = Number(rt.total ?? rows.value.length)
+  } finally {
+    loading.value = false
+  }
+}
+
+function search() {
+  query.page = 1
+  load()
+}
+
+function reset() {
+  query.page = 1
+  query.pageSize = 10
+  query.keyword = ''
+  query.status = ''
+  query.range = []
+  load()
+}
+
+function onTabChange(name: string | number) {
+  const tab = String(name) as Scene
+  const map: Record<Scene, string> = {
+    list: '/otr/award/list',
+    pool: '/otr/award/pool',
+    trends: '/otr/award/trends',
+  }
+  router.push(map[tab])
+}
+
+async function openDetail(row: any) {
+  const id = row?.id ?? 1
+  const res: any = await awardGet(id)
+  Object.assign(detail, res?.result || row || {})
+  detailVisible.value = true
+}
+
+function openCreate() {
+  createVisible.value = true
+}
+
+async function submitCreate() {
+  if (!createForm.title) {
+    ElMessage.warning('请填写奖励名称')
+    return
+  }
+  creating.value = true
+  try {
+    await awardAdd({ ...createForm })
+    ElMessage.success('新增成功')
+    createVisible.value = false
+    createForm.title = ''
+    createForm.ownerName = ''
+    createForm.amount = 0
+    createForm.remark = ''
+    load()
+  } finally {
+    creating.value = false
+  }
+}
+
+onMounted(() => {
+  if (route.path.includes('/otr/award/pool')) active.value = 'pool'
+  if (route.path.includes('/otr/award/trends')) active.value = 'trends'
+  load()
+})
+</script>
+
+<style scoped>
+.header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.pager {
+  margin-top: 12px;
+  display: flex;
+  justify-content: flex-end;
+}
+</style>

+ 7 - 0
ui/sp-user-center/src/modules/otr/award/views/List.vue

@@ -0,0 +1,7 @@
+<template>
+  <award-workspace title="奖励管理" scene="list" />
+</template>
+
+<script setup lang="ts">
+import AwardWorkspace from '@/modules/otr/award/components/AwardWorkspace.vue'
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/award/views/Pool.vue

@@ -0,0 +1,7 @@
+<template>
+  <award-workspace title="奖金池管理" scene="pool" />
+</template>
+
+<script setup lang="ts">
+import AwardWorkspace from '@/modules/otr/award/components/AwardWorkspace.vue'
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/award/views/Trends.vue

@@ -0,0 +1,7 @@
+<template>
+  <award-workspace title="奖励动态" scene="trends" />
+</template>
+
+<script setup lang="ts">
+import AwardWorkspace from '@/modules/otr/award/components/AwardWorkspace.vue'
+</script>

+ 19 - 0
ui/sp-user-center/src/modules/otr/constants/targetMeta.ts

@@ -0,0 +1,19 @@
+/** 迁自 sp-tems-ui `config/status.js` 中与目标相关的枚举 */
+
+export const TARGET_STATUS_OPTIONS = [
+  { value: 1, label: '未开始' },
+  { value: 2, label: '进行中' },
+  { value: 3, label: '已完成' },
+  { value: 5, label: '已结束' },
+]
+
+export const TARGET_REMARK_OPTIONS = [
+  { value: 1, label: '年度目标' },
+  { value: 2, label: '季度目标' },
+  { value: 3, label: '月度目标' },
+]
+
+export function statusLabel(status: number | string | undefined) {
+  const n = Number(status)
+  return TARGET_STATUS_OPTIONS.find((x) => x.value === n)?.label ?? String(status ?? '')
+}

+ 225 - 0
ui/sp-user-center/src/modules/otr/effect/components/EffectWorkspace.vue

@@ -0,0 +1,225 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <div class="header">
+        <span>{{ title }}</span>
+        <div class="right">
+          <el-button type="primary" @click="goTemplateForm">新建考核模板</el-button>
+          <el-button @click="goContentForm">新建考核内容</el-button>
+        </div>
+      </div>
+    </template>
+
+    <el-form :inline="true" @submit.prevent>
+      <el-form-item label="关键词">
+        <el-input v-model="query.keyword" clearable placeholder="请输入考核名称/人员" @keyup.enter="search" />
+      </el-form-item>
+      <el-form-item label="状态">
+        <el-select v-model="query.status" clearable style="width: 140px">
+          <el-option label="进行中" value="processing" />
+          <el-option label="已完成" value="done" />
+          <el-option label="待审核" value="review" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="时间">
+        <el-date-picker
+          v-model="query.range"
+          type="daterange"
+          value-format="YYYY-MM-DD"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          style="width: 260px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="search">查询</el-button>
+        <el-button @click="reset">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-tabs v-model="active" @tab-change="onTabChange">
+      <el-tab-pane label="绩效管理" name="manage" />
+      <el-tab-pane label="我的绩效" name="mine" />
+      <el-tab-pane label="绩效看板" name="board" />
+      <el-tab-pane label="绩效待办" name="backlog" />
+      <el-tab-pane label="绩效模板" name="template" />
+    </el-tabs>
+
+    <el-table v-loading="loading" :data="rows" border stripe>
+      <el-table-column v-for="col in columns" :key="col.prop" :prop="col.prop" :label="col.label" :min-width="col.minWidth || 120" show-overflow-tooltip />
+      <el-table-column label="操作" width="140" fixed="right">
+        <template #default="{ row }">
+          <el-button link type="primary" @click="goDetail(row)">详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <div class="pager">
+      <el-pagination
+        v-model:current-page="query.page"
+        v-model:page-size="query.pageSize"
+        :total="total"
+        :page-sizes="[10, 20, 50]"
+        layout="total, sizes, prev, pager, next"
+        @size-change="load"
+        @current-change="load"
+      />
+    </div>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { effectBacklogList, effectBoardList, effectManageList, effectMineList, effectTemplateList } from '@/api/otr/effect/core'
+
+type Scene = 'manage' | 'mine' | 'board' | 'backlog' | 'template'
+const props = defineProps<{ title: string; scene: Scene }>()
+
+const router = useRouter()
+const route = useRoute()
+const active = ref<Scene>(props.scene)
+const loading = ref(false)
+const rows = ref<any[]>([])
+const total = ref(0)
+const query = reactive({
+  page: 1,
+  pageSize: 10,
+  keyword: '',
+  status: '',
+  range: [] as string[],
+})
+
+const columns = computed(() => {
+  if (active.value === 'mine') {
+    return [
+      { prop: 'title', label: '考核名称', minWidth: 220 },
+      { prop: 'score', label: '当前得分' },
+      { prop: 'rank', label: '排名' },
+      { prop: 'statusLabel', label: '状态' },
+    ]
+  }
+  if (active.value === 'board') {
+    return [
+      { prop: 'ownerName', label: '负责人' },
+      { prop: 'deptName', label: '部门' },
+      { prop: 'targetCount', label: '考核数' },
+      { prop: 'avgScore', label: '平均得分' },
+    ]
+  }
+  if (active.value === 'backlog') {
+    return [
+      { prop: 'title', label: '待办标题', minWidth: 220 },
+      { prop: 'ownerName', label: '负责人' },
+      { prop: 'deadline', label: '截止时间', minWidth: 160 },
+      { prop: 'statusLabel', label: '状态' },
+    ]
+  }
+  if (active.value === 'template') {
+    return [
+      { prop: 'name', label: '模板名称', minWidth: 220 },
+      { prop: 'scene', label: '适用场景' },
+      { prop: 'updatedBy', label: '更新人' },
+      { prop: 'updatedAt', label: '更新时间', minWidth: 160 },
+    ]
+  }
+  return [
+    { prop: 'title', label: '考核名称', minWidth: 220 },
+    { prop: 'ownerName', label: '负责人' },
+    { prop: 'cycleName', label: '考核周期' },
+    { prop: 'statusLabel', label: '状态' },
+  ]
+})
+
+function buildParams() {
+  return {
+    page: query.page,
+    pageSize: query.pageSize,
+    keyword: query.keyword,
+    status: query.status,
+    start: query.range?.[0] || '',
+    end: query.range?.[1] || '',
+  }
+}
+
+async function load() {
+  loading.value = true
+  try {
+    const params = buildParams()
+    let res: any
+    if (active.value === 'mine') res = await effectMineList(params)
+    else if (active.value === 'board') res = await effectBoardList(params)
+    else if (active.value === 'backlog') res = await effectBacklogList(params)
+    else if (active.value === 'template') res = await effectTemplateList(params)
+    else res = await effectManageList(params)
+    const rt = res?.result ?? {}
+    rows.value = rt.records ?? rt.list ?? []
+    total.value = Number(rt.total ?? rows.value.length)
+  } finally {
+    loading.value = false
+  }
+}
+
+function search() {
+  query.page = 1
+  load()
+}
+
+function reset() {
+  query.page = 1
+  query.pageSize = 10
+  query.keyword = ''
+  query.status = ''
+  query.range = []
+  load()
+}
+
+function onTabChange(name: string | number) {
+  const tab = String(name) as Scene
+  const map: Record<Scene, string> = {
+    manage: '/otr/effect/manage',
+    mine: '/otr/effect/mine',
+    board: '/otr/effect/board',
+    backlog: '/otr/effect/backlog',
+    template: '/otr/effect/template',
+  }
+  router.push(map[tab])
+}
+
+function goDetail(row: any) {
+  router.push({ path: '/otr/effect/detail', query: { id: String(row.id || '') } })
+}
+
+function goTemplateForm() {
+  router.push('/otr/effect/template-form')
+}
+
+function goContentForm() {
+  router.push('/otr/effect/content-form')
+}
+
+onMounted(() => {
+  if (route.path.includes('/otr/effect/mine')) active.value = 'mine'
+  if (route.path.includes('/otr/effect/board')) active.value = 'board'
+  if (route.path.includes('/otr/effect/backlog')) active.value = 'backlog'
+  if (route.path.includes('/otr/effect/template')) active.value = 'template'
+  load()
+})
+</script>
+
+<style scoped>
+.header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.right {
+  display: flex;
+  gap: 8px;
+}
+.pager {
+  margin-top: 12px;
+  display: flex;
+  justify-content: flex-end;
+}
+</style>

+ 7 - 0
ui/sp-user-center/src/modules/otr/effect/views/Backlog.vue

@@ -0,0 +1,7 @@
+<template>
+  <effect-workspace title="绩效待办" scene="backlog" />
+</template>
+
+<script setup lang="ts">
+import EffectWorkspace from '@/modules/otr/effect/components/EffectWorkspace.vue'
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/effect/views/Board.vue

@@ -0,0 +1,7 @@
+<template>
+  <effect-workspace title="绩效看板" scene="board" />
+</template>
+
+<script setup lang="ts">
+import EffectWorkspace from '@/modules/otr/effect/components/EffectWorkspace.vue'
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/effect/views/ContentForm.vue

@@ -0,0 +1,7 @@
+<template>
+  <otr-simple-form-scene title="新建考核内容" />
+</template>
+
+<script setup lang="ts">
+import OtrSimpleFormScene from '@/modules/otr/_shared/components/OtrSimpleFormScene.vue'
+</script>

+ 14 - 0
ui/sp-user-center/src/modules/otr/effect/views/Dept.vue

@@ -0,0 +1,14 @@
+<template>
+  <otr-simple-table-workspace title="部门绩效" keyword-placeholder="请输入部门名称" :columns="columns" :fetcher="effectDeptList" />
+</template>
+
+<script setup lang="ts">
+import { effectDeptList } from '@/api/otr/effect/core'
+import OtrSimpleTableWorkspace from '@/modules/otr/_shared/components/OtrSimpleTableWorkspace.vue'
+
+const columns = [
+  { prop: 'deptName', label: '部门', width: 140 },
+  { prop: 'targetCount', label: '考核数', width: 100 },
+  { prop: 'avgScore', label: '平均分', width: 120 },
+]
+</script>

+ 15 - 0
ui/sp-user-center/src/modules/otr/effect/views/Detail.vue

@@ -0,0 +1,15 @@
+<template>
+  <otr-simple-table-workspace title="绩效详情" keyword-placeholder="请输入考核名称" :columns="columns" :fetcher="effectDetailList" />
+</template>
+
+<script setup lang="ts">
+import { effectDetailList } from '@/api/otr/effect/core'
+import OtrSimpleTableWorkspace from '@/modules/otr/_shared/components/OtrSimpleTableWorkspace.vue'
+
+const columns = [
+  { prop: 'title', label: '考核名称', minWidth: 220 },
+  { prop: 'ownerName', label: '负责人', width: 120 },
+  { prop: 'score', label: '得分', width: 100 },
+  { prop: 'statusLabel', label: '状态', width: 120 },
+]
+</script>

+ 14 - 0
ui/sp-user-center/src/modules/otr/effect/views/Evaluated.vue

@@ -0,0 +1,14 @@
+<template>
+  <otr-simple-table-workspace title="待评绩效" keyword-placeholder="请输入考核名称" :columns="columns" :fetcher="effectEvaluatedList" />
+</template>
+
+<script setup lang="ts">
+import { effectEvaluatedList } from '@/api/otr/effect/core'
+import OtrSimpleTableWorkspace from '@/modules/otr/_shared/components/OtrSimpleTableWorkspace.vue'
+
+const columns = [
+  { prop: 'title', label: '考核名称', minWidth: 220 },
+  { prop: 'evaluateeName', label: '被评人', width: 120 },
+  { prop: 'deadline', label: '截止时间', width: 180 },
+]
+</script>

+ 15 - 0
ui/sp-user-center/src/modules/otr/effect/views/Index.vue

@@ -0,0 +1,15 @@
+<template>
+  <otr-simple-table-workspace title="全部绩效" keyword-placeholder="请输入考核名称" :columns="columns" :fetcher="effectIndexList" />
+</template>
+
+<script setup lang="ts">
+import { effectIndexList } from '@/api/otr/effect/core'
+import OtrSimpleTableWorkspace from '@/modules/otr/_shared/components/OtrSimpleTableWorkspace.vue'
+
+const columns = [
+  { prop: 'title', label: '考核名称', minWidth: 220 },
+  { prop: 'ownerName', label: '负责人', width: 120 },
+  { prop: 'deptName', label: '部门', width: 140 },
+  { prop: 'statusLabel', label: '状态', width: 120 },
+]
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/effect/views/Manage.vue

@@ -0,0 +1,7 @@
+<template>
+  <effect-workspace title="绩效管理" scene="manage" />
+</template>
+
+<script setup lang="ts">
+import EffectWorkspace from '@/modules/otr/effect/components/EffectWorkspace.vue'
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/effect/views/Mine.vue

@@ -0,0 +1,7 @@
+<template>
+  <effect-workspace title="我的绩效" scene="mine" />
+</template>
+
+<script setup lang="ts">
+import EffectWorkspace from '@/modules/otr/effect/components/EffectWorkspace.vue'
+</script>

+ 14 - 0
ui/sp-user-center/src/modules/otr/effect/views/Objection.vue

@@ -0,0 +1,14 @@
+<template>
+  <otr-simple-table-workspace title="异议管理" keyword-placeholder="请输入异议标题" :columns="columns" :fetcher="effectObjectionList" />
+</template>
+
+<script setup lang="ts">
+import { effectObjectionList } from '@/api/otr/effect/core'
+import OtrSimpleTableWorkspace from '@/modules/otr/_shared/components/OtrSimpleTableWorkspace.vue'
+
+const columns = [
+  { prop: 'title', label: '异议标题', minWidth: 220 },
+  { prop: 'ownerName', label: '提出人', width: 120 },
+  { prop: 'statusLabel', label: '处理状态', width: 120 },
+]
+</script>

+ 15 - 0
ui/sp-user-center/src/modules/otr/effect/views/Person.vue

@@ -0,0 +1,15 @@
+<template>
+  <otr-simple-table-workspace title="个人绩效" keyword-placeholder="请输入姓名" :columns="columns" :fetcher="effectPersonList" />
+</template>
+
+<script setup lang="ts">
+import { effectPersonList } from '@/api/otr/effect/core'
+import OtrSimpleTableWorkspace from '@/modules/otr/_shared/components/OtrSimpleTableWorkspace.vue'
+
+const columns = [
+  { prop: 'staffName', label: '姓名', width: 120 },
+  { prop: 'deptName', label: '部门', width: 140 },
+  { prop: 'score', label: '得分', width: 100 },
+  { prop: 'rank', label: '排名', width: 100 },
+]
+</script>

+ 14 - 0
ui/sp-user-center/src/modules/otr/effect/views/Review.vue

@@ -0,0 +1,14 @@
+<template>
+  <otr-simple-table-workspace title="待审绩效" keyword-placeholder="请输入考核名称" :columns="columns" :fetcher="effectReviewList" />
+</template>
+
+<script setup lang="ts">
+import { effectReviewList } from '@/api/otr/effect/core'
+import OtrSimpleTableWorkspace from '@/modules/otr/_shared/components/OtrSimpleTableWorkspace.vue'
+
+const columns = [
+  { prop: 'title', label: '考核名称', minWidth: 220 },
+  { prop: 'applicantName', label: '提交人', width: 120 },
+  { prop: 'statusLabel', label: '状态', width: 120 },
+]
+</script>

+ 14 - 0
ui/sp-user-center/src/modules/otr/effect/views/Setting.vue

@@ -0,0 +1,14 @@
+<template>
+  <otr-simple-table-workspace title="绩效设置" keyword-placeholder="请输入配置项" :columns="columns" :fetcher="effectSettingList" />
+</template>
+
+<script setup lang="ts">
+import { effectSettingList } from '@/api/otr/effect/core'
+import OtrSimpleTableWorkspace from '@/modules/otr/_shared/components/OtrSimpleTableWorkspace.vue'
+
+const columns = [
+  { prop: 'name', label: '配置项', minWidth: 220 },
+  { prop: 'value', label: '配置值', minWidth: 180 },
+  { prop: 'updatedAt', label: '更新时间', width: 180 },
+]
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/effect/views/Template.vue

@@ -0,0 +1,7 @@
+<template>
+  <effect-workspace title="绩效模板" scene="template" />
+</template>
+
+<script setup lang="ts">
+import EffectWorkspace from '@/modules/otr/effect/components/EffectWorkspace.vue'
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/effect/views/TemplateForm.vue

@@ -0,0 +1,7 @@
+<template>
+  <otr-simple-form-scene title="新建考核模板" />
+</template>
+
+<script setup lang="ts">
+import OtrSimpleFormScene from '@/modules/otr/_shared/components/OtrSimpleFormScene.vue'
+</script>

+ 14 - 0
ui/sp-user-center/src/modules/otr/effect/views/Whitelist.vue

@@ -0,0 +1,14 @@
+<template>
+  <otr-simple-table-workspace title="白名单" keyword-placeholder="请输入姓名" :columns="columns" :fetcher="effectWhitelistList" />
+</template>
+
+<script setup lang="ts">
+import { effectWhitelistList } from '@/api/otr/effect/core'
+import OtrSimpleTableWorkspace from '@/modules/otr/_shared/components/OtrSimpleTableWorkspace.vue'
+
+const columns = [
+  { prop: 'staffName', label: '姓名', width: 120 },
+  { prop: 'deptName', label: '部门', width: 140 },
+  { prop: 'remark', label: '备注', minWidth: 220 },
+]
+</script>

+ 20 - 0
ui/sp-user-center/src/modules/otr/notice/views/List.vue

@@ -0,0 +1,20 @@
+<template>
+  <otr-simple-table-workspace
+    title="系统通知"
+    keyword-placeholder="请输入通知标题"
+    :columns="columns"
+    :fetcher="noticeList"
+  />
+</template>
+
+<script setup lang="ts">
+import { noticeList } from '@/api/otr/notice/core'
+import OtrSimpleTableWorkspace from '@/modules/otr/_shared/components/OtrSimpleTableWorkspace.vue'
+
+const columns = [
+  { prop: 'title', label: '通知标题', minWidth: 240 },
+  { prop: 'type', label: '类型', width: 120 },
+  { prop: 'senderName', label: '发送人', width: 120 },
+  { prop: 'createdAt', label: '时间', width: 180 },
+]
+</script>

+ 21 - 0
ui/sp-user-center/src/modules/otr/report/views/PersonReport.vue

@@ -0,0 +1,21 @@
+<template>
+  <otr-simple-table-workspace
+    title="人员报告"
+    keyword-placeholder="请输入姓名/部门"
+    :columns="columns"
+    :fetcher="personReportList"
+  />
+</template>
+
+<script setup lang="ts">
+import { personReportList } from '@/api/otr/report/core'
+import OtrSimpleTableWorkspace from '@/modules/otr/_shared/components/OtrSimpleTableWorkspace.vue'
+
+const columns = [
+  { prop: 'staffName', label: '姓名', width: 120 },
+  { prop: 'deptName', label: '部门', width: 140 },
+  { prop: 'targetCount', label: '目标数', width: 100 },
+  { prop: 'progress', label: '平均进度', width: 120 },
+  { prop: 'effectScore', label: '绩效得分', width: 120 },
+]
+</script>

+ 96 - 0
ui/sp-user-center/src/modules/otr/summary/components/SummaryFormScene.vue

@@ -0,0 +1,96 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <div class="header">{{ title }}</div>
+    </template>
+
+    <el-form label-width="100px" style="max-width: 760px">
+      <el-form-item label="标题" required>
+        <el-input v-model="form.title" placeholder="请输入标题" />
+      </el-form-item>
+      <el-form-item label="类型">
+        <el-select v-model="form.summaryType" style="width: 160px">
+          <el-option label="日报" value="1" />
+          <el-option label="周报" value="2" />
+          <el-option label="月报" value="3" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="时间区间">
+        <el-date-picker
+          v-model="range"
+          type="daterange"
+          value-format="YYYY-MM-DD"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          style="width: 260px"
+        />
+      </el-form-item>
+      <el-form-item label="内容">
+        <el-input v-model="form.content" type="textarea" :rows="6" placeholder="请输入简报内容" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
+        <el-button @click="load">重载</el-button>
+      </el-form-item>
+    </el-form>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import { ElMessage } from 'element-plus'
+import { onMounted, reactive, ref } from 'vue'
+import { useRoute } from 'vue-router'
+import { summaryAdd, summaryEdit, summaryGet } from '@/api/otr/summary/core'
+
+const props = defineProps<{ title: string; isShare?: boolean }>()
+const route = useRoute()
+const submitting = ref(false)
+const range = ref<string[]>([])
+const form = reactive<Record<string, any>>({
+  id: '',
+  title: '',
+  summaryType: '2',
+  content: '',
+})
+
+async function load() {
+  const id = String(route.query.id || '')
+  if (!id) return
+  const res: any = await summaryGet(id)
+  const rt = res?.result || {}
+  form.id = rt.id || id
+  form.title = rt.title || ''
+  form.summaryType = String(rt.summaryType || '2')
+  form.content = rt.content || ''
+  range.value = [rt.beginDate || '', rt.endDate || ''].filter(Boolean)
+}
+
+async function submit() {
+  if (!form.title) {
+    ElMessage.warning('请填写标题')
+    return
+  }
+  submitting.value = true
+  try {
+    const payload = {
+      ...form,
+      beginDate: range.value?.[0] || '',
+      endDate: range.value?.[1] || '',
+      shareMode: props.isShare ? 1 : 0,
+    }
+    if (form.id) await summaryEdit(payload)
+    else await summaryAdd(payload)
+    ElMessage.success('保存成功')
+  } finally {
+    submitting.value = false
+  }
+}
+
+onMounted(load)
+</script>
+
+<style scoped>
+.header {
+  font-weight: 600;
+}
+</style>

+ 205 - 0
ui/sp-user-center/src/modules/otr/summary/components/SummaryWorkspace.vue

@@ -0,0 +1,205 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <div class="header">
+        <span>{{ title }}</span>
+        <el-button type="primary" @click="goCreate">新增简报</el-button>
+      </div>
+    </template>
+
+    <el-form :inline="true" @submit.prevent>
+      <el-form-item label="关键词">
+        <el-input v-model="query.kws" clearable placeholder="请输入标题/内容关键字" @keyup.enter="search" />
+      </el-form-item>
+      <el-form-item label="类型">
+        <el-select v-model="query.summaryType" clearable style="width: 140px">
+          <el-option label="日报" value="1" />
+          <el-option label="周报" value="2" />
+          <el-option label="月报" value="3" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态">
+        <el-select v-model="query.status" clearable style="width: 140px">
+          <el-option label="待审阅" value="0" />
+          <el-option label="已通过" value="1" />
+          <el-option label="未通过" value="2" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="时间">
+        <el-date-picker
+          v-model="query.range"
+          type="daterange"
+          value-format="YYYY-MM-DD"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          style="width: 260px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="search">查询</el-button>
+        <el-button @click="reset">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-table v-loading="loading" :data="rows" border stripe>
+      <el-table-column v-for="col in columns" :key="col.prop" :prop="col.prop" :label="col.label" :min-width="col.minWidth || 120" show-overflow-tooltip />
+      <el-table-column label="操作" width="150" fixed="right">
+        <template #default="{ row }">
+          <el-button link type="primary" @click="goDetail(row)">查看</el-button>
+          <el-button v-if="scene === 'list' || scene === 'draft'" link @click="goEdit(row)">编辑</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <div class="pager">
+      <el-pagination
+        v-model:current-page="query.page"
+        v-model:page-size="query.pageSize"
+        :total="total"
+        :page-sizes="[10, 20, 50]"
+        layout="total, sizes, prev, pager, next"
+        @size-change="load"
+        @current-change="load"
+      />
+    </div>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { summaryAnnouncementList, summaryDraftList, summaryList, summaryMemberList, summaryShareList, summaryStatisticsList } from '@/api/otr/summary/core'
+
+type Scene = 'list' | 'member' | 'statistics' | 'draft' | 'share' | 'announcement'
+const props = defineProps<{ title: string; scene: Scene }>()
+const router = useRouter()
+const loading = ref(false)
+const rows = ref<any[]>([])
+const total = ref(0)
+
+const query = reactive({
+  page: 1,
+  pageSize: 10,
+  kws: '',
+  summaryType: '',
+  status: '',
+  range: [] as string[],
+})
+
+const columns = computed(() => {
+  if (props.scene === 'member') {
+    return [
+      { prop: 'memberName', label: '成员' },
+      { prop: 'title', label: '简报标题', minWidth: 240 },
+      { prop: 'period', label: '周期' },
+      { prop: 'createdAt', label: '提交时间', minWidth: 160 },
+    ]
+  }
+  if (props.scene === 'statistics') {
+    return [
+      { prop: 'dimension', label: '统计维度', minWidth: 180 },
+      { prop: 'value', label: '数值' },
+      { prop: 'ratio', label: '占比' },
+    ]
+  }
+  if (props.scene === 'draft') {
+    return [
+      { prop: 'title', label: '草稿标题', minWidth: 240 },
+      { prop: 'ownerName', label: '作者' },
+      { prop: 'updatedAt', label: '更新时间', minWidth: 160 },
+    ]
+  }
+  if (props.scene === 'share') {
+    return [
+      { prop: 'title', label: '分享标题', minWidth: 240 },
+      { prop: 'author', label: '分享人' },
+      { prop: 'likes', label: '点赞' },
+      { prop: 'createdAt', label: '发布时间', minWidth: 160 },
+    ]
+  }
+  if (props.scene === 'announcement') {
+    return [
+      { prop: 'title', label: '公告标题', minWidth: 240 },
+      { prop: 'publisher', label: '发布人' },
+      { prop: 'createdAt', label: '发布时间', minWidth: 160 },
+    ]
+  }
+  return [
+    { prop: 'title', label: '简报标题', minWidth: 240 },
+    { prop: 'ownerName', label: '提交人' },
+    { prop: 'period', label: '周期' },
+    { prop: 'createdAt', label: '提交时间', minWidth: 160 },
+  ]
+})
+
+async function load() {
+  loading.value = true
+  try {
+    const params = {
+      pageNo: query.page,
+      pageSize: query.pageSize,
+      kws: query.kws,
+      summaryType: query.summaryType,
+      status: query.status,
+      start: query.range?.[0] || '',
+      end: query.range?.[1] || '',
+      scene: props.scene,
+    }
+    let res: any
+    if (props.scene === 'member') res = await summaryMemberList(params)
+    else if (props.scene === 'statistics') res = await summaryStatisticsList(params)
+    else if (props.scene === 'draft') res = await summaryDraftList(params)
+    else if (props.scene === 'share') res = await summaryShareList(params)
+    else if (props.scene === 'announcement') res = await summaryAnnouncementList(params)
+    else res = await summaryList(params)
+    const result = res?.result ?? {}
+    rows.value = result.records ?? result.list ?? []
+    total.value = Number(result.total ?? rows.value.length)
+  } finally {
+    loading.value = false
+  }
+}
+
+function search() {
+  query.page = 1
+  load()
+}
+
+function reset() {
+  query.page = 1
+  query.pageSize = 10
+  query.kws = ''
+  query.summaryType = ''
+  query.status = ''
+  query.range = []
+  load()
+}
+
+function goCreate() {
+  router.push('/otr/summary/create')
+}
+
+function goEdit(row: any) {
+  router.push({ path: '/otr/summary/form', query: { id: String(row.id || '') } })
+}
+
+function goDetail(row: any) {
+  const path = props.scene === 'share' ? '/otr/summary/share-detail' : '/otr/summary/form'
+  router.push({ path, query: { id: String(row.id || '') } })
+}
+
+onMounted(load)
+</script>
+
+<style scoped>
+.header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.pager {
+  margin-top: 12px;
+  display: flex;
+  justify-content: flex-end;
+}
+</style>

+ 7 - 0
ui/sp-user-center/src/modules/otr/summary/views/Announcement.vue

@@ -0,0 +1,7 @@
+<template>
+  <summary-workspace title="公告列表" scene="announcement" />
+</template>
+
+<script setup lang="ts">
+import SummaryWorkspace from '@/modules/otr/summary/components/SummaryWorkspace.vue'
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/summary/views/Create.vue

@@ -0,0 +1,7 @@
+<template>
+  <summary-form-scene title="新建简报" />
+</template>
+
+<script setup lang="ts">
+import SummaryFormScene from '@/modules/otr/summary/components/SummaryFormScene.vue'
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/summary/views/Draft.vue

@@ -0,0 +1,7 @@
+<template>
+  <summary-workspace title="草稿箱" scene="draft" />
+</template>
+
+<script setup lang="ts">
+import SummaryWorkspace from '@/modules/otr/summary/components/SummaryWorkspace.vue'
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/summary/views/Form.vue

@@ -0,0 +1,7 @@
+<template>
+  <summary-form-scene title="简报表单" />
+</template>
+
+<script setup lang="ts">
+import SummaryFormScene from '@/modules/otr/summary/components/SummaryFormScene.vue'
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/summary/views/List.vue

@@ -0,0 +1,7 @@
+<template>
+  <summary-workspace title="工作总结" scene="list" />
+</template>
+
+<script setup lang="ts">
+import SummaryWorkspace from '@/modules/otr/summary/components/SummaryWorkspace.vue'
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/summary/views/Member.vue

@@ -0,0 +1,7 @@
+<template>
+  <summary-workspace title="成员简报" scene="member" />
+</template>
+
+<script setup lang="ts">
+import SummaryWorkspace from '@/modules/otr/summary/components/SummaryWorkspace.vue'
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/summary/views/RemindTask.vue

@@ -0,0 +1,7 @@
+<template>
+  <otr-simple-form-scene title="简报提醒" />
+</template>
+
+<script setup lang="ts">
+import OtrSimpleFormScene from '@/modules/otr/_shared/components/OtrSimpleFormScene.vue'
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/summary/views/Settings.vue

@@ -0,0 +1,7 @@
+<template>
+  <otr-simple-form-scene title="简报设置" />
+</template>
+
+<script setup lang="ts">
+import OtrSimpleFormScene from '@/modules/otr/_shared/components/OtrSimpleFormScene.vue'
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/summary/views/Share.vue

@@ -0,0 +1,7 @@
+<template>
+  <summary-workspace title="心得分享" scene="share" />
+</template>
+
+<script setup lang="ts">
+import SummaryWorkspace from '@/modules/otr/summary/components/SummaryWorkspace.vue'
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/summary/views/ShareDetail.vue

@@ -0,0 +1,7 @@
+<template>
+  <summary-form-scene title="心得详情" is-share />
+</template>
+
+<script setup lang="ts">
+import SummaryFormScene from '@/modules/otr/summary/components/SummaryFormScene.vue'
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/summary/views/Statistics.vue

@@ -0,0 +1,7 @@
+<template>
+  <summary-workspace title="简报统计" scene="statistics" />
+</template>
+
+<script setup lang="ts">
+import SummaryWorkspace from '@/modules/otr/summary/components/SummaryWorkspace.vue'
+</script>

+ 199 - 0
ui/sp-user-center/src/modules/otr/task/components/TaskWorkspace.vue

@@ -0,0 +1,199 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <div class="header">
+        <span>{{ title }}</span>
+        <div class="right">
+          <el-button type="primary" @click="goCreate">新建任务</el-button>
+        </div>
+      </div>
+    </template>
+
+    <el-form :inline="true" @submit.prevent>
+      <el-form-item label="关键词">
+        <el-input v-model="query.kws" clearable placeholder="请输入任务关键字" @keyup.enter="handleSearch" />
+      </el-form-item>
+      <el-form-item label="状态">
+        <el-select v-model="query.status" multiple collapse-tags collapse-tags-tooltip clearable style="width: 180px">
+          <el-option label="未开始" value="1" />
+          <el-option label="进行中" value="2" />
+          <el-option label="已完成" value="3" />
+          <el-option label="已逾期" value="5" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="日期">
+        <el-date-picker
+          v-model="query.range"
+          type="daterange"
+          value-format="YYYY-MM-DD"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          style="width: 260px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="handleSearch">查询</el-button>
+        <el-button @click="reset">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-tabs v-model="active" @tab-change="onTabChange">
+      <el-tab-pane label="我的任务" name="list" />
+      <el-tab-pane label="成员任务" name="member" />
+      <el-tab-pane label="@我的任务" name="at-list" />
+      <el-tab-pane label="任务看板" name="table" />
+    </el-tabs>
+
+    <el-table v-loading="loading" :data="rows" border stripe>
+      <el-table-column v-for="col in columns" :key="col.prop" :prop="col.prop" :label="col.label" :min-width="col.minWidth || 120" />
+      <el-table-column label="操作" width="140" fixed="right">
+        <template #default="{ row }">
+          <el-button link type="primary" @click="goDetail(row)">详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <div class="pager">
+      <el-pagination
+        v-model:current-page="query.page"
+        v-model:page-size="query.pageSize"
+        :total="total"
+        :page-sizes="[10, 20, 50]"
+        layout="total, sizes, prev, pager, next"
+        @size-change="load"
+        @current-change="load"
+      />
+    </div>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { taskListAtMine, taskListByTable, taskListMineGantt, taskListSubordinateGantt } from '@/api/otr/task/core'
+
+const props = defineProps<{ title: string; scene: 'list' | 'member' | 'at-list' | 'table' }>()
+const router = useRouter()
+const route = useRoute()
+
+const active = ref(props.scene)
+const loading = ref(false)
+const total = ref(0)
+const rows = ref<any[]>([])
+const query = reactive({
+  page: 1,
+  pageSize: 10,
+  kws: '',
+  status: [] as string[],
+  range: [] as string[],
+})
+
+const columns = computed(() => {
+  if (active.value === 'member') {
+    return [
+      { prop: 'memberName', label: '成员' },
+      { prop: 'name', label: '任务名称', minWidth: 220 },
+      { prop: 'deadline', label: '截止时间', minWidth: 160 },
+      { prop: 'statusLabel', label: '状态' },
+    ]
+  }
+  if (active.value === 'at-list') {
+    return [
+      { prop: 'name', label: '任务名称', minWidth: 220 },
+      { prop: 'fromName', label: '@我人' },
+      { prop: 'createdAt', label: '时间', minWidth: 160 },
+      { prop: 'statusLabel', label: '状态' },
+    ]
+  }
+  if (active.value === 'table') {
+    return [
+      { prop: 'name', label: '任务名称', minWidth: 220 },
+      { prop: 'ownerName', label: '负责人' },
+      { prop: 'progress', label: '进度' },
+      { prop: 'statusLabel', label: '状态' },
+    ]
+  }
+  return [
+    { prop: 'name', label: '任务名称', minWidth: 220 },
+    { prop: 'ownerName', label: '负责人' },
+    { prop: 'priority', label: '优先级' },
+    { prop: 'deadline', label: '截止时间', minWidth: 160 },
+    { prop: 'statusLabel', label: '状态' },
+  ]
+})
+
+function buildParams() {
+  return {
+    page: query.page,
+    psize: query.pageSize,
+    kws: query.kws,
+    sta: query.status,
+    start: query.range?.[0] || '',
+    end: query.range?.[1] || '',
+  }
+}
+
+async function load() {
+  loading.value = true
+  try {
+    const params = buildParams()
+    let res: any
+    if (active.value === 'member') res = await taskListSubordinateGantt(params)
+    else if (active.value === 'at-list') res = await taskListAtMine(params)
+    else if (active.value === 'table') res = await taskListByTable(params)
+    else res = await taskListMineGantt(params)
+    const rt = res?.result ?? {}
+    rows.value = rt.records ?? rt.list ?? []
+    total.value = Number(rt.total ?? rows.value.length)
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleSearch() {
+  query.page = 1
+  load()
+}
+
+function reset() {
+  query.page = 1
+  query.pageSize = 10
+  query.kws = ''
+  query.status = []
+  query.range = []
+  load()
+}
+
+function goCreate() {
+  router.push('/otr/task/form')
+}
+
+function goDetail(row: any) {
+  router.push({ path: '/otr/task/detail', query: { id: String(row.taskDateId ?? row.id ?? '') } })
+}
+
+function onTabChange(name: string | number) {
+  const tab = String(name)
+  router.push(`/otr/task/${tab}`)
+}
+
+onMounted(() => {
+  if (route.path.includes('/otr/task/member')) active.value = 'member'
+  if (route.path.includes('/otr/task/at-list')) active.value = 'at-list'
+  if (route.path.includes('/otr/task/table')) active.value = 'table'
+  load()
+})
+</script>
+
+<style scoped>
+.header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.pager {
+  margin-top: 12px;
+  display: flex;
+  justify-content: flex-end;
+}
+</style>

+ 7 - 0
ui/sp-user-center/src/modules/otr/task/views/AtList.vue

@@ -0,0 +1,7 @@
+<template>
+  <task-workspace title="@我的任务" scene="at-list" />
+</template>
+
+<script setup lang="ts">
+import TaskWorkspace from '@/modules/otr/task/components/TaskWorkspace.vue'
+</script>

+ 137 - 0
ui/sp-user-center/src/modules/otr/task/views/Detail.vue

@@ -0,0 +1,137 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <div class="header">
+        <span>任务详情</span>
+        <div>
+          <el-input v-model="taskIdInput" placeholder="任务ID" style="width: 140px; margin-right: 8px" />
+          <el-button type="primary" @click="load">查询</el-button>
+        </div>
+      </div>
+    </template>
+
+    <el-descriptions v-loading="loading" :column="2" border>
+      <el-descriptions-item label="任务ID">{{ detail.id || '-' }}</el-descriptions-item>
+      <el-descriptions-item label="任务标题">{{ detail.title || detail.name || '-' }}</el-descriptions-item>
+      <el-descriptions-item label="负责人">{{ detail.leaderName || detail.ownerName || '-' }}</el-descriptions-item>
+      <el-descriptions-item label="状态">{{ detail.statusLabel || '-' }}</el-descriptions-item>
+      <el-descriptions-item label="开始时间">{{ detail.startTime || '-' }}</el-descriptions-item>
+      <el-descriptions-item label="结束时间">{{ detail.endTime || '-' }}</el-descriptions-item>
+      <el-descriptions-item label="内容" :span="2">{{ detail.content || '-' }}</el-descriptions-item>
+    </el-descriptions>
+
+    <el-tabs v-model="activeTab" style="margin-top: 16px">
+      <el-tab-pane label="日志记录" name="log">
+        <el-timeline>
+          <el-timeline-item v-for="(item, idx) in logs" :key="`l-${idx}`" :timestamp="item.createTime || '-'">
+            <div>{{ item.content || '-' }}</div>
+          </el-timeline-item>
+        </el-timeline>
+      </el-tab-pane>
+
+      <el-tab-pane label="评论" name="comment">
+        <div class="comment-editor">
+          <el-input v-model="commentContent" type="textarea" :rows="3" placeholder="输入评论内容" />
+          <div style="margin-top: 8px">
+            <el-button type="primary" :loading="commentSubmitting" @click="submitComment">发表评论</el-button>
+          </div>
+        </div>
+        <el-empty v-if="!comments.length" description="暂无评论" />
+        <el-card v-for="(item, idx) in comments" :key="`c-${idx}`" class="comment-item" shadow="never">
+          <div class="comment-meta">
+            <span>{{ item.staffName || '匿名' }}</span>
+            <span>{{ item.createTime || '-' }}</span>
+          </div>
+          <div class="comment-content">{{ item.content || '-' }}</div>
+        </el-card>
+      </el-tab-pane>
+
+      <el-tab-pane label="附件" name="resource">
+        <el-table :data="resources" border>
+          <el-table-column prop="fileName" label="文件名" min-width="220" />
+          <el-table-column prop="fileSize" label="大小" width="120" />
+          <el-table-column prop="createTime" label="上传时间" width="180" />
+        </el-table>
+      </el-tab-pane>
+    </el-tabs>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import { ElMessage } from 'element-plus'
+import { onMounted, reactive, ref } from 'vue'
+import { useRoute } from 'vue-router'
+import { taskCommentAdd, taskCommentList, taskGet, taskLogList, taskResourceList } from '@/api/otr/task/core'
+
+const route = useRoute()
+const loading = ref(false)
+const taskIdInput = ref(String(route.query.id || '1'))
+const detail = reactive<Record<string, any>>({})
+const activeTab = ref('log')
+const logs = ref<any[]>([])
+const comments = ref<any[]>([])
+const resources = ref<any[]>([])
+const commentContent = ref('')
+const commentSubmitting = ref(false)
+
+async function load() {
+  loading.value = true
+  try {
+    const id = taskIdInput.value || '1'
+    const [res, logRes, commentRes, resourceRes]: any = await Promise.all([
+      taskGet(id),
+      taskLogList({ taskId: id, page: 1, psize: 20 }),
+      taskCommentList({ taskId: id, page: 1, pageSize: 20 }),
+      taskResourceList({ taskId: id }),
+    ])
+    Object.assign(detail, res?.result || {})
+    logs.value = logRes?.result?.records || []
+    comments.value = commentRes?.result?.records || []
+    resources.value = resourceRes?.result?.records || []
+  } finally {
+    loading.value = false
+  }
+}
+
+async function submitComment() {
+  if (!commentContent.value.trim()) {
+    ElMessage.warning('请先输入评论内容')
+    return
+  }
+  commentSubmitting.value = true
+  try {
+    await taskCommentAdd({
+      taskId: taskIdInput.value || '1',
+      content: commentContent.value,
+    })
+    ElMessage.success('评论已提交')
+    commentContent.value = ''
+    const commentRes: any = await taskCommentList({ taskId: taskIdInput.value || '1', page: 1, pageSize: 20 })
+    comments.value = commentRes?.result?.records || []
+  } finally {
+    commentSubmitting.value = false
+  }
+}
+
+onMounted(load)
+</script>
+
+<style scoped>
+.header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.comment-item {
+  margin-top: 10px;
+}
+.comment-meta {
+  display: flex;
+  justify-content: space-between;
+  color: var(--el-text-color-secondary);
+  font-size: 12px;
+}
+.comment-content {
+  margin-top: 6px;
+}
+</style>

+ 85 - 0
ui/sp-user-center/src/modules/otr/task/views/Form.vue

@@ -0,0 +1,85 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <div class="header">新建任务</div>
+    </template>
+    <el-form label-width="100px" style="max-width: 760px">
+      <el-form-item label="任务标题" required>
+        <el-input v-model="form.title" placeholder="请输入任务标题" />
+      </el-form-item>
+      <el-form-item label="任务内容">
+        <el-input v-model="form.content" type="textarea" :rows="4" placeholder="请输入任务内容" />
+      </el-form-item>
+      <el-form-item label="负责人">
+        <el-input v-model="form.leaderName" placeholder="负责人(示例)" />
+      </el-form-item>
+      <el-form-item label="优先级">
+        <el-select v-model="form.priorityType" style="width: 180px">
+          <el-option label="高" :value="1" />
+          <el-option label="中" :value="2" />
+          <el-option label="低" :value="3" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="计划时间">
+        <el-date-picker
+          v-model="range"
+          type="datetimerange"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          style="width: 360px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
+        <el-button @click="goBack">返回</el-button>
+      </el-form-item>
+    </el-form>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import { ElMessage } from 'element-plus'
+import { ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { taskAdd } from '@/api/otr/task/core'
+
+const router = useRouter()
+const submitting = ref(false)
+const range = ref<string[]>([])
+const form = ref({
+  title: '',
+  content: '',
+  leaderName: '',
+  priorityType: 2,
+})
+
+async function submit() {
+  if (!form.value.title) {
+    ElMessage.warning('请先填写任务标题')
+    return
+  }
+  submitting.value = true
+  try {
+    await taskAdd({
+      ...form.value,
+      startTime: range.value?.[0] || '',
+      endTime: range.value?.[1] || '',
+    })
+    ElMessage.success('任务已保存')
+    router.push('/otr/task/list')
+  } finally {
+    submitting.value = false
+  }
+}
+
+function goBack() {
+  router.back()
+}
+</script>
+
+<style scoped>
+.header {
+  font-weight: 600;
+}
+</style>

+ 7 - 0
ui/sp-user-center/src/modules/otr/task/views/List.vue

@@ -0,0 +1,7 @@
+<template>
+  <task-workspace title="任务管理" scene="list" />
+</template>
+
+<script setup lang="ts">
+import TaskWorkspace from '@/modules/otr/task/components/TaskWorkspace.vue'
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/task/views/Member.vue

@@ -0,0 +1,7 @@
+<template>
+  <task-workspace title="成员任务" scene="member" />
+</template>
+
+<script setup lang="ts">
+import TaskWorkspace from '@/modules/otr/task/components/TaskWorkspace.vue'
+</script>

+ 7 - 0
ui/sp-user-center/src/modules/otr/task/views/Table.vue

@@ -0,0 +1,7 @@
+<template>
+  <task-workspace title="任务看板" scene="table" />
+</template>
+
+<script setup lang="ts">
+import TaskWorkspace from '@/modules/otr/task/components/TaskWorkspace.vue'
+</script>

+ 173 - 0
ui/sp-user-center/src/modules/sales/views/bulletin/add.vue

@@ -0,0 +1,173 @@
+<!-- 发布公告 -->
+<template>
+    <el-dialog :title="`${ editType == 'add' ? '发布' : '编辑' }公告`" v-model="visible" width="1150px" destroy-on-close>
+        <el-form ref="bulletinFormRef" :model="form" :rules="rules" label-width="100px">
+            <el-form-item label="选择栏目:" prop="columnValue">
+                <SelectDict placeholder="请选择栏目" v-model="form.columnValue" dictCode="bulletin_column"></SelectDict>
+            </el-form-item>
+            <el-form-item label="公告标题:" prop="title">
+                <el-input v-model="form.title" placeholder="请输入公告标题" />
+            </el-form-item>
+            <el-form-item label="接收人员:" prop="ownerBy">
+                <el-radio-group v-model="form.bulletinType" style="margin-bottom: 5px" @change="changeType">
+                    <el-radio :value="1" label="全体员工"></el-radio>
+                    <el-radio :value="2" label="指定人员"></el-radio>
+                </el-radio-group>
+                <SelectUser v-model="form.noticeUserIdList" multiple v-if="form.bulletinType == 2"></SelectUser>
+            </el-form-item>
+            <el-form-item label="公告详情:" prop="content">
+                <ckeditor class="template_ckeditor" @sendContnet="getContent" :content="form.content"></ckeditor>
+            </el-form-item>
+            <el-form-item label="上传附件:" class="form-item">
+                <Upload :isTips="false" v-model="form.fileIds" :resourceList="fileList" @uploadStatus="uploadStatus" />
+            </el-form-item>
+            <el-form-item label="其他设置:" prop="taskContent">
+                <el-checkbox v-model="form.isTalk" :true-value="1" :false-value="2">允许评论</el-checkbox>
+            </el-form-item>
+            <el-form-item label="提醒方式:">
+                <SelectDict
+                    type="checkbox"
+                    disabledItem="site"
+                    :selectedFirst="editType == 'add'"
+                    v-model="remindModels"
+                    dictCode="task_remind_mode"
+                ></SelectDict>
+            </el-form-item>
+        </el-form>
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button @click="cancel">取消</el-button>
+                <el-button :loading="buttonLoading" :disabled="fileLoading" type="primary" @click="submitForm">提交</el-button>
+            </div>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup lang="ts">
+    import { addBulletin, updateBulletin, getBulletin } from '@/api/system/bulletin';
+    import { BulletinForm, BulletinQuery } from '@/api/system/bulletin/types';
+    import { useDictCache } from '@/hooks/web/useDict'
+    import ckeditor from '@/components/ckeditor/index.vue'
+    import useUserStore from '@/store/modules/user';
+    const { queryDictDetailByCodes } = useDictCache(['task_remind_mode','bulletin_column'])
+    queryDictDetailByCodes()
+
+    const emits = defineEmits(['refresh'])
+
+    const getContent = (val: string) => {
+        form.value.content = val
+    }
+
+    const visible = ref(false)
+    const buttonLoading = ref(false)
+
+    const fileList = ref([])
+    const bulletinFormRef = ref<ElFormInstance>();
+    const remindModels = ref([])
+    const initFormData: BulletinForm = {
+        id: undefined,
+        title: '',
+        content: '',
+        columnValue: '',
+        noticeUserIdList: [],
+        noticeUserIds: '',
+        isTalk: 1,
+        remindMode: '',
+        fileIds: [],
+        files: undefined,
+        bulletinType: 1
+    }
+
+    const data = reactive<PageData<BulletinForm, BulletinQuery>>({
+        form: { ...initFormData },
+        rules: {
+            columnValue: [
+                { required: true, message: "请选择栏目", trigger: "change" }
+            ],
+            title: [
+                { required: true, message: "公告标题不能为空", trigger: "change" }
+            ],
+            content: [
+                { required: true, message: "公告详情不能为空", trigger: "blur" }
+            ]
+        }
+    });
+
+    const { form, rules } = toRefs(data);
+
+    const editType = ref('')
+    const { userInfo } = useUserStore();
+    const open = (type, id) => {
+        reset();
+        editType.value = type
+        fileList.value = []
+        if(id) {
+            getBulletin(id, userInfo.id).then(res => {
+                if(res.success) {
+                    let datas = res.result
+                    fileList.value = datas.files || []
+                    setTimeout(() => {
+                        form.value.content = datas.content
+                        Object.assign(form.value, datas)
+                        remindModels.value = res.result.remindMode?.split(',')
+                        form.value.noticeUserIdList = form.value.noticeUserIds ? form.value.noticeUserIds.split(',') : []
+                        delete form.value.files
+                    }, 100)
+                }
+            })
+        }
+        visible.value = true
+    }
+
+    const changeType = () => {
+        form.value.noticeUserIdList = []
+        form.value.noticeUserIds = ''
+    }
+
+    const fileLoading = ref(false)
+    const uploadStatus = (type) => {
+        if(type == 'loading') {
+            fileLoading.value = true
+        } else {
+            fileLoading.value = false
+        }
+    }
+
+    const submitForm = () => {
+        bulletinFormRef.value?.validate(async (valid: boolean) => {
+            if (valid) {
+                let params = JSON.parse(JSON.stringify(form.value))
+                params.remindMode = remindModels.value.join(',')
+                params.noticeUserIds = params.noticeUserIdList.join(',')
+                buttonLoading.value = true;
+                let request = editType.value == 'edit' ? updateBulletin : addBulletin
+                await request(params).finally(() => buttonLoading.value = false);
+                ElMessage.success((editType.value == 'add' ? '新增' : '编辑') + "成功");
+                reset()
+                visible.value = false
+                emits('refresh')
+            }
+        });
+    }
+
+    const cancel = () => {
+        reset();
+        visible.value = false
+    }
+
+    /** 表单重置 */
+    const reset = () => {
+        form.value = { ...initFormData };
+        bulletinFormRef.value?.resetFields();
+    }
+
+    defineExpose({
+        open
+    })
+</script>
+
+<style lang="scss" scoped>
+    :deep(.sle-user-el-input) {
+        margin-top: 5px;
+    }
+</style>

+ 278 - 0
ui/sp-user-center/src/modules/sales/views/bulletin/comment.vue

@@ -0,0 +1,278 @@
+<template>
+    <div class="commit-content mt-10">
+        <div class="commit-item"
+            v-for="(commit, oidx) in pageData"
+            :key="oidx"
+        >
+            <div class="commit-uinfo">
+                <el-avatar :src="commit.staffPhoto" style="margin-right: 5px"></el-avatar>
+                <span class="uname">{{ commit.staffName }}</span>
+                <el-icon class="ml-10 pointer" v-if="commit.canDelete" color="#666" @click="delDraft(commit)">
+                    <Delete />
+                </el-icon>
+            </div>
+            <div class="commit-info mb-5">
+                <span class="info detail-editor" v-html="commit.content"></span>
+            </div>
+            <div class="commit-info">
+                <span class="time">{{ commit.createTime }}</span>
+                <el-tag type="primary" style="cursor: pointer;margin-left:10px" @click="submitCommitW(oidx,commit.staffName)">回复</el-tag>
+            </div>
+            <div v-if="commit.commentList && commit.commentList.length > 0" class='second_reply'>
+                <div class="commit-item"
+                    v-for="(commitChirden, cidx) in commit.commentList"
+                    :key="cidx"
+                >
+                    <div class="commit-uinfo">
+                        <el-avatar :src="commitChirden.staffPhoto" style="margin-right: 5px"></el-avatar>
+                        <span class="uname">{{commitChirden.staffName}}</span>
+                        <span class="middle">回复了</span>
+                        <span class="uname">{{commitChirden.staffParentName}}</span>
+                        <el-icon class="ml-10 pointer" v-if="commitChirden.canDelete" color="#666" @click="delDraft(commitChirden)">
+                            <Delete />
+                        </el-icon>
+                    </div>
+                    <div class="commit-info">
+                        <span class="info detail-editor" v-html="commitChirden.content"></span>
+                    </div>
+                    <div class="commit-info">
+                        <span class="time">{{ commitChirden.createTime }}</span>
+                        <el-tag type="primary" style="cursor: pointer;margin-left:10px" @click="submitCommitS(oidx,cidx,commitChirden.staffName)">回复</el-tag>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <el-dialog :title="dialogTitle" v-model="visible" width="850px" destroy-on-close>
+        <ckeditor type="mini" class="mt-10" :content="commentText" @sendContnet="getContent"></ckeditor>
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button @click="cancel">取消</el-button>
+                <el-button :loading="buttonLoading" type="primary" @click="submitForm">提交</el-button>
+            </div>
+        </template>
+    </el-dialog>
+</template>
+
+<script lang="ts" setup>
+    import { addComment, deleteComment } from '@/api/public'
+    import ckeditor from '@/components/ckeditor/index.vue'
+
+    const props = defineProps({
+        pageData: {
+            type: Array<any>,
+            default: () => []
+        },
+        commitList: {
+            type: Object,
+            default: () => {}
+        }
+    })
+    const emits = defineEmits(['refresh'])
+
+    const delDraft = (commit) => {
+        ElMessageBox.confirm(
+            '是否确认删除当前评论?',
+            '确认提示',
+            { type: 'warning', confirmButtonText: '提交' }
+        ).then(() => {
+            deleteComment(commit.id).then(res => {
+                if(res.success) {
+                    ElMessage.success('删除成功')
+                    emits('refresh', props.commitList.id)
+                }else{
+                    ElMessage.error('删除失败')
+                }
+            })
+        }).catch(() => {})
+    }
+
+    const visible = ref(false)
+    const commentText = ref('')
+    const getContent = (val) => {
+        commentText.value = val
+    }
+    const paramsData = ref()
+    const dialogTitle = ref('')
+    const submitCommitW = (oidx, name) => {
+        let data = props.commitList.commentls[oidx]
+        let formData= {
+            parentId:data.id,
+            topId:data.id,
+            tagIdls:[],
+            type: '120',
+            recordId: props.commitList.id,
+            isFollow: 0
+        }
+        paramsData.value = formData
+        commentText.value = '';
+        dialogTitle.value = '回复 @' + name
+        visible.value = true
+    }
+
+    const submitCommitS = (oidx, cidx, name) => {
+        let data = props.commitList['commentls'][oidx].commentList[cidx]
+        let formData = {
+            parentId: data.id,
+            topId: props.commitList['commentls'][oidx].id,
+            tagIdls:[],
+            type: '120',
+            recordId: props.commitList.id,
+            isFollow: 0
+        }
+        paramsData.value = formData
+        commentText.value = '';
+        dialogTitle.value = '回复 @' + name
+        visible.value = true
+    }
+
+    const cancel = () => {
+        visible.value = false
+    }
+    const buttonLoading = ref(false)
+    const submitForm = () => {
+        if(!commentText.value) {
+            ElMessage.error(`请填写评论内容`)
+            return false
+        }
+        let formData = paramsData.value
+        formData.content = commentText.value
+        buttonLoading.value = true
+        addComment(formData).then(res => {
+            if(res.success) {
+                ElMessage.success('回复成功')
+                emits('refresh', props.commitList.id)
+                visible.value = false
+            }else{
+                ElMessage.error('回复失败')
+            }
+        }).finally(() => {
+            buttonLoading.value = false
+        })
+    }
+</script>
+
+<style lang="scss" scoped>
+.item-commit .commit-content .second_reply{
+	border-top: 1px solid #ededed;
+	padding: 10px 0 10px 24px;
+	margin-top: 10px;
+}
+.item-commit .commit-content .commit-item{
+	margin-bottom: 10px;
+}
+.item-commit .commit-content .commit-item .commit-uinfo{
+	height: 30px;
+	line-height: 30px;
+    display: flex;
+    align-items: center;
+}
+.item-commit .commit-content .commit-item .commit-uinfo .middle{
+	font-size: 12px;
+	margin:0 8px;
+}
+.score_num{
+	border:1px solid #1874FF;
+	color:#1874FF;
+	border-radius: 4px;
+	padding:0 3px;
+	margin-left: 20px;
+	font-size: 12px;
+}
+.item-commit .commit-content .commit-item .commit-uinfo .photo{
+	display: inline-block;
+	width: 20px;
+	height: 20px;
+	border-radius: 10px;
+	background-color: var(--grey);
+	vertical-align: middle;
+	margin-right: 5px;
+}
+.item-commit .commit-content .commit-item .commit-uinfo .uname{
+	font-size: 12px;
+	color: var(--normal);
+}
+.item-commit .commit-content .commit-item .commit-uinfo .time{
+	font-size: 12px;
+	color: var(--normal);
+	margin-left: 10px;
+}
+.item-commit .commit-content .commit-item .commit-info{
+	padding: 0px 26px;
+}
+.item-commit .commit-content .commit-item .commit-info .atuname{
+	background-color: var(--grey);
+	padding: 2px 5px;
+	border-radius: 2px;
+	font-size: 12px;
+	margin-right: 10px;
+}
+.item-commit .commit-content .commit-item .commit-info .info{
+	font-size: 13px;
+	color: var(--main);
+}
+.item-commit .commit-content .commit-item .commit-info .info img{
+	max-width: 100%;
+	cursor:pointer;
+}
+.item-commit .commit-content .commit-item .commit-info .time{
+	color:#666;
+	font-size: 12px;
+}
+.item-commit .commit-content .commit-item .commit-info .like{
+	float: right;
+    line-height: 24px;
+}
+.item-commit .commit-content .commit-item .commit-info .like img{
+	width: 24px;
+	vertical-align: bottom;
+	margin-right:4px;
+	cursor: pointer;
+}
+.item-commit .submit_btn{
+	border:1px solid #c2c2c2;
+	float:right;margin-top:10px;
+	width:75px;
+	text-align:center;
+	height: 30px;
+	line-height: 30px;
+}
+.item-commit .submit_cancel{
+	margin-right: 20px;
+}
+.wang_content{
+	padding-top: 10px;
+}
+.item-commit .draft_submit_content{
+	display:flex;
+	justify-content: right;
+}
+.score_list{
+	display: flex;
+}
+.item-commit .score_list>div{
+	padding:2px 20px;
+	margin-right: 10px;
+	border:1px solid #ededed;
+	border-radius: 25px;
+	cursor: pointer;
+	margin:5px 10px 10px 0;
+	white-space: nowrap;
+}
+.commit-form .item-commit .score_list>div{
+	padding:2px 11px;
+}
+.item-commit .score_content p{
+	color: #666;
+	font-size: 12px;
+}
+.item-commit .score_list>div:hover,.item-commit .score_list>div.on{
+	background-color: #1874FF;
+	color: #fff;
+}
+.el-avatar{
+    width: 22px;
+    height: 22px;
+}
+</style>

+ 421 - 0
ui/sp-user-center/src/modules/sales/views/bulletin/detail.vue

@@ -0,0 +1,421 @@
+<template>
+    <div class="w-100% h-100% announcement">
+		<div class="frame-card h-100% flex flex-col" v-loading="loading">
+			<div class="search">
+                <el-form :model="queryParams" ref="queryFormRef" :inline="true">
+                    <el-form-item prop="title">
+                        <el-input
+                            v-model="queryParams.title"
+                            placeholder="搜索公告标题"
+                            clearable
+                            @keyup.enter="handleQuery"
+                            prefix-icon="Search"
+                        />
+                    </el-form-item>
+                    <el-form-item prop="startDate">
+                        <el-date-picker
+                            v-model="rangeTime"
+                            type="daterange"
+                            value-format="YYYY-MM-DD"
+                            start-placeholder="发布时间"
+                            end-placeholder="发布时间"
+                            @change="changeTime"
+                        />
+                    </el-form-item>
+                    <el-form-item prop="columnValue">
+                        <SelectDict v-model="queryParams.columnValue" placeholder="选择栏目" dictCode="bulletin_column"></SelectDict>
+                    </el-form-item>
+                    <el-form-item prop="createUserId">
+                        <SelectUser multiple v-model="queryParams.createByIds" placeholder="发布人员"></SelectUser>
+                    </el-form-item>
+                    <el-form-item>
+                        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+                        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+                    </el-form-item>
+                </el-form>
+            </div>
+			<div class="frame-card-content mt-16px flex-1 overflow-auto">
+                <template v-if="pageData.length > 0">
+                    <div v-for="(item,idx) in pageData" :key="idx" class="share_user_list bg-#FFF" id="top">
+                        <div class="share_user_msg" style="justify-content: space-between;">
+                            <span class="share_top_h gap-2">
+                                <div class="flex items-center">
+                                    <el-avatar :src="item.staffPhoto" style="margin-right: 8px"></el-avatar>
+                                    <span class="name">{{ item.staffName }}</span>
+                                </div>
+                                <div>{{ item.columnName }}</div>
+                                <span class="time">{{ item.createTime }}</span>
+                            </span>
+                        </div>
+                        <div class="mt-10 bold color-#333 text-center">
+                            <span class="title">{{item.title}}</span>
+                        </div>
+                        <div class="email-content" v-html="item.content"></div>
+                        <div class="files-div flex items-start" v-if="item?.files && item?.files.length > 0">
+                            <p class="file_title">附件:</p>
+                            <div class="email_file flex-1 flex flex-wrap">
+                                <div class="file" v-for="file in item.files" @click="download(file.resourceUrl, file.resourceName)">
+                                    {{ file.resourceName }}({{ file.resourceSize }})
+                                </div>
+                            </div>
+                        </div>
+                        <div class="user-notice">
+                            <span class='notice'>接收人员:</span>
+                            <span class="photo" v-if="item.userShowMore">
+                                <span class="user_img" v-for="(user,odx) in item.readVOsLes" :key="odx">
+                                    <img :src="user.staffPhoto" :class="{'img-light':user.hasRead}"/>
+                                    <el-icon v-if="user.hasRead" color="rgb(27,190,107)"><CircleCheckFilled /></el-icon>
+                                </span>
+                                <span class="more blue" v-if="item.readVOs.length>10" @click="item.userShowMore=false">展开</span>
+                            </span>
+                            <span class="photo" v-else>
+                                <span class="user_img" v-for="(user,odx) in item.readVOs" :key="odx">
+                                    <img :src="user.staffPhoto" :class="{'img-light':user.hasRead}"/>
+                                    <el-icon v-if="user.hasRead" color="rgb(27,190,107)"><CircleCheckFilled /></el-icon>
+                                </span>
+                                <span class="more blue" v-if="item.readVOs.length>10" @click="item.userShowMore=true">收起</span>
+                            </span>
+                        </div>
+                        <!--评论区-->
+                        <div class="item-commit">
+                            <div style="text-align: right" v-if="item.isTalk == '1'">
+                                <el-button type="primary" @click="showCommit(idx)">评论</el-button>
+                            </div>
+                            <div class="commit-header" v-if="item.commentls.length > 0">
+                                <span class="color-#1874FF flex items-center">
+                                    <el-icon class="mr-1"><ChatLineRound /></el-icon>评论
+                                </span>
+                            </div>
+                            <Comment
+                                :pageData='item.commentls'
+                                :commitList='item'
+                                @refresh="refreshCommit"
+                            />
+                            <template v-if="item.showCommit">
+                                <ckeditor type="mini" class="mt-10" :content="commentText" @sendContnet="getContent"></ckeditor>
+                                <div class="text-right">
+                                    <el-button type="primary" @click="handleCommit(item)" class="mt-10">发送</el-button>
+                                </div>
+                            </template>
+                        </div>
+                    </div>
+                </template>
+                <div v-if="pageData.length == 0 && !loading">
+                    <p class="text-center">暂无数据</p>
+                </div>
+				<div
+                    v-if="showLoading"
+                    class="share-more"
+                    @click="handleLoadMore"
+                >点击加载更多>></div>
+
+			</div>
+			<a href="#top"><div class="back_top" v-if="pageData.length > 1"><i class="el-icon-upload2"></i></div></a>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts">
+    import { listBulletin, readAll } from '@/api/system/bulletin';
+    import { addComment } from '@/api/public'
+    import ckeditor from '@/components/ckeditor/index.vue'
+    import Comment from './comment.vue';
+    import { useNoticeStore } from '@/store/modules/notice'
+    import { useDictCache } from '@/hooks/web/useDict'
+
+    const { queryDictDetailByCodes } = useDictCache(['bulletin_column'])
+    queryDictDetailByCodes()
+
+    const rangeTime = ref()
+    const changeTime = (value: string[] | null) => {
+        queryParams.value.startDate = value ? value[0] : undefined
+        queryParams.value.endDate = value ? value[1] : undefined
+    }
+    const queryParams = ref({
+        pageIndex: 1,
+        pageSize: 20,
+        title: undefined,
+        startDate: undefined,
+        endDate: undefined,
+        columnValue: undefined,
+        createByIds: undefined,
+        id: undefined
+    })
+
+    const changeRead = () => {
+        readAll().then(res => {
+            if(res.success) {
+                useNoticeStore().clearBulletinCount()
+            }
+        })
+    }
+
+    const showLoading = ref(false)
+    const pageData = ref([])
+    const summaryIds = ref([])
+    const loading = ref(false)
+    const getData = () => {
+        loading.value = true
+        if(queryParams.value.pageIndex == 1) {
+            pageData.value = []
+            showLoading.value = false
+        } 
+        listBulletin(queryParams.value).then(res => {
+            if(res.success) {
+                showLoading.value = res.result.pages > queryParams.value.pageIndex;
+                res.result.records.forEach(function(item){
+                    summaryIds.value.push(item.id);
+                    item.score = 0
+                    item.commitTxt = '';
+                    item.commentls = item.commentls || [];
+                    item.showCommit=false
+                    item.readVOs = item.readVOs || []
+                    if(item.readVOs.length>10){
+                        item.userShowMore=true
+                        item.readVOsLes = item.readVOs.slice(0,10)
+                    }else{
+                        item.userShowMore=false
+                    }
+                    pageData.value.push(item);
+                })
+            }
+        }).finally(() => {
+            loading.value = false
+        })
+    }
+
+    const handleLoadMore = () => {
+        queryParams.value.pageIndex++
+        getData()
+    }
+
+    const handleQuery = () => {
+        queryParams.value.pageIndex = 1
+        queryParams.value.id = undefined
+        getData()
+    }
+
+    const queryFormRef = ref()
+    const resetQuery = () => {
+        rangeTime.value = []
+        queryParams.value.startDate = undefined
+        queryParams.value.endDate = undefined
+        queryFormRef.value?.resetFields();
+        handleQuery()
+    }
+
+    const commentText = ref('')
+    const getContent = (val) => {
+        commentText.value = val
+    }
+    const showCommit = (index) => {
+        pageData.value[index].showCommit = !pageData.value[index].showCommit
+    }
+    const handleCommit = (item) => {
+        if(!commentText.value) {
+            ElMessage.error(`请填写评论内容`)
+            return false
+        }
+        let params = {
+            type: '120',
+            content: commentText.value,
+            recordId: item.id,
+            isFollow: 0
+        }
+        addComment(params).then(res => {
+            if(res.success) {
+                ElMessage.success('评论成功')
+                commentText.value = ''
+                refreshCommit(item.id)
+            }
+        })
+    }
+
+    const refreshCommit = (data) => {
+        let params = JSON.parse(JSON.stringify(queryParams.value))
+        params.id = data
+        listBulletin(params).then(res => {
+            if(res.success) {
+                let datas = res.result.records[0]
+                pageData.value.forEach(function(item){
+                    if(item.id == data) {
+                        item.commentls = datas.commentls
+                    }
+                })
+            }
+        })
+    }
+
+    const download = (url: string, fileName: string) => {
+        const a = document.createElement('a');
+        a.style.display = 'none';
+        a.href = url;
+        a.download = fileName;
+        a.target = '_blank'
+        document.body.appendChild(a);
+        a.click();
+        document.body.removeChild(a);
+    }
+
+    const route = useRoute()
+    onMounted(() => {
+        if(route.query.id) {
+            queryParams.value.id = route.query.id
+        }
+        getData()
+        changeRead()
+    })
+</script>
+
+<style lang="scss" scoped>
+    .search{
+        padding: 16px 20px 0px;
+        .el-select{
+            width: 200px;
+        }
+        :deep(.sle-user-el-input){
+            min-width: 200px;
+        }
+    }
+    .share_user_msg{
+        display: flex;
+        align-items: center;
+        padding: 10px 12px;
+        border-bottom: 1px solid rgb(237, 237, 237);
+    }
+    .share_top_h{
+        display: flex;
+        align-items: center;
+        margin-right: 30px;
+        .el-avatar{
+            width: 22px;
+            height: 22px;
+        }
+    }
+    .title{
+        font-size: 32px;
+    }
+    .customer-date-picker .el-input--prefix .el-input__inner{
+        padding:0px;
+    }
+    .effect-header .target-select-element{
+        padding:0 19px
+    }
+    .effect-header .all-status{
+        padding: 6px 19px;
+    }
+    .customer-date-picker-blue .el-input--prefix .el-input__inner {
+        color: #666;
+        height:30px;
+    }
+    .files-div{
+        padding: 10px 20px 10px 48px;
+        .file{
+            padding: 5px;
+            color: #1874FF;
+            margin: 0 15px 10px 0;
+            cursor: pointer;
+        }
+    }
+    .user-notice{
+        display: flex;
+        padding: 10px 20px 10px 48px;
+        align-items: baseline;
+    }
+    .user-notice .notice{
+        margin-right:10px;
+    }
+    .user-notice .photo{
+        flex:1
+    }
+    .user-notice .user_img{
+        display: inline-block;
+        position: relative;
+        margin-bottom: 10px;
+    }
+    .user-notice .user_img img{
+        width: 32px;
+        height:32px;
+        border-radius: 100%;
+        margin-right: 8px;
+        vertical-align: middle;
+    }
+    .user-notice .user_img i{
+        position: absolute;
+        top: -2px;
+        right: 2px;
+    }
+    .user-notice .more{
+        margin-left: 10px;
+    }
+    .img-light{
+        border: 2px solid rgb(27,190,107);
+    }
+    .share_user_list{
+        margin-bottom: 16px;
+        .item-commit{
+            padding: 10px 20px;
+            border-top: 1px solid rgb(237, 237, 237);
+        }
+    }
+
+
+    .announcement .share_user_list .email-content{
+        padding: 50px 260px 13px 260px;
+        color: #666;
+        font-size: 15px;
+    }
+    .announcement .share_user_list p{
+        line-height: 30px;
+    }
+    .blue{
+        color: #1874FF;
+        cursor: pointer;
+        margin-left: 10px;
+    }
+    .email-content{
+        :deep(p){
+            line-height: 32px;
+        }
+        :deep(ul){
+            padding-inline-start: 20px;
+            margin-block-end: 0;
+            margin-block-start: 0;
+            li{
+                list-style: disc !important;
+            }
+
+        }
+        :deep(ol){
+            padding-inline-start: 20px;
+            margin-block-end: 0;
+            margin-block-start: 0;
+            li{
+                list-style: decimal !important;
+            }
+
+        }
+        :deep(ol, li){
+            padding-inline-start: 20px;
+            margin-block-end: 0;
+            margin-block-start: 0;
+        }
+        :deep(ul, li){
+            padding-inline-start: 20px;
+            margin-block-end: 0;
+            margin-block-start: 0;
+        }
+        :deep(a){
+            text-decoration: underline;
+            color: #1874FF;
+        }
+        :deep(img){
+            max-width: 100%;
+        }
+    }
+    .share-more{
+        text-align: center;
+        cursor: pointer;
+        color: #1874FF;
+    }
+</style>

+ 355 - 0
ui/sp-user-center/src/modules/sales/views/bulletin/detail2.vue

@@ -0,0 +1,355 @@
+<template>
+    <div class="bulletin_detail">
+        <div class="left">
+            <div class="left-content">
+                <p class="title">{{ bulletinData?.title }}</p>
+                <div class="flex gap-8 color-#595959 mt-10 mb-10">
+                    <p>发布人员:{{ bulletinData?.publisher }}</p>
+                    <p>发布时间:{{ bulletinData?.createTime }}</p>
+                </div>
+                <div class="email-content" v-html="bulletinData?.content"></div>
+            </div>
+            <template v-if="bulletinData?.files && bulletinData?.files.length > 0">
+                <p style="margin: 15px 0;" class="file_title">附件</p>
+                <div class="email_file flex">
+                    <div class="file" v-for="item in bulletinData.files" @click="download(item.resourceUrl, item.resourceName)">
+                        {{ item.resourceName }}({{ item.resourceSize }})
+                    </div>
+                </div>
+            </template>
+            <div class="user-notice">
+                <span class='notice'>接收人员:</span>
+                <span class="photo" v-if="userShowMore">
+                    <span class="user_img" v-for="(user,odx) in bulletinData?.readVOsLes" :key="odx">
+                        <img :src="user.staffPhoto" :class="{'img-light':user.hasRead}"/>
+                        <i v-if="user.hasRead" class="el-icon-success" style="color:rgb(27,190,107)"></i>
+                    </span>
+                    <span class="more" v-if="bulletinData?.readVOs.length > 10" @click="userShowMore=false">展开</span>
+                </span>
+                <span class="photo" v-else>
+                    <span class="user_img" v-for="(user,odx) in bulletinData?.readVOs" :key="odx">
+                        <img :src="user.staffPhoto" :class="{'img-light':user.hasRead}"/>
+                        <i v-if="user.hasRead" class="el-icon-success" style="color:rgb(27,190,107)"></i>
+                    </span>
+                    <span class="more" v-if="bulletinData?.readVOs.length>10" @click="userShowMore=true">收起</span>
+                </span>
+            </div>
+        </div>
+        <div class="right">
+            <el-tabs class="flex flex-col h-100%" v-model="activeName">
+                <el-tab-pane label="评论" name="comment" v-if="bulletinData?.isTalk == '1'">
+                    <template v-if="commentList.length > 0">
+                        <div class="flex-1 overflow-y-auto">
+                            <div class="follow_item flex" v-for="item in commentList">
+                                <img :src="item.staffPhoto">
+                                <div class="follow_content">
+                                    <div class="flex-center">
+                                        <span class="text_black bold">{{ item.staffName }}</span>
+                                        <span>{{ item.createTime }}</span>
+                                        <el-icon class="del_icon" @click="delComment(item.id)">
+                                            <Delete />
+                                        </el-icon>
+                                    </div>
+                                    <div class="follow_desc">{{ item.content }}</div>
+                                </div>
+                            </div>
+                        </div>
+                    </template>
+                    <div v-else>暂无评论</div>
+                    <div class="comment-input">
+                        <el-input v-model="commentText" style="width: 100%" placeholder="请输入评论内容">
+                            <template #append>
+                                <el-button type="primary" @click="comment()">评论</el-button>
+                            </template>
+                        </el-input>
+                    </div>
+                </el-tab-pane>
+                <el-tab-pane label="浏览记录" name="record" v-hasPermi="['system:bulletin:lljl']">
+                    <div>
+                        <el-radio-group v-model="activeRecord" style="margin-bottom: 10px">
+                            <el-radio-button value="yes" label="浏览名单"></el-radio-button>
+                            <el-radio-button value="no" label="未浏览名单"></el-radio-button>
+                        </el-radio-group>
+                        <template v-if="activeRecord == 'yes'">
+                            <el-table :height="500" :data="readList" style="width: 100%">
+                                <el-table-column prop="deptName" label="部门" />
+                                <el-table-column prop="staffName" label="人员" />
+                                <el-table-column prop="times" label="浏览时间" />
+                            </el-table>
+                        </template>
+                        <template v-else>
+                            <el-table :height="500" :data="noReadList" style="width: 100%">
+                                <el-table-column prop="deptName" label="部门" />
+                                <el-table-column prop="staffName" label="人员" />
+                            </el-table>
+                        </template>
+                    </div>
+                </el-tab-pane>
+            </el-tabs>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+    import { getBulletin } from '@/api/system/bulletin';
+    import useUserStore from '@/store/modules/user';
+    import { listComment, addComment, deleteComment } from '@/api/public'
+    import moment from 'moment';
+
+    const visible = ref(false)
+    const bulletinData = ref()
+    const { userInfo } = useUserStore();
+    const bulletinId = ref()
+    const route = useRoute()
+    const userShowMore = ref(false)
+    const getData = () => {
+        bulletinId.value = route.query.id
+        getBulletin(bulletinId.value, userInfo.id).then(res => {
+            if(res.success) {
+                bulletinData.value = res.result
+                if(bulletinData.value.readVOs.length>10){
+                    userShowMore.value = true
+                    bulletinData.value.readVOsLes = bulletinData.value.readVOs.slice(0,10)
+                }else{
+                    userShowMore.value = false
+                }
+                readList.value = bulletinData.value?.readVOs.filter(item => item.hasRead).map(item => {
+                    item.times = moment(item.times).format('MM-DD HH:mm')
+                    return item
+                })
+                noReadList.value = bulletinData.value?.readVOs.filter(item => !item.hasRead)
+            }
+        })
+        getComment()
+        visible.value = true
+    }
+
+    const activeName = ref('comment')
+
+    // 获取评论
+    const commentList = ref([])
+    const getComment = ()=>{
+        let params = {
+            pageIndex: 1,
+            pageSize: 999999,
+            type: '120',
+            recordIds: bulletinId.value
+        }
+        listComment(params).then(res => {
+            if(res.success) {
+                commentList.value = res.result.records
+            }
+        })
+    }
+
+    const download = (url: string, fileName: string) => {
+        const a = document.createElement('a');
+        a.style.display = 'none';
+        a.href = url;
+        a.download = fileName;
+        a.target = '_blank'
+        document.body.appendChild(a);
+        a.click();
+        document.body.removeChild(a);
+    }
+
+    const commentText = ref('')
+    // 评论
+    const comment = () => {
+        if(!commentText.value) {
+            ElMessage.error(`请填写评论内容`)
+            return false
+        }
+        let params = {
+            type: '120',
+            content: commentText.value,
+            recordId: bulletinId.value,
+            isFollow: 0
+        }
+        addComment(params).then(res => {
+            if(res.success) {
+                ElMessage.success('评论成功')
+                commentText.value = ''
+                getComment()
+            }
+        })
+    }
+
+    const delComment = (id) => {
+        ElMessageBox.confirm(
+            '是否确认删除当前评论?',
+            '确认提示',
+            { type: 'warning', confirmButtonText: '提交' }
+        ).then(() => {
+            deleteComment(id).then(res => {
+                if(res.success) {
+                    ElMessage.success('删除成功')
+                    getComment()
+                }else{
+                    ElMessage.error('删除失败')
+                }
+            })
+        }).catch(() => {})
+    }
+
+    const activeRecord = ref('yes')
+    const readList = ref([])
+    const noReadList = ref([])
+
+    onMounted(() => {
+        getData()
+    })
+</script>
+
+<style lang="scss" scoped>
+    .bulletin_detail{
+        width: 100%;
+        background: #FFF;
+        display: flex;
+        .left{
+            flex: 1;
+            padding: 20px 60px;
+            overflow: auto;
+            .left-content{
+                display: flex;
+                flex-direction: column;
+                align-items: center;
+            }
+            .title{
+                color: #333333;
+                font-size: 18px;
+                font-weight: bold;
+            }
+            .file_title{
+                border-left: 3px solid var(--el-color-primary);
+                font-size: 16px;
+                padding-left: 10px;
+                line-height: 1;
+            }
+        }
+        .right{
+            width: 460px;
+            border-left: 1px solid #DCDEE2;
+            padding: 10px 25px;
+            :deep(.el-tabs__nav-wrap:after){
+                display: none;
+            }
+            .comment-input{
+                margin-top: 15px;
+            }
+            :deep(.el-table__inner-wrapper:before){
+                display: none;
+            }
+            :deep(.el-tabs__content){
+                height: 100%;
+            }
+            .el-tab-pane{
+                height: 100%;
+                display: flex;
+                flex-direction: column;
+                justify-content: space-between;
+            }
+        }
+    }
+    .user-notice{
+        display: flex;
+        margin-top: 20px;
+        align-items: baseline;
+    }
+    .user-notice .notice{
+        margin-right:10px;
+    }
+    .user-notice .photo{
+        flex:1
+    }
+    .user-notice .user_img{
+        display: inline-block;
+        position: relative;
+        margin-bottom: 10px;
+    }
+    .user-notice .user_img img{
+        width: 32px;
+        height:32px;
+        border-radius: 100%;
+        margin-right: 8px;
+        vertical-align: middle;
+    }
+    .user-notice .user_img i{
+        position: absolute;
+        top: -2px;
+        right: 2px;
+    }
+    .user-notice .more{
+        margin-left: 10px;
+        cursor: pointer;
+        color: #1874FF;
+    }
+    .email_file{
+        margin-top: 15px;
+        flex-wrap: wrap;
+        .file{
+            border-radius: 2px;
+            border: 1px solid #1874FF;
+            padding: 4px 17px;
+            color: #1874FF;
+            margin: 0 15px 10px 0;
+            cursor: pointer;
+        }
+    }
+    .email-content{
+        :deep(p){
+            line-height: 32px;
+        }
+        :deep(ul){
+            padding-inline-start: 20px;
+            margin-block-end: 0;
+            margin-block-start: 0;
+            li{
+                list-style: disc !important;
+            }
+
+        }
+        :deep(ol){
+            padding-inline-start: 20px;
+            margin-block-end: 0;
+            margin-block-start: 0;
+            li{
+                list-style: decimal !important;
+            }
+
+        }
+        :deep(ol, li){
+            padding-inline-start: 20px;
+            margin-block-end: 0;
+            margin-block-start: 0;
+        }
+        :deep(ul, li){
+            padding-inline-start: 20px;
+            margin-block-end: 0;
+            margin-block-start: 0;
+        }
+        :deep(a){
+            text-decoration: underline;
+            color: #1874FF;
+        }
+        :deep(img){
+            max-width: 100%;
+        }
+    }
+    .follow_item{
+        margin: 10px 0 0;
+    }
+    .follow_content{
+        .follow_desc{
+            margin:8px 0 0;
+        }
+    }
+    .el-input-group__append button.el-button{
+        background-color: #1874FF;
+        color: #fff;
+    }
+    .del_icon{
+        cursor: pointer;
+        margin-left: 10px;
+    }
+</style>

+ 132 - 0
ui/sp-user-center/src/modules/sales/views/bulletin/index.vue

@@ -0,0 +1,132 @@
+<template>
+	<el-card shadow="never" class="list-card">
+		<template #header>
+			<div class="search">
+                <el-form :model="queryParams" ref="queryFormRef" :inline="true">
+                    <el-form-item prop="title">
+                        <el-input
+                            v-model="queryParams.title"
+                            placeholder="搜索公告标题"
+                            clearable
+                            @keyup.enter="handleQuery"
+                            prefix-icon="Search"
+                        />
+                    </el-form-item>
+                    <el-form-item prop="startDate">
+                        <el-date-picker
+                            v-model="rangeTime"
+                            type="daterange"
+                            value-format="YYYY-MM-DD"
+                            start-placeholder="发布时间"
+                            end-placeholder="发布时间"
+                            @change="changeTime"
+                        />
+                    </el-form-item>
+                    <el-form-item prop="columnValue">
+                        <SelectDict v-model="queryParams.columnValue" placeholder="选择栏目" dictCode="bulletin_column"></SelectDict>
+                    </el-form-item>
+                    <el-form-item prop="createUserId">
+                        <SelectUser multiple v-model="queryParams.createByIds" placeholder="发布人员"></SelectUser>
+                    </el-form-item>
+                    <el-form-item>
+                        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+                        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+                    </el-form-item>
+                </el-form>
+            </div>
+		</template>
+
+		<Table
+			ref="taskTableRef"
+			:tableParams="tableParams"
+			:btnRole="btnRole"
+			@handleAdd="handleAdd('add')"
+			@handleEdit="handleAdd('edit', $event)"
+			:right-btn="[]"
+			:is-checkbox="false"
+		>
+            <template #left>
+                <el-button type="primary" v-hasPermi="['system:bulletin:add']" @click="handleAdd('add')">
+                    发布
+                </el-button>
+            </template>
+		</Table>
+	</el-card>
+
+	<AddBulletin ref="addBulletinRef" @refresh="handleQuery" />
+</template>
+
+<script setup name="Task" lang="ts">
+    import { listBulletin, delBulletin } from '@/api/system/bulletin';
+    import { BulletinVO, BulletinQuery } from '@/api/system/bulletin/types';
+    import Table from '@/components/Table/index.vue'
+    import { tableTypes } from '@/components/Table/types'
+    import { useDictCache } from '@/hooks/web/useDict'
+    import AddBulletin from './add.vue'
+
+    const { queryDictDetailByCodes } = useDictCache(['bulletin_column'])
+    queryDictDetailByCodes()
+
+    const router = useRouter()
+    const btnRole = ref([
+        { type: 'edit', hasPermi: `system:bulletin:edit` },
+        { type: 'remove', hasPermi: `system:bulletin:remove` },
+    ])
+
+    const rangeTime = ref()
+    const changeTime = (value: string[] | null) => {
+        queryParams.value.startDate = value ? value[0] : undefined
+        queryParams.value.endDate = value ? value[1] : undefined
+    }
+
+    const tableParams = reactive<tableTypes<BulletinQuery, BulletinVO>>({
+        menuId: router.currentRoute.value?.meta?.menuId,
+        tableColumn: [
+            { show: true, label: '公告标题', prop: 'title' },
+            { show: true, label: '发布人员', prop: 'staffName' },
+		    { show: true, label: '发布时间', prop: 'createTime' },
+            { show: true, label: '所属栏目', prop: 'columnName' },
+            { show: true, label: '接收人员', prop: 'noticeUserNames', render: ({ column }: { column: BulletinVO }) => {
+                return column.bulletinType == 1 ? '全体员工' : (column.noticeUserNames || '全体员工')
+            } },
+            {
+                show: true, label: '操作', width: 150, prop: 'operation', disabledMove: true, render: () => {
+                    return []
+                }
+            },
+        ],
+        queryParams: {
+            title: undefined,
+            startDate: undefined,
+            columnValue: undefined,
+            createByIds: undefined,
+        },
+        listFn: listBulletin,
+        delFn: delBulletin,
+        keyId: 'id',
+        exportCode: 'EXPORT_TASK_EXCEL',
+    })
+
+    const { queryParams } = toRefs(tableParams);
+
+    const taskTableRef = ref()
+    /** 搜索按钮操作 */
+    const handleQuery = () => {
+        taskTableRef.value.getData()
+    }
+
+    const queryFormRef = ref()
+    const resetQuery = () => {
+        rangeTime.value = []
+        queryParams.value.startDate = undefined
+        queryParams.value.endDate = undefined
+        queryFormRef.value?.resetFields();
+        handleQuery()
+    }
+
+    /** 新增按钮操作 */
+    const addBulletinRef = ref()
+    const handleAdd = (type: string, data?: BulletinVO) => {
+        addBulletinRef.value.open(type, data?.id)
+    }
+</script>

+ 10 - 0
ui/sp-user-center/src/modules/sales/views/business/allBusiness/index.vue

@@ -0,0 +1,10 @@
+<template>
+    <CommonBusiness
+    perPrefix="allBusiness"
+    :queryRangeType="0"
+    ></CommonBusiness>
+</template>
+
+<script setup name="allBusiness" lang="ts">
+import CommonBusiness from '@/modules/sales/views/business/component/CommonBusiness.vue'
+</script>

+ 285 - 0
ui/sp-user-center/src/modules/sales/views/business/allBusinessDetailMenu.sql

@@ -0,0 +1,285 @@
+
+##详情按钮权限/////////////////////////////
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('添加产品报价', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tjcpbj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+##上面已执行////////////////////////////
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('添加联系人', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tjlxr', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('添加费用', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tjfy', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('写新跟进', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:xxgj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('新建任务', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:xjrw', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('转为订单', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:zwdd', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('转移商机', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:zysj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('添加协作', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tjxz', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('编辑商机', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:bjsj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('删除商机', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:scsj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('流失/唤醒商机', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:sjlshhx', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+
+##tab//////////////////////
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('概况信息', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gkxx', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('联系人', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_lxr', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+##tab联系人按钮//////////////////////
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('联系人-添加', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_lxr_tj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('联系人-导入', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_lxr_dr', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('联系人-导出', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_lxr_dc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('联系人-发邮件', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_lxr_fyj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('联系人-删除', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_lxr_sc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('联系人-详情', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_lxr_xq', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('跟进记录', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gjjl', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+##tab跟进记录按钮//////////////////////
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('跟进记录-导出', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gjjl_dc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('跟进记录-评论', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gjjl_tj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('跟进记录-删除', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gjjl_sc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('跟进记录-详情', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gjjl_xq', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+
+
+
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('任务记录', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_rwjl', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+##tab跟进记录按钮//////////////////////
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('任务记录-添加', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_rwjl_tj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('任务记录-详情', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_rwjl_tj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('任务记录-导出', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_rwjl_dc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('任务记录-编辑', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_rwjl_bj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('任务记录-删除', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_rwjl_sc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('关联订单', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gldd', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+##tab关联订单按钮//////////////////////
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('关联订单-添加', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gldd_tj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('关联订单-导出', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gldd_dc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('关联订单-删除', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gldd_sc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('关联订单-详情', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gldd_xq', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('产品报价', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_cpbj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('产品报价-编辑', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_cpbj_bj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('产品报价-下载中文报价单', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_cpbj_xzzwbjd', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('产品报价-下载英文报价单', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_cpbj_xzywbjd', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+##tab关联订单按钮//////////////////////
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('寄样记录', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_jyjl', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+##tab关联订单按钮//////////////////////
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('寄样记录-添加', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_jyjl_tj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('寄样记录-导入', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_jyjl_dr', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('寄样记录-导出', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_jyjl_dc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('寄样记录-详情', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_jyjl_xq', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('寄样记录-编辑', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_jyjl_bj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('寄样记录-删除', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_jyjl_sc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('费用记录', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_fyjl', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+##tab费用记录按钮//////////////////////
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('费用记录-添加', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_fyjl', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('费用记录-导出', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_fyjl_dc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('费用记录-删除', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_fyjl_sc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('费用记录-详情', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_fyjl_xq', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('相关附件', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_xgfj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+##tab相关附件按钮//////////////////////
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('相关附件-添加', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_xgfj_tj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('相关附件-下载', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_xgfj_xz', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('相关附件-删除', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_xgfj_sc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('操作日志', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_czrz', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+##tab相关附件按钮//////////////////////
+

+ 313 - 0
ui/sp-user-center/src/modules/sales/views/business/allBusinessMenu.sql

@@ -0,0 +1,313 @@
+
+
+-- 菜单 new SQL
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES (342091493154818, '全部商机', DEFAULT, '69', null, 'allBusiness', 'business/allBusiness/index', 'allBusiness', null, 1, 'business:allBusiness:list', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+
+-- 按钮 new SQL
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES (342091493154819, '商机查询', DEFAULT, 342091493154818, null, '', '', '', null, 3, 'business:allBusiness:query', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES (342091493154820, '商机新增', DEFAULT, 342091493154818, null, '', '', '', null, 3, 'business:allBusiness:add', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES (342091493154821, '商机修改', DEFAULT, 342091493154818, null, '', '', '', null, 3, 'business:allBusiness:edit', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES (342091493154822, '商机删除', DEFAULT, 342091493154818, null, '', '', '', null, 3, 'business:allBusiness:remove', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES (342091493154823, '商机导出', DEFAULT, 342091493154818, null, '', '', '', null, 3, 'business:allBusiness:export', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES (342091493154824, '商机置顶', DEFAULT, 342091493154818, null, '', '', '', null, 3, 'business:allBusiness:top', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES (342091493154825, '商机跟进', DEFAULT, 342091493154818, null, '', '', '', null, 3, 'business:allBusiness:flowUp', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 342091493154824, 1, sysdate(), null, 0);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 342091493154825, 1, sysdate(), null, 0);
+
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 342091493154818, 1, sysdate(), null, 0);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 342091493154819, 1, sysdate(), null, 0);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 342091493154820, 1, sysdate(), null, 0);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 342091493154821, 1, sysdate(), null, 0);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 342091493154822, 1, sysdate(), null, 0);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 342091493154823, 1, sysdate(), null, 0);
+
+
+##详情按钮权限/////////////////////////////
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('添加产品报价', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:detail_add', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+##上面已执行////////////////////////////
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('添加联系人', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tjlxr', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('添加费用', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tjfy', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('写新跟进', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:xxgj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('新建任务', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:xjrw', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('转为订单', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:zwdd', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('转移商机', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:zysj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('添加协作', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tjxz', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('编辑商机', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:bjsj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('删除商机', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:scsj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+
+##tab//////////////////////
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('概况信息', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gkxx', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('联系人', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_lxr', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+##tab联系人按钮//////////////////////
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('联系人-添加', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_lxr_tj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('联系人-导入', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_lxr_dr', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('联系人-导出', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_lxr_dc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('联系人-发邮件', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_lxr_fyj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('联系人-删除', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_lxr_sc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('联系人-详情', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_lxr_xq', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('跟进记录', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gjjl', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+##tab跟进记录按钮//////////////////////
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('跟进记录-导出', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gjjl_dc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('跟进记录-评论', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gjjl_tj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('跟进记录-删除', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gjjl_sc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('跟进记录-详情', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gjjl_xq', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+
+
+
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('任务记录', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_rwjl', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+##tab跟进记录按钮//////////////////////
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('任务记录-添加', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_rwjl_tj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('任务记录-详情', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_rwjl_tj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('任务记录-导出', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_rwjl_dc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('任务记录-编辑', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_rwjl_bj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('任务记录-删除', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_rwjl_sc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('关联订单', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gldd', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+##tab关联订单按钮//////////////////////
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('关联订单-添加', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gldd_tj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('关联订单-导出', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gldd_dc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('关联订单-删除', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gldd_sc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('关联订单-详情', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gldd_xq', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('产品报价', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_cpbj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+##tab关联订单按钮//////////////////////
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('寄样记录', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_jyjl', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+##tab关联订单按钮//////////////////////
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('寄样记录-添加', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_jyjl_tj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('寄样记录-导入', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_jyjl_dr', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('寄样记录-导出', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_jyjl_dc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('寄样记录-详情', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_jyjl_xq', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('寄样记录-编辑', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_jyjl_bj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('寄样记录-删除', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_jyjl_sc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('费用记录', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_fyjl', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+##tab费用记录按钮//////////////////////
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('费用记录-添加', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_fyjl', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('费用记录-导出', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_fyjl_dc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('费用记录-删除', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_fyjl_sc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('费用记录-详情', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_fyjl_xq', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('相关附件', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_xgfj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+##tab相关附件按钮//////////////////////
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('相关附件-添加', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_xgfj_tj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('相关附件-下载', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_xgfj_xz', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+    INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+    VALUES ('相关附件-删除', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_xgfj_sc', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+    insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+    values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+
+
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('操作日志', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_czrz', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);
+##tab相关附件按钮//////////////////////
+
+-- add
+INSERT INTO menu ( menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ('跟进记录-编辑', DEFAULT, 3059155905527860, null, '', '', '', null, 3, 'business:detail:tab_gjjl_bj', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, LAST_INSERT_ID(), 1, sysdate(), null, 0);

+ 109 - 0
ui/sp-user-center/src/modules/sales/views/business/component/Addcontact.vue

@@ -0,0 +1,109 @@
+<!-- 添加协作 -->
+<template>
+    <el-dialog title="添加联系人" v-model="visible" width="600px">
+        <el-form ref="formRef" :model="form" label-width="120px">
+            <el-form-item label="选择联系人:" prop="liaisonId"
+                :rule="{required: true, message: '请选择联系人', trigger: 'change'}">
+                <el-select-v2 v-model="form.liaisonId" :options="liaisonOption" placeholder="请选择客户联系人" filterable />
+            </el-form-item>
+
+
+        </el-form>
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button @click="addConcatRouter">新增客户联系人</el-button>
+                <el-button @click="cancel">取消</el-button>
+                <el-button :loading="buttonLoading" type="primary" @click="submitForm">提交</el-button>
+            </div>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup lang="ts">
+import {listLiaison, addLiaison, liaisonResourceAdd} from "@/api/customer/liaison";
+    import { updateConcat} from '@/api/business/allBusiness'
+    import {LiaisonForm} from "@/api/customer/liaison/types";
+
+    const emit = defineEmits(['onOk'])
+    const visible = ref(false)
+    const formRef = ref()
+    const buttonLoading = ref(false)
+    const router = useRouter()
+    const form = ref<LiaisonForm>({
+        id: '',
+        customerId: '',
+        hasPrimaryLialison: 0,
+        liaisonId: '',
+        resourceType: '30',
+        resourceTypeId: '',
+    })
+    const resourceName = ref('')
+    const open = (customerId: string,resourceTypeId: string,title?: string ) => {
+        form.value.resourceTypeId = resourceTypeId
+        form.value.customerId = customerId
+        resourceName.value = title
+        getLiaison()
+        visible.value = true
+    }
+
+
+
+    const rules = reactive({
+        liaisonId:[{required: true, message: '请选择联系人', trigger: 'change'}],
+        type:[{required: true, message: '请选择联系人角色', trigger: 'change'}],
+    })
+
+    const liaisonOption = ref([])
+    const getLiaison = () => {
+        listLiaison({ pageIndex: 1, pageSize: 999999, customerId: form.value.customerId }).then(res => {
+            if(res.success) {
+                liaisonOption.value = res.result.records.map((item: { name: string, id: number }) => {
+                    let obj = { label: item.name, value: item.id }
+                    return obj
+                })
+            }
+        })
+    }
+
+    const addConcatRouter = () => {
+        router.push(`/customer/liaison/add?type=add&resourceType=30&resourceTypeId=${ form.value.resourceTypeId }&resourceName=${ resourceName.value }&customerId=${ form.value.customerId }`)
+    }
+
+    const submitForm = async () => {
+        formRef.value?.validate(async (valid: boolean) => {
+            if (valid) {
+               let res = await liaisonResourceAdd(form.value)
+                emit('onOk', 'addConcat')
+                reset()
+                visible.value = false
+
+            }
+        })
+
+    }
+
+    const cancel = () => {
+        visible.value = false
+    }
+    /** 表单重置 */
+    const reset = () => {
+
+        Object.assign(form.value, {
+            id: '',
+            customerId: '',
+            hasPrimaryLialison: '',
+        })
+        formRef.value?.resetFields();
+    }
+
+    defineExpose({
+        open
+    })
+</script>
+
+<style lang="scss" scoped>
+.tips{
+    padding-left: 120px;
+    margin-bottom: 2px;
+}
+</style>

+ 498 - 0
ui/sp-user-center/src/modules/sales/views/business/component/CommonBusiness.vue

@@ -0,0 +1,498 @@
+<template>
+    <el-card shadow="never" class="list-card">
+
+        <template #header>
+            <div class="search table_area" v-show="showSearch">
+                <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
+                    <el-row :gutter="16">
+                        <el-col :span="4">
+                            <el-form-item prop="customerName">
+                                <el-input v-model="queryParams.customerName" placeholder="搜索客户" clearable
+                                    @keyup.enter="handleQuery" prefix-icon="Search" />
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="4">
+                            <el-form-item prop="title">
+                                <el-input v-model="queryParams.title" placeholder="搜索标题" clearable
+                                    @keyup.enter="handleQuery" prefix-icon="Search" />
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="4" v-if="props.stage != '4'">
+                            <el-form-item prop="salesPhaseDictValue">
+                                <SelectDict v-model="queryParams.stage" placeholder="销售阶段" dictCode="business_stage">
+                                </SelectDict>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="4">
+                            <el-form-item prop="continent">
+                                <el-cascader style="width: 100%" placeholder="区域名称" filterable :options="region"
+                                    v-model="regionValue" clearable @change="getRegion" />
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="4" v-if='showAll || props.stage == "4"'>
+                            <el-form-item prop="sourcesOfBusinessDictValue">
+                                <SelectDict v-model="queryParams.sourcesOfBusinessDictValue" placeholder="商机来源"
+                                    dictCode="sources_of_business"></SelectDict>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="4" v-if='showAll'>
+                            <el-form-item prop="transactionProbabilityDictValue">
+                                <SelectDict v-model="queryParams.transactionProbabilityDictValue" placeholder="成交机率"
+                                    dictCode="transaction_probability"></SelectDict>
+                            </el-form-item>
+                        </el-col>
+
+                        <el-col :span="6" v-if='!showAll'>
+                            <el-form-item>
+                                <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+                                <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+                                <el-button type="primary" icon="ArrowDown" text @click="showAll=true">展开检索</el-button>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+
+                    <el-row :gutter="16" v-if='showAll'>
+                        <el-col :span="4">
+                            <el-form-item prop="expectedSigningDate">
+                                <el-date-picker clearable v-model="queryParams.expectedSigningDate" type="date"
+                                    value-format="YYYY-MM-DD" placeholder="预计签单日期" />
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="4">
+                            <el-form-item prop="businessTypeDictValue">
+                                <SelectDict v-model="queryParams.businessTypeDictValue" placeholder="商机类型"
+                                    dictCode="business_type"></SelectDict>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="4">
+                            <el-form-item prop="ownerBy">
+                                <SelectUser v-model.lazy="queryParams.ownerByIds" multiple placeholder="归属人员">
+                                </SelectUser>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="6">
+                            <el-form-item>
+                                <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+                                <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+                                <el-button type="primary" icon="ArrowUp" text @click="showAll=false">收起检索</el-button>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-form>
+            </div>
+        </template>
+        <!--        <span>fdsafdsafffffffffffff</span>-->
+        <!--        <svg class="icon" aria-hidden="true">-->
+        <!--            <use xlink:href="#icon-youjianfujian"></use>-->
+        <!--        </svg>-->
+        <!--        <svg-icon icon-class="liushishangji" />-->
+        <!--        <i class="iconfont icon-youdangdaichuli"></i>-->
+        <!--        <el-icon>-->
+        <!--            <i class="iconfont icon-youdangdaichuli"></i>-->
+        <!--        </el-icon>-->
+        <Table ref="allBusinessTable" :tableParams="tableParams" :btnRole="leftBtn" @handleAdd="handleAdd"
+            @handleEdit="handleUpdate" @selectionChange="handleSelectionChange">
+            <template #left>
+                <el-button v-hasPermi="[`business:${props.perPrefix}:zysjlist`]" @click="turnBus">转移商机</el-button>
+                <el-button v-hasPermi="[`business:${props.perPrefix}:tjxzlist`]"
+                    @click="addCollaborateUsers">添加协作</el-button>
+                <el-button v-hasPermi="[`business:${props.perPrefix}:xjrwlist`]" @click="addTask">新建任务</el-button>
+            </template>
+
+        </Table>
+    </el-card>
+
+
+    <createTask :isCorrelation="false" @refresh="handleQuery" ref="createTaskRef"></createTask>
+    <AddFollow ref="addFollow"></AddFollow>
+
+    <addCollaborate @onOk="handleQuery" ref="addCollaborateRef"></addCollaborate>
+    <transferClue @refresh="handleQuery" ref="turnBusRef"></transferClue>
+</template>
+
+<script setup name="AllBusiness" lang="ts">
+import {
+    listAllBusiness,
+    getById,
+    delAllBusiness,
+    addAllBusiness,
+    updateAllBusiness
+} from '@/api/business/allBusiness';
+import transferClue from '@/components/common/transferClue.vue'
+// import correlationSelect from '@/components/common/correlationSelect.vue'
+import addCollaborate from '@/components/common/addCollaborate.vue'
+import turnBussnese from '@/modules/sales/views/business/component/TurnBus.vue'
+import createTask from '@/components/common/createTask.vue'
+import AddFollow from '@/components/Follow/add.vue'
+
+import {AllBusinessVO, AllBusinessQuery, AllBusinessForm} from '@/api/business/allBusiness/types';
+import auth from '@/plugins/auth'
+import Table from '@/components/Table/index.vue'
+import {tableTypes} from '@/components/Table/types'
+import {listCustomer} from "@/api/customer";
+import {CustomerVO} from "@/api/customer/types";
+import {region} from '@/utils/region';
+
+import {useDictCache} from '@/hooks/web/useDict'
+
+const {proxy} = getCurrentInstance() as ComponentInternalInstance;
+const {queryDictDetailByCodes} = useDictCache(['sales_phase', 'business_type', 'transaction_probability', 'sources_of_business', 'task_remind_type', 'task_remind_mode', 'business_stage'])
+queryDictDetailByCodes()
+const addCollaborateRef = ref()
+//创建任务
+const createTaskRef = ref()
+//转移商机
+const turnBusRef = ref()
+const router = useRouter();
+
+const addCollaborateUsers = async () => {
+    if (ids.value.length <= 0) {
+        ElMessage.error('请选择需要添加协作的数据')
+        return
+    }
+    addCollaborateRef.value.open({
+        resourceType: 30,
+        resourceIdList: ids.value
+    })
+}
+const turnBus = async () => {
+    if (ids.value.length <= 0) {
+        ElMessage.error('请选择需要转移的数据')
+        return
+    }
+    turnBusRef.value.open({
+        resourceType: 30,
+        typeName: '商机',
+        idList: ids,
+    })
+}
+
+const addTask = async () => {
+    if (ids.value.length <= 0) {
+        ElMessage.error('请选择需要添加任务的数据')
+        return
+    }
+    createTaskRef.value.open({
+        type: 'add',
+        obj: {
+            resourceType: '30',
+            resourceTypeId: ids.value
+        }
+    })
+}
+
+
+const customerOption = ref([])
+const getCustomerData = () => {
+    listCustomer({pageIndex: 1, pageSize: 999999}).then(res => {
+        if (res.success) {
+            customerOption.value = res.result.records.map((item: { customerName: string, id: number }) => {
+                let obj = {label: item.customerName, value: item.id}
+                return obj
+            })
+        }
+    })
+}
+
+const allBusinessList = ref<AllBusinessVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+
+const props = defineProps({
+    leftBtn: {
+        type: Array,
+        required: true
+    },
+    perPrefix: {
+        type: String,
+        required: true
+    },
+    stage: {
+        type: String,
+        required: true
+    },
+    queryRangeType: {
+        type: [Number, String],
+        required: true
+    },
+    isLoss:{
+        type: Boolean,
+        required: true
+
+    }
+})
+const leftBtn = ref([
+    { type: 'add', hasPermi: `business:${props.perPrefix}:add` },
+    {type: 'edit', hasPermi: `business:${props.perPrefix}:edit`},
+    {type: 'remove', hasPermi: `business:${props.perPrefix}:remove`},
+    {type: 'import', hasPermi: `business:${props.perPrefix}:import`},
+    {type: 'export', hasPermi: `business:${props.perPrefix}:export`}
+])
+
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const showAll = ref(false)
+
+const queryFormRef = ref<ElFormInstance>();
+const allBusinessFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+    visible: false,
+    title: ''
+});
+
+const route = useRoute();
+const detailCus = async (column: AllBusinessVO) => {
+    router.pushTarget('/customer/customerDetail?id=' + column.customerId + '&from=' + props.perPrefix)
+}
+const detail = async (id: string | number) => {
+    router.pushTarget('/business_opportunity/detail?id=' + id + '&type=detail&from=' + props.perPrefix + '&scopMenuId=' + router.currentRoute.value?.meta?.menuId)
+}
+
+const tableParams = reactive<tableTypes<AllBusinessQuery, AllBusinessVO>>({
+    cloumnCode: props.perPrefix,
+    tableColumn: [
+        {
+            show: true, label: '商机标题', prop: 'title', sortable: true, render: ({column}: { column: AllBusinessVO }) => {
+                return h('span', {
+                    class: 'link',
+                    style: {cursor: 'pointer'}, onClick: () => {
+                        detail(column.id)
+                    }
+                }, column.title)
+            }
+        },
+        {
+            show: true, label: '关联客户', prop: 'title', sortable: false, render: ({column}: { column: AllBusinessVO }) => {
+                return h('span', {
+                    class: 'link',
+                    style: {cursor: 'pointer'}, onClick: () => {
+                        detailCus(column)
+                    }
+                }, column.customerName)
+            }
+        },
+        // {show: true, label: '关联客户', prop: 'customerName', sortable: false},
+        {show: true, label: '商机阶段', prop: 'stageName', sortable: false},
+        // {show: true, label: '报价记录', prop: 'offerRecord', sortable: false},
+        {show: true, label: '预计销售金额(¥)', prop: 'estimatedSalesAmount', sortable: true, toFixed: true},
+        {show: true, label: '报价金额(¥)', prop: 'offerAmount', sortable: false, toFixed: true},
+        {show: true, label: '最后跟进', prop: 'lastFollowUpTime', sortable: false},
+        {show: true, label: '未跟进天数', prop: 'notFollowedDays', sortable: false},
+        {show: false, label: '成交机率', prop: 'stransactionProbabilityName', sortable: true},
+        {show: false, label: '商机来源', prop: 'sourcesOfBusinessName', sortable: false},
+        {show: false, label: '获取日期', prop: 'getedDate', sortable: true},
+        {show: false, label: '备注信息', prop: 'remark', sortable: false},
+        {show: props.isLoss, label: '流失原因', prop: 'lossReasonName', sortable: false},
+        {show: false, label: '创建时间', prop: 'createTime', sortable: true},
+        {show: false, label: '修改时间', prop: 'updateTime', sortable: false},
+        {
+            show: true,
+            label: '操作',
+            prop: 'operation',
+            disabledMove: true,
+            render: ({column}: { column: AllBusinessVO }) => {
+                return [
+                    auth.hasPermi(`business:${props.perPrefix}:flowUp`) ? h(ElButton, {
+                        link: true, type: 'primary', onClick: () => {
+                            openFllow(column)
+                        }
+                    }, '跟进') : '',
+                    // auth.hasPermi(`business:${props.perPrefix}:top`) ? h(ElButton, {
+                    //     link: true, type: 'primary', onClick: () => {
+                    //
+                    //     }
+                    // }, '置顶') : '',
+                ]
+            }
+        },
+    ],
+    keyId: 'id',
+    menuId: router.currentRoute.value?.meta?.menuId,
+    queryParams: {
+        blurry: undefined,
+        title: undefined,
+        customerId: undefined,
+        liaisonId: undefined,
+        estimatedSalesAmount: undefined,
+        expectedSigningDate: undefined,
+        salesPhaseDictValue: undefined,
+        businessTypeDictValue: undefined,
+        transactionProbabilityDictValue: undefined,
+        sourcesOfBusinessDictValue: undefined,
+        getedDate: undefined,
+        isDelete: undefined,
+        enabled: undefined,
+        ownerBy: undefined,
+        sort: undefined,
+        queryRangeType: props.queryRangeType,
+        stage: props.stage,
+        hasLoss: props.isLoss ? 1 : undefined,
+        ownerByIds: [],
+        customerName:undefined,
+        country:undefined,
+    },
+    listFn: listAllBusiness,
+    delFn: delAllBusiness,
+    importCode: 'IMPORT_BUSINESS_EXCEL',
+    exportCode: 'EXPORT_BUSINESS_EXCEL',
+    commonQueryOrderBy: 'getedDate'
+})
+const regionValue = ref([])
+const getRegion = (value) => {
+    tableParams.queryParams.country = value?value[1]:''
+}
+// @queryRangeType(value = "归属分类(QueryRangeTypeEnum)(0全部客户、10:我的客户、20:我发起的、30:下属客户、40我协作的、50:下属协作的,70:重点)")
+const addFollow = ref()
+const openFllow = (column: AllBusinessVO) => {
+    let query = {
+        type: 'add',
+        customerId: column.customerId,
+        customerName: column.customerName,
+        resourceType: 30,
+        resourceTypeId: column.id,
+        resourceName: column.title,
+        resourceStageName: column?.stageName,//阶段
+        resourceStageDictValue: column?.stage
+
+    }
+    console.log(query)
+    addFollow.value.open(query)
+}
+const initFormData: AllBusinessForm = {
+    id: undefined,
+    title: undefined,
+    customerId: undefined,
+    liaisonId: undefined,
+    estimatedSalesAmount: undefined,
+    expectedSigningDate: undefined,
+    salesPhaseDictValue: undefined,
+    businessTypeDictValue: undefined,
+    transactionProbabilityDictValue: undefined,
+    sourcesOfBusinessDictValue: undefined,
+    getedDate: undefined,
+    remark: undefined,
+    isDelete: undefined,
+    enabled: undefined,
+    ownerBy: undefined,
+    sort: undefined,
+    isCreateTask: undefined,
+    task: {
+        taskBeginTime: '',
+        taskContent: '',
+        remindModels: [],
+        remindType: undefined,
+        ownerBy: undefined
+    }
+}
+const data = reactive<PageData<AllBusinessForm, AllBusinessQuery>>({
+    form: {...initFormData},
+    rules: {
+        id: [
+            {required: true, message: "商机主键不能为空", trigger: "blur"}
+        ],
+        title: [
+            {required: true, message: "商机标题不能为空", trigger: "blur"}
+        ],
+        customerId: [
+            { required: true, message: "关联客户id不能为空", trigger: "change" }
+        ],
+        liaisonId: [
+            { required: true, message: "主要联系人id不能为空", trigger: "change" }
+        ],
+        estimatedSalesAmount: [
+            {required: true, message: "预计销售金额不能为空", type: 'string', trigger: "change"}
+        ],
+        expectedSigningDate: [
+            {required: true, message: "预计签单日期不能为空", trigger: "change"}
+        ],
+        salesPhaseDictValue: [
+            {required: true, message: "销售阶段不能为空", trigger: "change"}
+        ],
+        businessTypeDictValue: [
+            {required: true, message: "商机类型不能为空", trigger: "change"}
+        ],
+        transactionProbabilityDictValue: [
+            {required: true, message: "成交机率不能为空", trigger: "change"}
+        ],
+        sourcesOfBusinessDictValue: [
+            {required: true, message: "商机来源不能为空", trigger: "change"}
+        ],
+        getedDate: [
+            {required: true, message: "获取日期不能为空", trigger: "blur"}
+        ],
+        remark: [
+            {required: true, message: "备注信息不能为空", trigger: "blur"}
+        ],
+
+        ownerBy: [
+            {required: true, message: "拥有者不能为空", type: 'string', trigger: "blur, change"}
+        ],
+        'task.taskBeginTime': [
+            {required: true, message: "拥有者不能为空", type: 'date', trigger: "blur, change"}
+        ],
+        'task.ownerBy': [
+            {required: true, message: "拥有者不能为空", trigger: "blur, change"}
+        ],
+
+    }
+});
+
+const {form, rules} = toRefs(data);
+
+const {queryParams} = toRefs(tableParams);
+/** 取消按钮 */
+const cancel = () => {
+    reset();
+    dialog.visible = false;
+}
+
+/** 表单重置 */
+const reset = () => {
+    form.value = {...initFormData};
+    allBusinessFormRef.value?.resetFields();
+}
+
+const allBusinessTable = ref()
+/** 搜索按钮操作 */
+const handleQuery = () => {
+    allBusinessTable.value.getData()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+    regionValue.value = []
+    tableParams.queryParams.country = ''
+    queryFormRef.value?.resetFields();
+    handleQuery();
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: AllBusinessVO[]) => {
+    ids.value = selection.map(item => item.id);
+    single.value = selection.length != 1;
+    multiple.value = !selection.length;
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+    router.push('./addbus')
+}
+
+const handleUpdate = (row) => {
+
+    router.push(`./addbus?id=${row.id}`)
+}
+
+const getList = () => {
+    allBusinessTable.value.getData()
+}
+
+
+onMounted(() => {
+    getCustomerData()
+});
+</script>

+ 163 - 0
ui/sp-user-center/src/modules/sales/views/business/component/CostList.vue

@@ -0,0 +1,163 @@
+<template>
+    <el-card shadow="never" class="list-card">
+        <Table ref="costRecordTable"
+               :tableParams="tableParams"
+               :btnRole="leftBtn"
+               @handleAdd="handleAdd"
+               @handleEdit="handleUpdate"
+               @selectionChange="handleSelectionChange"
+        >
+        </Table>
+    </el-card>
+    <addCostForm ref="addCostFormRef" @ok="handleQuery"></addCostForm>
+    <CostFormDetail ref="costFormDetailFormRef" @edite="handleUpdate" @removeOk="handleQuery"></CostFormDetail>
+</template>
+
+<script setup name="CostRecord" lang="ts">
+import {
+    listCostRecord,
+    getCostRecord,
+    delCostRecord,
+    addCostRecord,
+    updateCostRecord
+} from '@/api/finance/costRecord/index';
+import addCostForm from '@/components/common/addCostForm.vue'
+import CostFormDetail from '@/components/common/CostFormDetail.vue'
+import {CostRecordVO, CostRecordQuery, CostRecordForm} from '@/api/finance/costRecord/types';
+import auth from '@/plugins/auth'
+import Table from '@/components/Table/index.vue'
+import {tableTypes} from '@/components/Table/types'
+import {useDictCache} from "@/hooks/web/useDict";
+const props = defineProps({
+    resourceTypeId:{
+        type: [Number, String],
+        required: true
+    },
+    perPrefix:{
+        type:String
+    },
+    customerId: {
+        type: [Number, String],
+        required: true
+    }
+})
+
+const {proxy} = getCurrentInstance() as ComponentInternalInstance;
+const { queryDictDetailByCodes } = useDictCache(['cost_status','cost_type'])
+queryDictDetailByCodes()
+const costRecordList = ref<CostRecordVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const addCostFormRef = ref();
+const ids = ref<Array<string | number>>([]);
+const leftBtn = ref(
+    [
+        {type: 'remove', hasPermi: `${props.perPrefix}:detail:tab_fyjl_sc`},
+        {type: 'add', hasPermi: `${props.perPrefix}:detail:tab_fyjl`},
+        {type: 'edit', hasPermi: `${props.perPrefix}:detail:edit`},
+        {type: 'import', hasPermi: `${props.perPrefix}:detail:import`},
+        {type: 'export', hasPermi: `${props.perPrefix}:detail:tab_fyjl_dc`},
+    ]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const groupData = ref<{ label: string; value: string | number }[]>([])
+const dialog = reactive<DialogOption>({
+    visible: false,
+    title: ''
+});
+
+
+const tableParams = reactive<tableTypes<CostRecordQuery, CostRecordVO>>({
+    tableColumn: [
+        {show: true, label: '费用编号', prop: 'costCode', sortable: true},
+        {show: true, label: '费用名称', prop: 'costName', sortable: true},
+        {show: true, label: '客户名称', prop: 'cusstomerId', sortable: true},
+        {show: true, label: '费用金额', prop: 'costAmount', sortable: true},
+        { show: true, label: '费用类型', prop: 'resourceType', sortable: true },
+        {show: true, label: '发生时间', prop: 'costTakeDate', sortable: true},
+        {show: true, label: '报销状态', prop: 'reimburseStatus', sortable: true},
+        {show: true, label: '负责人员', prop: 'ownerBy', sortable: true},
+        {
+            show: true,
+            label: '操作',
+            width: 200,
+            prop: 'operation',
+            disabledMove: true,
+            render: ({column}: { column: CostRecordVO }) => {
+                return [
+                    auth.hasPermi('business:detail:tab_fyjl_xq') ? h(ElButton, {
+                        link: true, type: 'primary', onClick: () => {
+                        }
+                    }, '详情') : '',
+                ]
+            }
+        },
+    ],
+    keyId: 'id',
+    queryParams: {
+        keyWord: undefined,
+        costCode: undefined,
+        costName: undefined,
+        costAmount: undefined,
+        costTakeDate: undefined,
+        reimburseStatus: undefined,
+        cusstomerId: undefined,
+        resourceType: 30,
+        resourceTypeId: props.resourceTypeId,
+        currencyUnitId: undefined,
+        isDelete: undefined,
+        enabled: undefined,
+        ownerBy: undefined,
+        sort: undefined,
+        startDate: undefined,
+        endDate: undefined,
+    },
+    listFn: listCostRecord,
+    delFn: delCostRecord,
+})
+
+
+const {queryParams} = toRefs(tableParams);
+const costRecordTable = ref()
+/** 搜索按钮操作 */
+const handleQuery = () => {
+    costRecordTable.value.getData()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+    queryFormRef.value?.resetFields();
+    handleQuery();
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: CostRecordVO[]) => {
+    ids.value = selection.map(item => item.id);
+    single.value = selection.length != 1;
+    multiple.value = !selection.length;
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+    let param = {
+        cusstomerId: props.customerId,
+        resourceTypeId: props.resourceTypeId,
+        resourceType: 30,
+    }
+    addCostFormRef.value.handleAdd(param)
+}
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: CostRecordVO) => {
+    addCostFormRef.value.handleUpdate(row)
+}
+const getList = () => {
+    costRecordTable.value.getData()
+}
+onMounted(() => {
+});
+</script>

+ 692 - 0
ui/sp-user-center/src/modules/sales/views/business/component/GetProductPrice.vue

@@ -0,0 +1,692 @@
+<template>
+    <el-dialog title="确认报价" class="get-price-dialog" v-model="checkDialog" width="1400px" append-to-body>
+        <template #header>
+            <div class="dialog-header" style="display: flex;justify-content: center;align-items: center;">
+                <el-select v-model="form.quotationType" style="width: 150px">
+                    <el-option :value="1" label="国内贸易"></el-option>
+                    <el-option :value="2" label="国外贸易"></el-option>
+                </el-select>
+            </div>
+        </template>
+        <el-row>
+            <el-col :span="24" style="border-right: 1px solid #e5e6e7; padding-left: 5px">
+                <el-row style="margin-bottom: 15px">
+                    <el-col :span="24">
+                        <el-button type="primary" icon="plus" @click="addProduct">添加产品</el-button>
+                        <el-button type="primary" icon="plus" @click="addOtherProduct">添加外部产品</el-button>
+                        <el-button type="primary" icon="plus" @click="addOtherFee">添加其他费用</el-button>
+                    </el-col>
+                </el-row>
+                <el-table :data="form.ssdSkuDetailVOList">
+
+                    <el-table-column prop="ssdSkuCode" label="内部编号" width="110"> </el-table-column>
+                    <el-table-column prop="ssdSkuCode" label="规格">
+                        <template #default="scope">
+                            <template v-if="scope.row.ssdSkuCode != '外部产品' && scope.row.ssdSkuCode != '其他费用'">
+                                <span class="ss">{{ scope.row.connectorType }}<small>|<br></small>{{
+                                    scope.row.productSize
+                                    }}<small>|<br></small>{{ scope.row.connectorProtocol }}<small>|<br></small>{{
+                                    scope.row.coreChipType }}<small>|<br></small>{{ scope.row.productLevel
+                                    }}<small>|<br></small>{{ scope.row.flashChipType }}<small>|<br></small>{{
+                                    scope.row.capacity }}</span>
+                            </template>
+                            <template v-else>
+                                <SelectDict v-if="scope.row.ssdSkuCode == '其他费用'" :clearable='false'
+                                    v-model="scope.row.specificationRemarks" placeholder="请选择其他费用"
+                                    dictCode="additional_costs"></SelectDict>
+                                <el-input v-else v-model="scope.row.specificationRemarks"
+                                    placeholder="请输入规格备注"></el-input>
+                            </template>
+
+                        </template>
+                    </el-table-column>
+                    <el-table-column prop="interface" label="功能配置">
+                        <template #default="scope">
+                            <template v-if="scope.row.ssdSkuCode != '外部产品' && scope.row.ssdSkuCode != '其他费用'">
+                                <el-checkbox :disabled="scope.row.forceOpenPhysicalDestroyTemp"
+                                    v-if="scope.row.openPhysicalDestroy === 1" @change="computeData(scope.row)"
+                                    v-model="scope.row.forceOpenPhysicalDestroy">物理销毁
+                                </el-checkbox>
+                                <el-checkbox :disabled="scope.row.forceOpenLogicDestroyTemp"
+                                    v-if="scope.row.openLogicDestroy === 1" @change="computeData(scope.row)"
+                                    v-model="scope.row.forceOpenLogicDestroy">逻辑销毁
+                                </el-checkbox>
+                                <el-checkbox :disabled="scope.row.forceOpenAesEncryptTemp"
+                                    v-if="scope.row.openAesEncrypt === 1" @change="computeData(scope.row)"
+                                    v-model="scope.row.forceOpenAesEncrypt">AES加密
+                                </el-checkbox>
+                                <el-checkbox :disabled="scope.row.forceOpenRapidBackupTemp"
+                                    v-if="scope.row.openRapidBackup === 1" @change="computeData(scope.row)"
+                                    v-model="scope.row.forceOpenRapidBackup">r-Backup
+                                </el-checkbox>
+                            </template>
+                            <template v-else>
+                                <el-input v-model="scope.row.functionRemarks" placeholder="请输入功能备注"></el-input>
+                            </template>
+                        </template>
+                    </el-table-column>
+                    <el-table-column prop="interface" label="数量" width="160">
+                        <template #default="scope">
+                            <el-input-number v-model="scope.row.productCount" style="width: 130px;" step="2"
+                                @change="computeData(scope.row)" :precision="0" :min="1" :max="99999"
+                                label="请输入数量"></el-input-number>
+                        </template>
+                    </el-table-column>
+                    <el-table-column prop="interface" label="国内不含税总价(¥)">
+                        <template #default="scope">
+                            <template v-if="scope.row.ssdSkuCode != '外部产品' && scope.row.ssdSkuCode != '其他费用'">
+                                <p style="font-weight: 600">标准价格:</p>
+                                <p>总价:{{ $formatNumber(scope.row.skuPriceCNYWithNoRate) }}</p>
+                                <p>单价:{{ $formatNumber(scope.row.unitPriceCNYWithNoRate) }}</p>
+                                <p style="font-weight: 600"> 报价金额:</p>
+                                <block v-if="form.quotationType == 1">
+                                    <p style="padding: 6px 0">总价:{{ $formatNumber(scope.row.skuPriceCNYWithNoRateActual
+                                        ) }}</p>
+                                    <p style="padding: 6px 0">单价:{{
+                                        $formatNumber(scope.row.unitPriceCNYWithNoRateActual) }}</p>
+                                </block>
+                                <blcok v-else>
+                                    <el-input title='' @change="computedDataUnit(scope.row, 3)" type="number"
+                                        style="margin-bottom: 5px" v-model="scope.row.skuPriceCNYWithNoRateActual"
+                                        placeholder="请输入总价">
+                                        <template #prepend>总价</template>
+                                    </el-input>
+                                    <el-input title='' @change="computedDataUnit(scope.row, 1)" type="number"
+                                        v-model="scope.row.unitPriceCNYWithNoRateActual" placeholder="请输入单价">
+                                        <template #prepend>单价</template>
+                                    </el-input>
+                                </blcok>
+
+                            </template>
+                            <template v-else>
+                                <p style="font-weight: 600"> 报价金额:</p>
+                                <block v-if="form.quotationType == 1">
+                                    <p style="padding: 6px 0">总价:{{ $formatNumber(scope.row.skuPriceCNYWithNoRateActual
+                                        ) }}</p>
+                                    <p style="padding: 6px 0">单价:{{
+                                        $formatNumber(scope.row.unitPriceCNYWithNoRateActual) }}</p>
+                                </block>
+                                <blcok v-else>
+                                    <el-input type="number" style="margin-bottom: 5px"
+                                        @change="computedDataUnit(scope.row, 3)"
+                                        v-model="scope.row.skuPriceCNYWithNoRateActual" placeholder="请输入总价">
+                                        <template #prepend>总价</template>
+                                    </el-input>
+                                    <el-input type="number" @change="computedDataUnit(scope.row, 1)"
+                                        v-model="scope.row.unitPriceCNYWithNoRateActual" placeholder="请输入单价">
+                                        <template #prepend>单价</template>
+                                    </el-input>
+                                </blcok>
+                            </template>
+                        </template>
+                    </el-table-column>
+                    <el-table-column prop="interface" label="国内价格(¥)(含13%增值税)">
+                        <template #default="scope">
+                            <template v-if="scope.row.ssdSkuCode != '外部产品'  && scope.row.ssdSkuCode != '其他费用'">
+                                <p style="font-weight: 600">标准价格:</p>
+                                <p>总价:{{ $formatNumber(scope.row.skuPriceCNY) }}</p>
+                                <p>单价:{{ $formatNumber(scope.row.unitPriceCNY) }}</p>
+                                <p style="font-weight: 600">报价金额:</p>
+                                <block v-if="form.quotationType == 2">
+                                    <p style="padding: 6px 0">总价:{{ $formatNumber(scope.row.skuPriceCNYActual ) }}</p>
+                                    <p style="padding: 6px 0">单价:{{ $formatNumber(scope.row.unitPriceCNYActual) }}</p>
+                                </block>
+
+                                <block v-else>
+                                    <el-input type="number" @change="computedDataUnit(scope.row, 6)"
+                                        style="margin-bottom: 5px" v-model="scope.row.skuPriceCNYActual"
+                                        placeholder="请输入总价"> <template #prepend>总价</template></el-input>
+                                    <el-input type="number" @change="computedDataUnit(scope.row, 5)"
+                                        v-model="scope.row.unitPriceCNYActual" placeholder="请输入单价"> <template
+                                            #prepend>单价</template></el-input>
+                                </block>
+
+                            </template>
+                            <template v-else>
+                                <p style="font-weight: 600"> 报价金额:</p>
+                                <block v-if="form.quotationType == 2">
+                                    <p style="padding: 6px 0">总价:{{ $formatNumber(scope.row.skuPriceCNYActual ) }}</p>
+                                    <p style="padding: 6px 0">单价:{{ $formatNumber(scope.row.unitPriceCNYActual) }}</p>
+                                </block>
+                                <block v-else>
+                                    <el-input type="number" @change="computedDataUnit(scope.row, 6)"
+                                        style="margin-bottom: 5px" v-model="scope.row.skuPriceCNYActual"
+                                        placeholder="请输入总价"> <template #prepend>总价</template></el-input>
+                                    <el-input type="number" @change="computedDataUnit(scope.row, 5)"
+                                        v-model="scope.row.unitPriceCNYActual" placeholder="请输入单价"> <template
+                                            #prepend>单价</template></el-input>
+                                </block>
+                                <!--                                <el-input type="number" title='' style="margin-bottom: 5px" v-model="scope.row.skuPriceCNYActual" placeholder="请输入总价">  <template #prepend>总价</template></el-input>-->
+                                <!--                                <el-input type="number" title='' v-model="scope.row.unitPriceCNYActual" placeholder="请输入单价"> <template #prepend>单价</template></el-input>-->
+                            </template>
+                        </template>
+                    </el-table-column>
+
+                    <el-table-column prop="interface" :label="`国外价格($)(不含税,美元汇率${USDToCNYRate})`">
+                        <template #default="scope">
+                            <template v-if="scope.row.ssdSkuCode != '外部产品' && scope.row.ssdSkuCode != '其他费用'">
+                                <p style="font-weight: 600">标准价格:</p>
+                                <p>总价:{{ $formatNumber(scope.row.skuPriceUSD) }}</p>
+                                <p>单价:{{ $formatNumber(scope.row.unitPriceUSD) }}</p>
+                                <p style="font-weight: 600">报价金额:</p>
+                                <el-input title='' @blur="computedDataUnit(scope.row, 4)" style="margin-bottom: 5px"
+                                    type="number" v-model="scope.row.skuPriceUSDActual" placeholder="请输入总价"> <template
+                                        #prepend>总价</template></el-input>
+                                <el-input title='' @blur="computedDataUnit(scope.row, 2)" type="number"
+                                    v-model="scope.row.unitPriceUSDActual" placeholder="请输入单价"> <template
+                                        #prepend>单价</template></el-input>
+                            </template>
+                            <template v-else>
+                                <p style="font-weight: 600"> 报价金额:</p>
+                                <el-input title='' style="margin-bottom: 5px" type="number"
+                                    @change="computedDataUnit(scope.row, 4)" v-model="scope.row.skuPriceUSDActual"
+                                    placeholder="请输入总价"> <template #prepend>总价</template></el-input>
+                                <el-input title='' type="number" @change="computedDataUnit(scope.row, 2)"
+                                    v-model="scope.row.unitPriceUSDActual" placeholder="请输入单价"> <template
+                                        #prepend>单价</template></el-input>
+                            </template>
+                        </template>
+                    </el-table-column>
+                    <el-table-column prop="deliveryDays" label="预计生产天数">
+                        <template #default="scope">
+                            <template v-if="scope.row.ssdSkuCode != '外部产品' && scope.row.ssdSkuCode != '其他费用'">
+                                <p>{{ scope.row.deliveryDays }}</p>
+                            </template>
+                            <template v-else>
+                                <el-input type="number" v-model="scope.row.deliveryDays" placeholder="请输入预计生产天数">
+                                    <template #prepend>天数</template></el-input>
+                            </template>
+                        </template>
+                    </el-table-column>
+                    <el-table-column prop="interface" label="操作">
+                        <template #default="scope">
+                            <!--                            <el-button size="small"  @click="detail(scope.row.datasheetUrl)">产品详情</el-button>-->
+                            <!--                            <el-link @click="openSkuDetail(scope.row)" style="margin-right: 5px" type="primary">产品详情</el-link>-->
+                            <el-link style="margin-right: 5px" @click="delPriceSkuData(scope.row)" type="danger"><i
+                                    class="el-icon-delete"></i>删除</el-link>
+
+                            <el-link v-if="scope.row.ssdSkuCode != '外部产品' && scope.row.ssdSkuCode != '其他费用'"
+                                :href="scope.row.datasheetUrl" style="color: #1874ff" tag="a" :download="getName(scope.row.datasheetUrl)"
+                                target="_blank">下载规格书</el-link>
+                            <!--                            <el-button type="danger" size="small" @click="delPriceSkuData(scope.row)">删除</el-button>-->
+                        </template>
+                    </el-table-column>
+                </el-table>
+            </el-col>
+        </el-row>
+        <el-form :model="form" ref="queryFormRef" style="margin-top: 20px" :inline="true">
+            <el-row>
+                <el-col :span="24">
+                    <div style="display: flex;justify-content: end; width: 100%">
+                        <el-form-item label="产品数量:">
+                            {{ countNum }}
+                        </el-form-item>
+                        <el-form-item label="产品总金额:">
+                            {{ $formatNumber(totalMoney) }}
+                        </el-form-item>
+                        <el-form-item label="其他费用:">
+                            {{ $formatNumber(otherTotalMoney) }}
+                        </el-form-item>
+                        <el-form-item>
+                            <el-button type="primary" @click="downPdf(1)">下载中文报价</el-button>
+                            <el-button type="primary" @click="downPdf(2)">下载英文报价</el-button>
+                        </el-form-item>
+                    </div>
+                </el-col>
+            </el-row>
+        </el-form>
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button @click="reset">取消</el-button>
+                <el-button type="primary" :loading="submitLoading" @click="submit">提交报价</el-button>
+
+            </div>
+        </template>
+    </el-dialog>
+
+    <ProductPrice ref="productPriceRef" @callBack="okPrice"></ProductPrice>
+</template>
+
+<script setup lang="ts">
+import skuDetail from './skuDetail.vue'
+import {
+    downLodaQuotation, getDetail,
+    listProductType,
+    querySkuVoList,
+    quotationAdd,
+    ssdStep1GetSku,
+    ssdStep1GetSkuList,
+    getUSDtoCnyRate,
+    quotationEdite,
+    getPriceCNY,
+    getPriceUSD
+} from '@/api/product/index'
+import downloadPlugins from '@/plugins/download'
+import {
+    ProductTypeVo,
+    SkuVo,
+    SkuQueryVo,
+    ProductTypeQuery,
+    MakeOrderSkuDetailVO,
+    SsdSkuDetailVOList
+} from '@/api/product/types'
+import {AllBusinessForm, AllBusinessQuery} from "@/api/business/allBusiness/types";
+import {useDictCache} from "@/hooks/web/useDict";
+const { queryDictDetailByCodes } = useDictCache(['additional_costs' ])
+queryDictDetailByCodes()
+
+const emits = defineEmits(['ok'])
+const skuData = ref<Array<SkuVo>>([])
+
+const isEdite = ref(false)
+const submitLoading = ref(false)
+
+const productTypeListVo = ref<Array<ProductTypeVo>>()
+const skuVoList = ref<Array<SkuVo>>()
+
+//确认报价
+const checkDialog = ref(false)
+//确认报价数据
+// const priceData = ref<Array<MakeOrderSkuDetailVO>>([])
+
+const initFormData:SsdSkuDetailVOList={
+    totalEarnestAmt: undefined,
+    totalNumber: undefined,
+    totalOrderAmt: undefined,
+    abutmentUserId: undefined,
+    businessId: undefined,
+    customerId: undefined,
+    describe: undefined,
+    id: undefined,
+    totalMoney: undefined,
+    orderFormId: undefined,
+    rate: undefined,
+    quotationType: 2,
+    ssdSkuDetailVOList:[]
+}
+//添加外部产品
+const addOtherProduct = ()=>{
+    let param = {
+        ssdSkuCode: '外部产品',
+        status: 2,
+        pageIndex: 1,
+        pageSize: 1,
+    }
+
+    getSkuList(param)
+}//添加外部产品
+const addOtherFee = ()=>{
+    let param = {
+        ssdSkuCode: '其他费用',
+        status: 2,
+        pageIndex: 1,
+        pageSize: 1,
+    }
+
+    getSkuList(param)
+}
+// const queryFormsSku =reactive( )
+
+const getSkuList = async (queryFormsSku)=>{
+    let data  = await querySkuVoList(queryFormsSku)
+    console.log(data.result.records)
+    okPrice(data.result.records)
+    // skuData.value = data.result.records
+}
+
+const computedDataUnit = async (data, type)=>{
+    let temp = form.value.ssdSkuDetailVOList.find(i=>i.randomId == data.randomId)
+    //国内
+    temp.quotationPricetype = type
+    let res = await getPriceCNY(temp)
+    Object.assign(data, temp)
+    if( res.result.skuPriceCNYActual) {
+        data.skuPriceCNYActual = res.result.skuPriceCNYActual
+    }
+    if( res.result.unitPriceCNYActual) {
+        data.unitPriceCNYActual = res.result.unitPriceCNYActual
+    }
+    if( res.result.unitPriceCNYWithNoRateActual) {
+        data.unitPriceCNYWithNoRateActual = res.result.unitPriceCNYWithNoRateActual
+    }
+    if( res.result.skuPriceUSDActual) {
+        data.skuPriceUSDActual = res.result.skuPriceUSDActual
+    }
+    if( res.result.unitPriceUSDActual) {
+        data.unitPriceUSDActual = res.result.unitPriceUSDActual
+    }
+    if( res.result.skuPriceCNYWithNoRateActual) {
+        data.skuPriceCNYWithNoRateActual = res.result.skuPriceCNYWithNoRateActual
+    }
+    // //总价 CNY含税(实际)
+    // data.skuPriceCNYActual = res.result.skuPriceCNYActual ? res.result.skuPriceCNYActual : 0
+    // //总价 CNY不含税(实际)
+    // data.skuPriceCNYWithNoRateActual = res.result.skuPriceCNYWithNoRateActual ? res.result.skuPriceCNYWithNoRateActual : 0
+    // //单价CNY含税(实际)
+    // data.unitPriceCNYActual = res.result.unitPriceCNYActual ? res.result.unitPriceCNYActual : 0
+    //
+    // //单价CNY不含税(实际)
+    // data.unitPriceCNYWithNoRateActual = res.result.unitPriceCNYWithNoRateActual ? res.result.unitPriceCNYWithNoRateActual : 0
+    // //美元总价
+    // data.skuPriceUSDActual = res.result.skuPriceUSDActual ? res.result.skuPriceUSDActual : 0
+    // //美元单价
+    // data.unitPriceUSDActual = res.result.unitPriceUSDActual ? res.result.unitPriceUSDActual : 0
+}
+
+const data = reactive({
+    form: {...initFormData},
+    rules: {
+        id: [
+            {required: true, message: "商机主键不能为空", trigger: "blur"}
+        ],
+    }
+})
+
+const {form, rules} = toRefs(data);
+
+
+const downPdf = async (type)=>{
+    if(form.value.ssdSkuDetailVOList.length <= 0) {
+        ElMessage.error('请添加产品')
+        return
+    }
+
+    form.value.ssdSkuDetailVOList.forEach(item=>{
+        item.languageType = type
+    })
+    let param = {
+        quotationType: form.value.quotationType,
+        ssdSkuDetailVOList: form.value.ssdSkuDetailVOList
+    }
+    let time = new Date().getTime()
+    let name = type === 1 ? `报价单-${time}.docx` : `Quotation-${time}.docx`
+    await downLodaQuotation(param, name)
+}
+
+//提交报价
+const submit=async ()=>{
+    submitLoading.value = true
+    if (form.value.ssdSkuDetailVOList.length <= 0) {
+        ElMessage.error('请添加产品')
+        submitLoading.value = false
+        return
+    }
+    if (form.value.businessId) {
+        let res = null
+        if (isEdite.value) {
+            res = await quotationEdite(form.value)
+        } else {
+            res = await quotationAdd(form.value)
+        }
+
+        if(res.success) {
+            ElMessage.success((isEdite.value ? '编辑' : '添加') + '成功')
+            reset()
+            emits('ok')
+            checkDialog.value = false
+        }else{
+            ElMessage.error((isEdite.value ? '编辑' : '添加') + '添加失败')
+        }
+        emits('ok')
+        submitLoading.value = false
+        return
+    }
+    submitLoading.value = false
+    checkDialog.value = false
+    form.value.totalMoney = totalMoney.value
+    emits('ok', form.value)
+}
+const computeData = async (data) => {
+    // if(data.ssdSkuCode == '外部产品') return
+
+    let temp = form.value.ssdSkuDetailVOList.find(i=>i.randomId == data.randomId)
+    computedDataUnit(temp,2)
+    let res = await ssdStep1GetSku(temp)
+    Object.assign(temp, res.result)
+}
+
+const skuDetailRef = ref()
+const openSkuDetail = (data)=>{
+    skuDetailRef.value.open(data)
+}
+const USDToCNYRate = ref(0)
+const getRate =  ()=>{
+    getUSDtoCnyRate().then(res=>{
+        USDToCNYRate.value = res.result
+        form.value.rate = res.result
+    })
+
+}
+
+//确认报价
+const okPrice = async (data) => {
+    data.forEach(item=>{
+        let param: MakeOrderSkuDetailVO = {
+            randomId:  generateRandomId(22),
+            ssdSkuId: 0,
+            ssdSkuCode: '',
+            connectorType: '',
+            coreChipType: '',
+            productLevel: '',
+            productSize: '',
+            flashChipType: '',
+            connectorProtocol: '',
+            capacity: '',
+            flashChipCount: 0,
+            flashChipPrice: 0,
+            additionalSurcharges: 0,
+            coreChipPrice: 0,
+            openPhysicalDestroy: 0,
+            physicalDestroyPrice: 0,
+            openLogicDestroy: 0,
+            logicDestroyPrice: 0,
+            openAesEncrypt: 0,
+            aesEncryptPrice: 0,
+            openRapidBackup: 0,
+            rapidBackupPrice: 0,
+            productCount: 1,
+            materialPricing: 0,
+            processingPricing: 0,
+            rate: 0,
+            ssdCoreShipId: 0,
+            ssdFlashShipId: 0,
+            estimateDay: 0,
+            datasheetName: '',
+            datasheetUrl: '',
+            skuPrice: 0,
+            skuunitPrice: 0,
+            forceOpenPhysicalDestroy: false,
+            forceOpenLogicDestroy: false,
+            forceOpenAesEncrypt: false,
+            forceOpenRapidBackup: false,
+            forceOpenPhysicalDestroyTemp: false,
+            forceOpenLogicDestroyTemp: false,
+            forceOpenAesEncryptTemp: false,
+            forceOpenRapidBackupTemp: false,
+            repeatData: false,
+            unitPriceCNY: 0,
+            unitPriceUSD: 0,
+            skuPriceUSD: 0,
+            USDToCNYRate: 0,
+            languageType: 1,
+            skuPriceCNYWithNoRate: 0,
+            unitPriceCNYWithNoRate: 0,
+            skuPriceCNY: 0,
+            skuPriceCNYActual: 0,
+            skuPriceCNYWithNoRateActual: 0,
+            skuPriceUSDActual: 0,
+            unitPriceCNYActual: 0,
+            unitPriceCNYWithNoRateActual: 0,
+            unitPriceUSDActual: 0,
+        };
+
+        Object.assign(param, item)
+
+        param.productCount = 1
+        form.value.ssdSkuDetailVOList.push(param)
+    })
+    let param: SsdSkuDetailVOList = {
+        ssdSkuDetailVOList:  form.value.ssdSkuDetailVOList
+    }
+    let res = await ssdStep1GetSkuList(param)
+    res.result.forEach(item=>{
+        form.value.ssdSkuDetailVOList.forEach(jtem=>{
+            if (item.randomId == jtem.randomId && jtem.ssdSkuCode != '外部产品') {
+                Object.assign(jtem, item)
+            }
+        })
+    })
+}
+
+const getName = (url) =>{
+    return url ? url.substring(url.lastIndexOf('/')) : '' ;
+}
+const reset=()=>{
+    form.value = {...initFormData}
+    form.value.ssdSkuDetailVOList = []
+    submitLoading.value = false
+    checkDialog.value = false
+}
+
+const delPriceSkuData = (item) =>{
+    form.value.ssdSkuDetailVOList = form.value.ssdSkuDetailVOList.filter(i=>i.randomId != item.randomId)
+}
+const generateRandomId = (length)=> {
+    var result = '';
+    var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+    var charactersLength = characters.length;
+    for (var i = 0; i < length; i++) {
+        result += characters.charAt(Math.floor(Math.random() * charactersLength));
+    }
+    return result;
+}
+
+const getSureSku=(data)=>{
+    console.log(data)
+    data.forEach(item=>{
+        item.randomId = generateRandomId(22)
+    })
+    skuData.value.push(...data)
+}
+const productPriceRef = ref()
+const addProduct = ()=>{
+
+    productPriceRef.value.open()
+}
+
+const download = (url)=>{
+    downloadPlugins.oss(url)
+}
+const getCheckedNodes = ()=>{
+
+}
+//产品数量
+const countNum = computed(()=>{
+    let count:number = 0
+    form.value.ssdSkuDetailVOList.forEach((item)=>{
+        if(item.ssdSkuCode != '其他费用') {
+            count += item.productCount
+        }
+    })
+    return count
+})//产品数量
+
+
+//产品总金额不含税
+const totalMoney = computed(()=>{
+    let count: number = 0
+
+    form.value.ssdSkuDetailVOList.forEach((item) => {
+        if(item.ssdSkuCode != '其他费用') {
+            if(form.value.quotationType == 2) {
+                count += item.skuPriceCNYWithNoRateActual ? Number(item.skuPriceCNYWithNoRateActual) : 0
+            } else {
+                count += item.skuPriceCNYActual ? Number(item.skuPriceCNYActual) : 0
+            }
+
+        }
+    })
+    return count.toFixed(2)
+})
+//产品总金额不含税
+const otherTotalMoney = computed(()=>{
+    let count: number = 0
+    form.value.ssdSkuDetailVOList.forEach((item) => {
+        if(item.ssdSkuCode == '其他费用') {
+            if(form.value.quotationType == 2) {
+                count += item.skuPriceCNYWithNoRateActual ? Number(item.skuPriceCNYWithNoRateActual) : 0
+            } else {
+                count += item.skuPriceCNYActual ? Number(item.skuPriceCNYActual) : 0
+            }
+            // count += item.skuPriceCNYWithNoRateActual ? Number(item.skuPriceCNYWithNoRateActual) : 0
+        }
+    })
+    return count.toFixed(2)
+})
+
+//产品总金额含税
+const totalMoneyNoRate = computed(()=>{
+    let count: number = 0
+    form.value.ssdSkuDetailVOList.forEach((item) => {
+        if(item.ssdSkuCode != '其他费用') {
+            count += item.skuPriceCNYWithNoRateActual ? Number(item.skuPriceCNYWithNoRate) : 0
+        }
+    })
+    return count.toFixed(2)
+})
+
+
+const open = async (businessId, customerId) => {
+    reset()
+    isEdite.value = false
+    form.value.businessId = businessId
+    form.value.customerId = customerId
+    checkDialog.value = true
+}
+const getQuotationList = async () =>{
+
+}
+
+const edite = async (param, callBack)=>{
+    reset()
+    isEdite.value = true
+    let data = await getDetail(param);
+    form.value = data.result
+    form.value.ssdSkuDetailVOList.forEach(item=>{
+        item.randomId = generateRandomId(22)
+        // item.forceOpenPhysicalDestroyTemp = item.forceOpenPhysicalDestroy
+        // item.forceOpenLogicDestroyTemp = item.forceOpenLogicDestroy
+        // item.forceOpenAesEncryptTemp = item.forceOpenAesEncrypt
+        // item.forceOpenRapidBackupTemp = item.forceOpenRapidBackup
+    })
+
+    checkDialog.value = true
+    if(callBack && typeof callBack === 'function') {
+        callBack(true)
+    }
+
+}
+
+
+defineExpose({
+    open,
+    edite
+})
+const selectedSkuVo = ref<SkuVo>()
+const handleSelectionChange=(selection)=>{
+    selectedSkuVo.value = selection
+}
+
+onMounted(()=>{
+    getRate()
+})
+</script>
+
+<style lang="scss" scoped>
+    .dialog-header{
+        display: flex;
+        justify-content: center;
+        align-items: center;
+
+    }
+</style>

+ 215 - 0
ui/sp-user-center/src/modules/sales/views/business/component/Information.vue

@@ -0,0 +1,215 @@
+<template>
+    <div class="nav_detail">
+        <div class="nav_sec">
+            <div class="title flex">
+                <span class="bold">基本信息</span>
+            </div>
+            <div class="form_area flex">
+                <div class="form_item">
+                    <div class="flex">
+                        <div class="label">商机标题:</div>
+                        <div class="value">{{ detailData?.title }}</div>
+                    </div>
+                    <div class="flex">
+                        <div class="label">关联订单:</div>
+                        <div class="value">
+                            <a style="color: #1874FF" v-if="detailData?.orderFormId"  target="_blank"
+                                :href="`/order/detail?id=${detailData?.orderFormId}`">
+                                {{ detailData?.orderFormTitle }}</a>
+                            <span v-else> --</span>
+                        </div>
+                    </div>
+                    <div class="flex">
+                        <div class="label">预计销售金额({{ !detailData?.currency ||  detailData?.currency == 1? 'CNY' : `USD`  }}):</div>
+                        <div class="value flex justify-between">
+                            <div>{{ detailData?.initialSalesAmount ?
+                                detailData?.initialSalesAmount.toFixed(2) : '--' }}</div>
+                            <div>{{ !detailData?.currency ||  detailData?.currency == 1? '' : `汇率${detailData?.exchangeRate},折算人民币${detailData?.estimatedSalesAmount}元`  }}</div>
+                        </div>
+
+                    </div>
+                    <div class="flex">
+                        <div class="label">销售阶段:</div>
+                        <div class="value">{{ detailData?.stageName }}</div>
+                    </div>
+                    <div class="flex">
+                        <div class="label">成交机率:</div>
+                        <div class="value">{{ detailData?.stransactionProbabilityName || '--' }}</div>
+                    </div>
+                    <div class="flex">
+                        <div class="label">获取日期:</div>
+                        <div class="value">{{ detailData?.getedDate || '--' }}</div>
+                    </div>
+                    <!-- <div class="flex">
+                        <div class="label">备注信息:</div>
+                        <div class="value">{{ detailData?.remark || '--' }}</div>
+                    </div> -->
+
+                </div>
+                <div class="form_item">
+                    <div class="flex">
+                        <div class="label">关联客户:</div>
+                        <div class="value">
+                            <a style="color: #1874FF" v-if="detailData?.customerId" type="primary" target="_blank"
+                                :href="`/customer/customerDetail?id=${detailData?.customerId}`"> {{
+                                    detailData?.customerName }}</a>
+                            <span v-else> --</span>
+                        </div>
+                    </div>
+                    <div class="flex">
+                        <div class="label">主要联系人:</div>
+                        <div class="value">{{ detailData?.liaisonName || '--' }}</div>
+                    </div>
+                    <div class="flex">
+                        <div class="label">预计签单日期:</div>
+                        <div class="value">{{ detailData?.expectedSigningDate || '--' }}</div>
+                    </div>
+                    <div class="flex">
+                        <div class="label">商机类型:</div>
+                        <div class="value">{{ detailData?.businessTypeName || '--' }}</div>
+                    </div>
+                    <div class="flex">
+                        <div class="label">商机来源:</div>
+                        <div class="value">{{ detailData?.sourcesOfBusinessName || '--' }}</div>
+                    </div>
+                    <div class="flex">
+                        <div class="label"> 归属人员:</div>
+                        <div class="value">{{ detailData?.ownerByName || '--' }}</div>
+                    </div>
+                    <!-- <div class="flex">
+                        <div class="label"> </div>
+                        <div class="value"></div>
+                    </div> -->
+                </div>
+            </div>
+            <div class="form_area flex" style="border:none">
+                <div class="flex form_item" style="flex:1">
+                    <div class="label">备注信息</div><div class="value">{{ detailData?.remark || '--' }}</div>
+                </div>
+                <div class="flex form_item" style="flex:1">
+                    <div class="label"></div><div class="value"></div>
+                </div>
+            </div>
+        </div>
+        <div class="nav_sec">
+            <div class="title flex">
+                <span class="bold">系统信息</span>
+            </div>
+            <div class="form_area flex">
+                <div class="form_item">
+                    <div class="flex">
+                        <div class="label">系统编号:</div>
+                        <div class="value">{{ detailData?.dataCode || '--' }}</div>
+                    </div>
+                    <div class="flex">
+                        <div class="label">所属部门:</div>
+                        <div class="value">{{ detailData?.deptName || '--' }}</div>
+                    </div>
+                    <div class="flex">
+                        <div class="label">前归属人:</div>
+                        <div class="value">{{ detailData?.formerOwnerByName || '--' }}</div>
+                    </div>
+                    <div class="flex">
+                        <div class="label">创建时间:</div>
+                        <div class="value">{{ detailData?.createTime || '--' }}</div>
+                    </div>
+                    <div class="flex">
+                        <div class="label">最后跟进:</div>
+                        <div class="value">{{ detailData?.lastFollowUpTime || '--' }}</div>
+                    </div>
+                </div>
+                <div class="form_item">
+                    <div class="flex">
+                        <div class="label">协作人员:</div>
+                        <div class="value">{{detailData?.collaboratorName || '--'}}</div>
+                    </div>
+                    <div class="flex">
+                        <div class="label"> 创建人员:</div>
+                        <div class="value">{{detailData?.createByName || '--'}}</div>
+                    </div>
+                    <div class="flex">
+                        <div class="label">前所属部门:</div>
+                        <div class="value">{{detailData?.formerDeptName || '--'}}</div>
+                    </div>
+                    <div class="flex">
+                        <div class="label">更新时间:</div>
+                        <div class="value">{{detailData?.updateTime || '--'}}</div>
+                    </div>
+                    <div class="flex">
+                        <div class="label">下次跟进:</div>
+                        <div class="value">{{detailData?.nextFollowUpTime || '--'}}</div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="nav_sec">
+            <div class="title flex">
+                <span class="bold">商机动态</span>
+                <span class="flex-center">
+                    <el-checkbox v-model="onlyFollow" size="large" />
+                    <span style="margin: 0 16px 0 4px;">只看跟进</span>
+                    <el-button type="primary" @click="openDialog()">写跟进</el-button>
+                </span>
+            </div>
+            <div>
+                <Follow :btnRole="leftBtn" :onlyShowFollow="onlyFollow" :followParams="followParams"
+                    :showAll="onlyFollow"></Follow>
+            </div>
+        </div>
+        <AddFollow ref="addFollowRef"></AddFollow>
+    </div>
+</template>
+
+
+<script setup lang="ts">
+    import Follow from '@/components/Follow/index.vue'
+    import AddFollow from '@/components/Follow/add.vue'
+    const props = defineProps({
+        //关联客户id
+        detailData: {
+            type: Object,
+            default: {}
+        },
+
+    })
+    const addFollowRef = ref()
+    const onlyFollow = ref(false)
+    const router: any = useRouter();
+    let queryData = router.currentRoute.value.query
+    const followParams:any = ref({
+        resourceType:30,
+        resourceTypeId: queryData.id,
+    })
+    const customerDetail = reactive({})
+    const {detailData} = toRefs(props)
+    const openDialog = ()=>{
+        let param = {
+            type: 'add',
+            customerId: detailData?.value?.customerId,
+            customerName: detailData?.value?.customerName,
+            resourceType: 30,
+            resourceTypeId: detailData?.value?.id,
+            resourceName: detailData?.value?.title,
+            resourceStageName: detailData?.value?.stageName,
+            resourceStageDictValue: detailData?.value?.stage
+        }
+        addFollowRef.value.open(param)
+    }
+
+
+const leftBtn = ref([
+    { type: 'detail', hasPermi: `business:detail:tab_gjjl_xq` },
+    { type: 'commit', hasPermi: `business:detail:tab_gjjl_tj` },
+    { type: 'remove', hasPermi: `business:detail:tab_gjjl_sc` },
+    { type: 'edit', hasPermi: `business:detail:tab_gjjl_bj` },
+
+])
+    defineExpose({
+        open
+    })
+
+
+</script>
+<style lang="scss" scoped>
+
+</style>

+ 277 - 0
ui/sp-user-center/src/modules/sales/views/business/component/QuotationList.vue

@@ -0,0 +1,277 @@
+<template>
+        <el-row>
+            <el-col :span="24" style=" padding-left: 5px">
+                <el-row style="margin-bottom: 15px">
+                    <el-col :span="24">
+                        <div style="display: flex;justify-content: end;margin-top: 15px">
+                            <div style="display: flex;align-items: center; margin-right: 5px;">
+                                <span> 产品数量: {{ssdSkuDetailVOList.totalNumber}}</span>
+                                <span style="margin-left: 15px">产品总金额(¥): {{$formatNumber(ssdSkuDetailVOList.productTotalOrderAmt)}}</span>
+                                <span style="margin-left: 15px">其他费用(¥): {{$formatNumber(ssdSkuDetailVOList.otherAmt)}}</span>
+                                <el-button v-hasPermi="[`${perPrefix}:detail:tab_cpbj_bj`]"  :disabled="!ssdSkuDetailVOList.ssdSkuDetailVOList || ssdSkuDetailVOList.ssdSkuDetailVOList?.length <= 0" :loading="editeLoading" icon="Edit" style="margin-left: 10px" @click="edite" type="primary">编辑</el-button>
+                                <el-button v-hasPermi="[`${perPrefix}:detail:tab_cpbj_xzzwbjd`]" :disabled="!ssdSkuDetailVOList.ssdSkuDetailVOList || ssdSkuDetailVOList.ssdSkuDetailVOList?.length <= 0" style="margin-left: 15px" @click="downPdf(1)" icon="Download" type="primary" >下载中文报价单</el-button>
+                                <el-button v-hasPermi="[`${perPrefix}:detail:tab_cpbj_xzywbjd`]" :disabled="!ssdSkuDetailVOList.ssdSkuDetailVOList || ssdSkuDetailVOList.ssdSkuDetailVOList?.length <= 0" style="margin-left: 15px" @click="downPdf(2)" icon="Download" type="primary" >下载英文报价单</el-button>
+                            </div>
+                        </div>
+                    </el-col>
+                </el-row>
+                <el-table :data="ssdSkuDetailVOList.ssdSkuDetailVOList" >
+                    <el-table-column  prop="ssdSkuCode"  label="内部编号" > </el-table-column>
+                    <el-table-column  prop="ssdSkuCode"  label="规格"  width="120">
+                        <template #default="scope">
+                            <template v-if="scope.row.ssdSkuCode != '外部产品' && scope.row.ssdSkuCode != '其他费用'">
+
+                             <span class="ss">{{scope.row.connectorType}}<small>|</small>{{scope.row.productSize}}<small>|</small>{{scope.row.connectorProtocol}}<small>|</small>{{scope.row.coreChipType}}<small>|</small>{{scope.row.productLevel}}<small>|</small>{{scope.row.flashChipType}}<small>|</small>{{scope.row.capacity}}</span>
+                            </template>
+                            <template v-else>
+                                <span>
+                                    {{scope.row.specificationRemarks}}
+                                </span>
+                            </template>
+                        </template>
+                    </el-table-column>
+                    <el-table-column  prop="interface"  label="功能配置" >
+                        <template  #default="scope">
+                            <template v-if="scope.row.ssdSkuCode != '外部产品' && scope.row.ssdSkuCode != '其他费用'">
+                                <label style="color:#999999;">逻辑销毁:</label><span style="color: #555555;">{{scope.row.forceOpenLogicDestroy  ? '开通' : '不开通'}} </span><br>
+                                <label style="color:#999999;">物理销毁:</label><span style="color: #555555;">{{scope.row.forceOpenPhysicalDestroy ? '开通' : '不开通'}}</span><br>
+                                <label style="color:#999999;">AES加密:</label><span style="color: #555555;">{{scope.row.forceOpenAesEncrypt ? '开通' : '不开通'}}</span><br>
+                                <label style="color:#999999;">r-Backup:</label><span style="color: #555555;">{{scope.row.forceOpenRapidBackup? '开通' : '不开通'}}</span><br>
+                            </template>
+                            <template v-else>
+                                <span>
+                                    {{scope.row.functionRemarks}}
+                                </span>
+                            </template>
+                        </template>
+                    </el-table-column>
+                    <el-table-column  prop="interface"  label="数量" width="130">
+                        <template  #default="scope">
+                            {{ scope.row.productCount }}
+                        </template>
+                    </el-table-column>
+                    <el-table-column  prop="interface"  label="国内不含税总价(¥)" >
+                        <template  #default="scope">
+<!--                            <p>总价(¥):{{ $formatNumber(scope.row.skuPriceCNYWithNoRate) }}</p>-->
+<!--                            <p>单价(¥):{{ $formatNumber(scope.row.unitPriceCNYWithNoRate) }}</p>-->
+
+                            <template v-if="scope.row.ssdSkuCode != '外部产品' && scope.row.ssdSkuCode != '其他费用'">
+                                <p style="font-weight: 600">标准价格:</p>
+                                <p>总价:{{ $formatNumber(scope.row.skuPriceCNYWithNoRate) }}</p>
+                                <p>单价:{{ $formatNumber(scope.row.unitPriceCNYWithNoRate) }}</p>
+                                <p style="font-weight: 600"> 报价金额:</p>
+                                <p>总价:{{ $formatNumber(scope.row.skuPriceCNYWithNoRateActual) }}</p>
+                                <p>单价:{{ $formatNumber(scope.row.unitPriceCNYWithNoRateActual) }}</p>
+                            </template>
+                            <template v-else>
+                                <p style="font-weight: 600"> 报价金额:</p>
+                                <p>总价:{{ $formatNumber(scope.row.skuPriceCNYWithNoRateActual) }}</p>
+                                <p>单价:{{ $formatNumber(scope.row.unitPriceCNYWithNoRateActual) }}</p>
+                            </template>
+
+                        </template>
+                    </el-table-column>
+                    <el-table-column  prop="interface"  label="国内价格(¥)(含13%增值税)" >
+<!--                        <template  #default="scope">-->
+<!--                            <p>总价(¥):{{ $formatNumber(scope.row.skuPriceCNY) }}</p>-->
+<!--                            <p>单价(¥):{{ $formatNumber(scope.row.unitPriceCNY) }}</p>-->
+<!--                        </template>-->
+                        <template #default="scope">
+                            <template v-if="scope.row.ssdSkuCode != '外部产品' && scope.row.ssdSkuCode != '其他费用'">
+                                <p style="font-weight: 600">标准价格:</p>
+                                <p>总价:{{ $formatNumber(scope.row.skuPriceCNY) }}</p>
+                                <p>单价:{{ $formatNumber(scope.row.unitPriceCNY) }}</p>
+                                <p style="font-weight: 600">报价金额:</p>
+                                <p >总价:{{ $formatNumber(scope.row.skuPriceCNYActual ) }}</p>
+                                <p >单价:{{ $formatNumber(scope.row.unitPriceCNYActual) }}</p>
+                            </template>
+                            <template v-else>
+                                <p style="font-weight: 600"> 报价金额:</p>
+                                <p>总价:{{ $formatNumber(scope.row.skuPriceCNYActual) }}</p>
+                                <p>单价:{{ $formatNumber(scope.row.unitPriceCNYActual) }}</p>
+                            </template>
+                        </template>
+                    </el-table-column>
+
+                    <el-table-column  prop="interface"  :label="`国外价格($)(不含税,美元汇率${USDToCNYRate ? USDToCNYRate.toFixed(4): '' })`" >
+<!--                        <template  #default="scope">-->
+<!--                            <p>总价($):{{ $formatNumber(scope.row.skuPriceUSD) }}</p>-->
+<!--                            <p>单价($):{{ $formatNumber(scope.row.unitPriceUSD) }}</p>-->
+<!--                        </template>-->
+
+                        <template #default="scope">
+                            <template v-if="scope.row.ssdSkuCode != '外部产品' && scope.row.ssdSkuCode != '其他费用'">
+                                <p style="font-weight: 600">标准价格:</p>
+                                <p>总价:{{ $formatNumber(scope.row.skuPriceUSD) }}</p>
+                                <p>单价:{{ $formatNumber(scope.row.unitPriceUSD) }}</p>
+                                <p style="font-weight: 600">报价金额:</p>
+                                <p>总价:{{ $formatNumber(scope.row.skuPriceUSDActual) }}</p>
+                                <p>单价:{{ $formatNumber(scope.row.unitPriceUSDActual) }}</p>
+                            </template>
+                            <template v-else>
+                                <p style="font-weight: 600"> 报价金额:</p>
+                                <p>总价:{{ $formatNumber(scope.row.skuPriceUSDActual) }}</p>
+                                <p>单价:{{ $formatNumber(scope.row.unitPriceUSDActual) }}</p>
+                            </template>
+                        </template>
+                    </el-table-column>
+                    <el-table-column  prop="deliveryDays"  label="预计生产天数" > </el-table-column>
+                    <el-table-column  prop="interface"  label="操作" >
+                        <template  #default="scope">
+                            <el-link @click="openSkuDetail(scope.row)" style="margin-right: 5px" type="primary">产品详情</el-link>
+                        </template>
+                    </el-table-column>
+                </el-table>
+            </el-col>
+        </el-row>
+    <GetProductPrice ref="getProductPriceRef" @ok="getQuotationList"></GetProductPrice>
+    <skuDetail ref="skuDetailRef"></skuDetail>
+</template>
+
+<script setup lang="ts">
+import GetProductPrice from './GetProductPrice.vue'
+import skuDetail from './skuDetail.vue'
+import {downLodaQuotation, downLodaQuotationById, getDetail, getUSDtoCnyRate} from '@/api/product/index'
+import downloadPlugins from '@/plugins/download'
+import {
+    SsdSkuDetailVOList
+} from '@/api/product/types'
+import {AllBusinessForm, AllBusinessQuery} from "@/api/business/allBusiness/types";
+const emits = defineEmits(['ok'])
+const props = defineProps({
+    resourceTypeId:{
+        type: [Number, String],
+        required: true
+    },
+    resourceType:{
+        type: [Number, String]
+    },
+    perPrefix:{
+        type:String
+    }
+})
+watch(()=>props.resourceTypeId, (newValue)=>{
+
+    getQuotationList()
+})
+
+const leftBtn = ref([
+    {type: 'edit', hasPermi: `${props.perPrefix}:business:edit`},
+    {type: 'export', hasPermi: `${props.perPrefix}:business:export`}
+])
+
+
+const ssdSkuDetailVOList = ref<SsdSkuDetailVOList>({
+    ssdSkuDetailVOList:[],
+    totalEarnestAmt:0,
+    totalNumber:0,
+    totalOrderAmt:0,
+    productTotalOrderAmt: 0,
+    otherAmt: 0
+})
+const skuDetailRef = ref()
+const USDToCNYRate = ref(0)
+
+const getRate =  ()=>{
+    getUSDtoCnyRate().then(res=>{
+        USDToCNYRate.value = res.result
+    })
+}
+
+
+const openSkuDetail = (data)=>{
+    skuDetailRef.value.open(data)
+}
+
+const getQuotationList = async () =>{
+    if(!props.resourceTypeId) return
+    let param:{businessId?:number|string, orderFormId?:number|string, id?:number|string} = {
+        businessId: undefined,
+        orderFormId: undefined,
+        id: undefined
+    }
+    const regex = /business/i;  // 'i' 表示不区分大小写
+    const regexCustomer = /customer/i;  // 'i' 表示不区分大小写
+    if(props.perPrefix && regex.test(props.perPrefix)) {
+        param.businessId = props.resourceTypeId
+    } else  if(props.perPrefix && regexCustomer.test(props.perPrefix)) {
+        param.id = props.resourceTypeId
+    } else {
+        param.orderFormId = props.resourceTypeId
+
+    }
+
+    let data = await getDetail(param);
+    ssdSkuDetailVOList.value = data.result
+}
+
+const getProductPriceRef = ref()
+const editeLoading = ref(false)
+const edite = ()=>{
+    editeLoading.value = true
+    if(!ssdSkuDetailVOList || ssdSkuDetailVOList?.value.ssdSkuDetailVOList.length <= 0){
+        ElMessage.error('没有找到报价单,请先添加报价单')
+        editeLoading.value = false
+        return
+    }
+    let param:{businessId?:number|string, orderFormId?:number|string, id: number|string} = {
+        businessId: undefined,
+        orderFormId: undefined,
+        id: undefined
+    }
+    const regex = /business/i;  // 'i' 表示不区分大小写
+    const regexCustomer = /customer/i;  // 'i' 表示不区分大小写
+    if(props.perPrefix && regex.test(props.perPrefix)) {
+        param.businessId = props.resourceTypeId
+    } else  if(props.perPrefix && regexCustomer.test(props.perPrefix)) {
+        param.id = props.resourceTypeId
+    } else {
+        param.orderFormId = props.resourceTypeId
+
+    }
+
+    getProductPriceRef.value.edite(param, ()=>{
+        editeLoading.value = false
+    })
+}
+
+//随机生成
+const download = (url)=>{
+    downloadPlugins.oss(url)
+}
+
+const downPdf = async (type)=>{
+    let time = new Date().getTime()
+    let name = type === 1 ? `报价单-${time}.docx` : `Quotation-${time}.docx`
+    let param:{businessId?:number|string, orderFormId?:number|string, languageType: number, id:number|string} = {
+        businessId: undefined,
+        orderFormId: undefined,
+        languageType: undefined,
+        id: undefined
+    }
+    const regex = /business/i;  // 'i' 表示不区分大小写
+    const regexCustomer = /customer/i;  // 'i' 表示不区分大小写
+    if(props.perPrefix && regex.test(props.perPrefix)) {
+        param.businessId = props.resourceTypeId
+    } else  if(props.perPrefix && regexCustomer.test(props.perPrefix)) {
+        param.id = props.resourceTypeId
+    } else {
+        param.orderFormId = props.resourceTypeId
+
+    }
+    param.languageType = type
+    await downLodaQuotationById(param, name)
+}
+defineExpose({
+
+    getQuotationList
+})
+onMounted(()=>{
+    getQuotationList()
+    getRate()
+})
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 90 - 0
ui/sp-user-center/src/modules/sales/views/business/component/TurnBus.vue

@@ -0,0 +1,90 @@
+<!-- 添加协作 -->
+<template>
+    <el-dialog title="添加协作" v-model="visible" width="850px">
+        <el-form ref="formRef" :model="form" label-width="120px">
+            <el-form-item label="接收对象:" prop="newOwnerBy"  :rule="{required: true, message: '请选择接收对象', trigger: 'change'}">
+                <SelectUser v-model="form.newOwnerBy"></SelectUser>
+            </el-form-item>
+<!--            <el-form-item label="选择类型:" prop="typeList" >-->
+<!--                <el-checkbox-group v-model="form.typeList">-->
+<!--                    <el-checkbox value="1">订单</el-checkbox>-->
+<!--                    <el-checkbox value="2">费用</el-checkbox>-->
+<!--                    <el-checkbox value="3">发票</el-checkbox>-->
+<!--                </el-checkbox-group>-->
+<!--            </el-form-item>-->
+            <el-form-item label="备注:" >
+                <el-input type="textarea" v-model="form.remark"></el-input>
+            </el-form-item>
+            <p class="tips">*如需变更客户相关记录归属人员请勾选相关选项</p>
+
+        </el-form>
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button @click="cancel">取消</el-button>
+                <el-button :loading="buttonLoading" type="primary" @click="submitForm">提交</el-button>
+            </div>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup lang="ts">
+    // import SelectUser from '@/components/SelectOrg/SelectUser.vue';
+
+    import {transferBusiness} from '@/api/business/allBusiness'
+
+    const emit = defineEmits(['onOk'])
+
+    const visible = ref(false)
+    const formRef = ref()
+    const buttonLoading = ref(false)
+    const radio1 = ref('1')
+
+    const open = (businessIdList) => {
+        form.businessIdList = businessIdList
+        visible.value = true
+    }
+
+    const form = reactive({
+        businessIdList: [],
+        newOwnerBy: undefined,
+        remark: undefined,
+        typeList: []
+    })
+
+    const submitForm = async () => {
+        formRef.value.validate(async (valid: boolean) => {
+            if (valid) {
+               let res = await transferBusiness(form)
+                reset()
+                visible.value = false
+                emit('onOk', 'turnBussnese')
+            }
+        })
+
+    }
+
+    const cancel = () => {
+        visible.value = false
+    }
+/** 表单重置 */
+const reset = () => {
+    Object.assign(form, {
+        businessIdList: [],
+        newOwnerBy: undefined,
+        remark: undefined,
+        typeList: []
+    })
+    formRef.value?.resetFields();
+}
+
+    defineExpose({
+        open
+    })
+</script>
+
+<style lang="scss" scoped>
+.tips{
+    padding-left: 120px;
+    margin-bottom: 2px;
+}
+</style>

+ 403 - 0
ui/sp-user-center/src/modules/sales/views/business/component/add.vue

@@ -0,0 +1,403 @@
+<template>
+    <div class="add-area add-area-bus">
+        <div class="header flex-center">
+            <p>{{catchId ? '编辑商机' : '添加商机'}}</p>
+            <p>*为必填项</p>
+        </div>
+        <div class="content">
+            <el-form ref="allBusinessFormRef" :model="form" :rules="rules" class="flex" label-width="140px">
+                <div class="order-form-content">
+                    <el-row>
+                        <el-col :span="24">
+                            <el-form-item label="商机标题:" prop="title">
+                                <el-input v-model="form.title" placeholder="请输入商机标题" />
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                    <el-row>
+                        <el-col :span="24">
+                            <correlationSelect labelLiaison="联系人:" v-model="form.customerId"
+                                v-model:liaisonId="form.liaisonId" :selectedName="form.customerName"
+                                :selectedId="$route.query.customerId as string">
+                            </correlationSelect>
+                        </el-col>
+
+                    </el-row>
+
+                    <el-row>
+                        <el-col :span="12">
+                            <el-form-item label="销售阶段:" prop="stage">
+                                <SelectDict v-model="form.stage" placeholder="请选择销售阶段" dictCode="business_stage">
+                                </SelectDict>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="12">
+
+                            <el-form-item label="商机类型:" prop="businessTypeDictValue">
+                                <SelectDict v-model="form.businessTypeDictValue" placeholder="请选择商机类型"
+                                    dictCode="business_type"></SelectDict>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                    <el-row>
+                        <el-col :span="12">
+                            <el-form-item label="商机来源:" prop="sourcesOfBusinessDictValue">
+                                <SelectDict v-model="form.sourcesOfBusinessDictValue" placeholder="请选择商机来源"
+                                    dictCode="sources_of_business"></SelectDict>
+                            </el-form-item>
+
+                        </el-col>
+                        <el-col :span="12">
+                            <el-form-item label="成交机率:">
+                                <SelectDict :clearable='false' v-model="form.transactionProbabilityDictValue"
+                                    placeholder="请选择成交机率" dictCode="transaction_probability"></SelectDict>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                    <el-row>
+                        <el-col :span="12">
+
+                            <el-form-item label="预计金额:">
+
+                                <div class="flex flex-column" style="width: 100%">
+                                    <div class="flex" style="width: 100%">
+
+                                        <div style="width: 70%">
+                                            <el-input type="number" style="width: 100%"
+                                                v-model="form.initialSalesAmount" placeholder="请输入预计销售金额"> </el-input>
+                                        </div>
+                                        <div style="width: 30%">
+                                            <el-select style="width: 100%" v-model="form.currency" placeholder="请选择币种">
+                                                <el-option :value="1" label="CNY"></el-option>
+                                                <el-option :value="2" label="USD"></el-option>
+                                            </el-select>
+                                        </div>
+                                    </div>
+                                    <p style="width: 100%; " v-if="form.currency == 2">
+                                        当前汇率:{{ rate }},结算人民币:{{ $formatNumber(form.initialSalesAmount * rate) }}</p>
+                                    <!-- <p style="width: 100%; " v-if="form.currency == 1">当前汇率:{{rate}},折算美元:{{$formatNumber(form.initialSalesAmount / rate)}}</p> -->
+                                </div>
+
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="12">
+                            <el-form-item label="获取日期:" prop="getedDate">
+                                <el-date-picker clearable v-model="form.getedDate" type="date" value-format="YYYY-MM-DD"
+                                    :disabled-date="disabledFutureDates" placeholder="请选择获取日期">
+                                </el-date-picker>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                    <el-row>
+                        <el-col :span="12">
+                            <el-form-item label="归属人员:" prop="ownerBy">
+                                <SelectUser v-model.lazy="form.ownerBy"></SelectUser>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="12">
+                            <el-form-item label="预计签单:">
+                                <el-date-picker :disabled-date="disabledPastDates" clearable
+                                    v-model="form.expectedSigningDate" type="date" value-format="YYYY-MM-DD"
+                                    placeholder="请选择预计签单日期">
+                                </el-date-picker>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+
+                    <CreateQuickTask v-model="form.task"></CreateQuickTask>
+
+                </div>
+            </el-form>
+        </div>
+        <div class="footer flex">
+            <el-button :loading="buttonLoading" type="primary" @click="submitForm">提交</el-button>
+            <el-button @click="reset">重置</el-button>
+        </div>
+    </div>
+
+</template>
+
+<script setup name="businessAdd" lang="ts">
+import {
+    listAllBusiness,
+    delAllBusiness,
+    addAllBusiness,
+    updateAllBusiness, getById
+} from '@/api/business/allBusiness';
+import {getUSDtoCnyRate} from '@/api/product/index'
+import correlationSelect from '@/components/common/correlationSelect.vue'
+import addCollaborate from '@/components/common/addCollaborate.vue'
+import createTask from '@/components/common/createTask.vue'
+import { AllBusinessVO, AllBusinessQuery, AllBusinessForm } from '@/api/business/allBusiness/types';
+import auth from '@/plugins/auth'
+import Table from '@/components/Table/index.vue'
+import { tableTypes } from '@/components/Table/types'
+import { useDictCache } from '@/hooks/web/useDict'
+import CreateQuickTask from '@/components/common/createQuickTask.vue';
+import { listCustomer } from "@/api/customer";
+import { useUserStore } from "@/store/modules/user";
+import { listDetialByCodes } from "@/api/system/dict";
+import useSettingsStore from "../../../store/modules/settings";
+import useTagsViewStore from "@/store/modules/tagsView";
+const tagsViewStore = useTagsViewStore()
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const userStore = useUserStore()
+const route = useRoute();
+const router = useRouter();
+const { queryDictDetailByCodes } = useDictCache(['sales_phase',
+    'business_type', 'transaction_probability',
+    'sources_of_business',
+    'task_remind_type',
+    'task_remind_mode',
+    'task_mark_tag',
+    'task_priority_grade',
+    'business_stage'
+])
+queryDictDetailByCodes()
+const dictData: any = reactive({
+    remindModeData: [],
+})
+// listDetialByCodes(['task_remind_mode']).then((res => {
+//     if(res.success) {
+//         dictData.remindModeData = res.result['task_remind_mode']
+//     }
+// }))
+// const customerOption = ref([])
+
+
+
+const disabledFutureDates = (time) => {
+    return time.getTime() > Date.now();
+};
+const disabledPastDates = (time) => {
+    return time.getTime() < Date.now() - 86400000; // 86400000 毫秒 = 1 天
+};
+
+const allBusinessList = ref<AllBusinessVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const rate = ref(0);
+const showSearch = ref(true);
+
+
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const allBusinessFormRef = ref<ElFormInstance>();
+const catchId = ref()
+
+const initFormData: AllBusinessForm = {
+    id: undefined,
+    title: undefined,
+    customerId: proxy?.$route.query.customerId as string,
+    liaisonId: proxy?.$route.query.liaisonId as string,
+    estimatedSalesAmount: undefined,
+    expectedSigningDate: undefined,
+    salesPhaseDictValue: undefined,
+    businessTypeDictValue: undefined,
+    stage: undefined,
+    transactionProbabilityDictValue: '10',
+    sourcesOfBusinessDictValue: undefined,
+    getedDate: undefined,
+    remark: undefined,
+    isDelete: undefined,
+    enabled: undefined,
+    ownerBy: userStore.userId as number,
+    hasLoss: 0,
+    sort: undefined,
+    initialSalesAmount: 0,
+    currency: 2,
+    task: {
+        taskBeginTime: '',
+        taskContent: '',
+        isCreateTask: false,
+        remindModels: ['site'],
+        remindType: undefined,
+        ownerBy: userStore.userId
+    }
+}
+const validateOwnerBy = (rule: any, value: any, callback: any) => {
+    if (!value) {
+        callback(new Error("拥有者不能为空"));
+    } else if (typeof value !== 'string') {
+        callback(new Error("拥有者必须是字符串"));
+    } else {
+        callback(); // 验证通过
+    }
+};
+const data = reactive<PageData<AllBusinessForm, AllBusinessQuery>>({
+    form: { ...initFormData },
+    rules: {
+        id: [
+            { required: true, message: "商机主键不能为空", trigger: "blur" }
+        ],
+        title: [
+            { required: true, message: "商机标题不能为空", trigger: "blur" }
+        ],
+        customerId: [
+            { required: true, message: "关联客户不能为空", trigger: "change" }
+        ],
+        liaisonId: [
+            { required: true, message: "主要联系人不能为空", trigger: "change" }
+        ],
+        estimatedSalesAmount: [
+            { required: true, message: "预计销售金额不能为空", trigger: "blur" }
+        ],
+        expectedSigningDate: [
+            { required: true, message: "预计签单日期不能为空", trigger: "change" }
+        ],
+        salesPhaseDictValue: [
+            { required: true, message: "销售阶段不能为空", trigger: "change" }
+        ],
+        currency: [
+            { required: true, message: "币种不能为空", trigger: "change" }
+        ],
+        stage: [
+            { required: true, message: "销售阶段不能为空", trigger: "change" }
+        ],
+        businessTypeDictValue: [
+            { required: true, message: "商机类型不能为空", trigger: "change" }
+        ],
+        transactionProbabilityDictValue: [
+            { required: true, message: "成交机率不能为空", trigger: "change" }
+        ],
+        sourcesOfBusinessDictValue: [
+            { required: true, message: "商机来源不能为空", trigger: "change" }
+        ],
+        getedDate: [
+            { required: true, message: "获取日期不能为空", trigger: "blur" }
+        ],
+        remark: [
+            { required: true, message: "备注信息不能为空", trigger: "blur" }
+        ],
+
+        ownerBy: [
+            { required: true, message: "拥有者不能为空", trigger: ["change", "blur"] }
+        ],
+        'task.taskBeginTime': [
+            { required: true, message: "下次跟进时间不能为空", type: 'date', trigger: "change" }
+        ],
+        'task.ownerBy': [
+            { required: true, message: "任务跟进人不能为空", trigger: "change" }
+        ],
+        'task.remindType': [
+            { required: true, message: "任务提醒不能为空", trigger: "change" }
+        ],
+        'task.taskContent': [
+            { required: true, message: "任务跟进主题不能为空", trigger: "blur" }
+        ],
+        'task.objectiveValue': [
+            { required: true, message: "任务跟进目的不能为空", trigger: "change" }
+        ],
+
+    }
+});
+
+const { form, rules } = toRefs(data);
+
+/** 表单重置 */
+const reset = () => {
+    form.value = {...initFormData};
+    allBusinessFormRef.value?.resetFields();
+    catchId.value = ''
+}
+
+
+/** 提交按钮 */
+const submitForm = () => {
+    allBusinessFormRef.value?.validate(async (valid: boolean) => {
+        if (valid) {
+            buttonLoading.value = true;
+            let res = await addAllBusiness(form.value).finally(() => buttonLoading.value = false);
+            proxy?.$modal.msgSuccess("操作成功");
+            router.push(`/business_opportunity/detail?id=${res.result}&type=detail&from=allBusiness`)
+        }
+    });
+}
+// const getCustomerData = () => {
+//     listCustomer({pageIndex: 1, pageSize: 999999}).then(res => {
+//         if (res.success) {
+//             customerOption.value = res.result.records.map((item: { customerName: string, id: number }) => {
+//                 let obj = {label: item.customerName, value: item.id}
+//                 return obj
+//             })
+//         }
+//     })
+// }
+
+
+const getDetail = async ()=>{
+    let id: string = proxy?.$route?.query?.id as string
+
+    if(proxy?.$route.query.id ){
+        if(catchId.value == id) {
+            return
+        }
+        catchId.value = id
+        loading.value = true
+        tagsViewStore.editeTitle(route.path, '编辑商机')
+
+        await getById({id}).then(res => {
+            if (res.success) {
+                form.value = res.result
+                form.value.task= {
+                    taskBeginTime: '',
+                        taskContent: '',
+                        remindModels: [],
+                        remindType: undefined,
+                        ownerBy: undefined
+                }
+            }
+        }).finally(() => loading.value = false)
+    } else {
+        if(catchId.value) {
+            reset()
+        }
+        catchId.value = undefined
+        tagsViewStore.editeTitle(route.path, '添加商机')
+    }
+
+}
+const getRate =  ()=>{
+    getUSDtoCnyRate().then(res=>{
+        rate.value = res.result
+    })
+
+}
+onActivated(()=>{
+    getDetail()
+    // getCustomerData()
+})
+
+onMounted(() => {
+    getDetail()
+    getRate()
+    // getCustomerData()
+});
+</script>
+
+<style scoped lang="scss">
+.add-area-bus .content .el-form{
+    .el-row{
+        width: 900px;
+    }
+    .el-input, :deep(.el-select){
+        flex: 1;
+    }
+}
+.form-item{
+    :deep(.el-form-item__content){
+        flex-direction: column;
+        align-items: flex-start;
+    }
+}
+.unit{
+    border: 1px solid #dcdfe6;
+    width: 52px;
+    text-align: center;
+    border-radius: 2px;
+    margin-left: -2px;
+}
+</style>

+ 540 - 0
ui/sp-user-center/src/modules/sales/views/business/component/detail.vue

@@ -0,0 +1,540 @@
+<template>
+    <div class="detail_area" v-loading="loadDetail">
+        <div class="detail_header">
+            <div class="detail_company flex">
+                <div class="flex-center">
+                    <div>
+                        <div class="company_name">{{ detailData?.title}}</div>
+                        <div class="company_user">
+                            <span>关联客户:{{ detailData?.customerName }}</span>
+                            <span>主要联系人:{{ detailData?.liaisonName }}</span>
+                            <span>下次跟进:{{ detailData?.nextFollowUpTime }}</span>
+                        </div>
+                    </div>
+                </div>
+                <div class="header_right flex">
+                    <div class="left" :class="detailData?.previousId ? '' : 'btn-grey'">
+                        <el-icon @click="goPreNexDetail(detailData?.previousId)">
+                            <ArrowLeft />
+                        </el-icon>
+                    </div>
+                    <div class="right" :class="detailData?.nextId ? '' : 'btn-grey'">
+                        <el-icon @click="goPreNexDetail(detailData?.nextId)">
+
+                            <ArrowRight />
+                        </el-icon>
+                    </div>
+                </div>
+            </div>
+            <div style="display: flex;justify-content: space-between">
+                <div>
+                    <el-dropdown @command="handleCommand"
+                        :disabled="hasOrderForm || detailData?.quotationId || detailData?.hasLoss"
+                        style="margin-right: 15px">
+                        <el-button type="primary" :disabled="hasOrderForm">
+                            {{ detailData?.stageName }}<el-icon class="el-icon--right"><arrow-down /></el-icon>
+                        </el-button>
+                        <template #dropdown>
+                            <el-dropdown-menu>
+                                <el-dropdown-item v-for="item in business_stage" :key="item.value" :command="item.value"
+                                    :disabled="item.label == '成交商机' || item.label == '产品报价'">
+                                    <span> {{ item.label }}</span>
+                                    <el-icon v-if="detailData?.stage == item.label" style='margin-left: 10px'><Select
+                                            color="#1874ff" /></el-icon>
+                                </el-dropdown-item>
+                            </el-dropdown-menu>
+                        </template>
+                    </el-dropdown>
+
+                    <el-dropdown style="margin-right: 12px;" @command="openDialog">
+                        <el-button type="primary">
+                            <el-icon>
+                                <Plus />
+                            </el-icon>添加<el-icon class="el-icon--right"><arrow-down /></el-icon>
+                        </el-button>
+                        <template #dropdown>
+                            <el-dropdown-menu>
+                                <el-dropdown-item v-if="auth.hasPermi(`business:business:detail_add`)"
+                                    command="productPrice">产品报价</el-dropdown-item>
+                                <el-dropdown-item v-if="auth.hasPermi(`business:business:detail_lisaon`)"
+                                    command="addConcat">联系人</el-dropdown-item>
+                                <!--                                <el-dropdown-item v-if="auth.hasPermi(`business:business:detail_fee`)"-->
+                                <!--                                    command="addFee">费用</el-dropdown-item>-->
+                            </el-dropdown-menu>
+                        </template>
+                    </el-dropdown>
+                    <template v-for="item in operateBtn">
+                        <el-button :key="item.value" v-if="item.show" @click="openDialog(item.value)"
+                            :disabled="item.disabled">{{ item.label }}</el-button>
+                    </template>
+
+                </div>
+                <div>
+                    <el-button :disabled="detailData?.stage == 4" v-hasPermi="[`business:detail:sjlshhx`]"
+                        :type="detailData?.hasLoss ? 'success' : 'warning'" @click="updateLossStatus">{{
+                        detailData?.hasLoss ? '唤醒商机' : '商机流失' }}</el-button>
+                    <el-button v-hasPermi="[`business:detail:bjsj`]" @click="handleUpdate">编辑商机</el-button>
+                    <el-button v-hasPermi="[`business:detail:scsj`]" @click="handleDelete">删除商机</el-button>
+                </div>
+
+            </div>
+        </div>
+
+        <div class="step" style="">
+            <el-steps :active="detailData?.stage" finish-status="success" simple>
+                <el-step v-for="(item, index) in business_stage" :key='index' :title="item.label" />
+            </el-steps>
+        </div>
+        <div class="detail_main">
+            <InfoMainNav :navList="navList" @change="changeTab" ref="informationRef"></InfoMainNav>
+            <!-- <transition name="fade" mode="out-in">
+                <section> -->
+            <Information perPrefix="business" :detailData="detailData" v-if="navValue == 'infomation'">
+            </Information>
+            <Liaison perPrefix="business" v-if="navValue == 'liaison'" resourceType="30"
+                :resourceTypeId="detailData?.id" :customerId="detailData?.customerId">
+            </Liaison>
+            <FollowUp perPrefix="business" v-if="navValue == 'follow'" resourceType="30"
+                :resourceTypeId="detailData?.id"></FollowUp>
+            <Task :isCorrelation="false" perPrefix="business" v-if="navValue == 'task'" resourceType="30"
+                :resourceTypeId="detailData?.id"></Task>
+            <sendRecord :customerId="detailData?.customerId" perPrefix="business" v-if="navValue == 'sendRecord'"
+                resourceType="30" :resourceTypeId="detailData?.id" :customerName="detailData.customerName"
+                :resourceName="detailData.title"></sendRecord>
+            <resourceList perPrefix="business" v-if="navValue == 'resourceList'" resourceType="30"
+                :resourceTypeId="detailData?.id" :customerId="detailData?.customerId"></resourceList>
+            <operationLog perPrefix="business" v-if="navValue == 'operationLog'" resourceType="30"
+                :resourceTypeId="detailData?.id"></operationLog>
+            <orderForm perPrefix="business" v-if="navValue == 'order'" :businessId="detailData?.id"></orderForm>
+            <QuotationList perPrefix="business" ref="quotationListRef" v-if="navValue == 'quotation'" resourceType="30"
+                :resourceTypeId="detailData?.id"></QuotationList>
+            <CostList perPrefix="business" v-if="navValue == 'cost'" resourceType="30"
+                :customerId="detailData?.customerId" :resourceTypeId="detailData?.id"></CostList>
+            <!-- </section>
+            </transition> -->
+        </div>
+
+    </div>
+    <el-dialog :title="detailData?.hasLoss ? '唤醒商机' : '流失商机'" v-model="lostVisible" width="600px">
+        <el-form ref="lossFormRef" :model="lostForm" :rules="lossRules" label-width="120px">
+            <el-form-item v-if="!detailData?.hasLoss" label="流失原因:" prop="lossReasonDictValue">
+                <SelectDict v-model="lostForm.lossReasonDictValue" placeholder="请选择流失原因" dictCode="loss_reason">
+                </SelectDict>
+            </el-form-item>
+            <el-form-item v-else label="唤醒原因:" prop="awakenReasonDictValue">
+                <SelectDict v-model="lostForm.awakenReasonDictValue" placeholder="请选择唤醒原因" dictCode="awaken_reason">
+                </SelectDict>
+            </el-form-item>
+            <el-form-item v-if="detailData?.hasLoss" label="备注:" prop="awakenRemark">
+                <el-input v-model="lostForm.awakenRemark" type="textarea" :rows="4" placeholder="请输入内容" />
+            </el-form-item>
+            <el-form-item v-else label="备注:" prop="lossRemark">
+                <el-input v-model="lostForm.lossRemark" type="textarea" :rows="4" placeholder="请输入内容" />
+            </el-form-item>
+        </el-form>
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button @click="lossCancel">取消</el-button>
+                <el-button :loading="lossButtonLoading" type="primary" @click="submitFormLoss">提交</el-button>
+            </div>
+        </template>
+    </el-dialog>
+    <GetProductPrice ref="getProductPriceRef" @ok="refreshPriceData"></GetProductPrice>
+    <!-- 按钮弹窗 -->
+    <component v-bind="{ isCorrelation: false }" :is="dialogComponent" @onOk="onOk" @refresh="onOk"
+        :ref="($event: any) => setItemRef($event)">
+    </component>
+
+</template>
+
+<script setup lang="ts">
+
+import Follow from '@/components/Follow/index.vue'
+import CreateTask from '@/components/common/createTask.vue'
+import AddFollow from '@/components/Follow/add.vue'
+import InfoMainNav from '@/components/common/InfoMainNav.vue'
+import transferClue from '@/components/common/transferClue.vue'
+import ConvertCustomer from '@/components/common/convertCustomer.vue'
+import Addcontact from './Addcontact.vue'
+import addCollaborate from '@/components/common/addCollaborate.vue'
+import turnBussnese from './TurnBus.vue'
+import GetProductPrice from './GetProductPrice.vue'
+import Information from './Information.vue'
+import CostList from './CostList.vue'
+import FollowUp from '@/components/detailCommon/followUp.vue'
+import Liaison from '@/components/detailCommon/liaison.vue'
+import sendRecord from '@/components/detailCommon/sendRecord.vue'
+import resourceList from '@/components/detailCommon/resourceList.vue'
+import operationLog from '@/components/detailCommon/operationLog.vue'
+import QuotationList from './QuotationList.vue'
+import orderForm from '@/components/detailCommon/orderForm.vue'
+import {
+    addAllBusiness,
+    changeStage,
+    delAllBusiness,
+    getById,
+    getPreNexById,
+    lostBusiness
+} from '@/api/business/allBusiness';
+import {AllBusinessForm, AllBusinessVO, LostBusinessDTO} from '@/api/business/allBusiness/types';
+import {useDictCache} from '@/hooks/web/useDict'
+import {useDictStore} from "@/store/modules/dict";
+import {storeToRefs} from "pinia";
+import {getCustomer} from "@/api/customer";
+import {onMounted} from "vue";
+import {deleteComment} from "@/api/public";
+import auth from '@/plugins/auth'
+import {liaisonResourceAdd} from "@/api/customer/liaison";
+import useAppStore from '@/store/modules/app';
+import useSettingsStore from '@/store/modules/settings'
+const useSetStore = useSettingsStore();
+const appStore = useAppStore();
+const toggleSideBar = () => {
+    appStore.closeSideBar(false);
+}
+
+const router = useRouter();
+let queryData: any = router.currentRoute.value.query
+
+const {proxy} = getCurrentInstance() as ComponentInternalInstance;
+const {queryDictDetailByCodes} = useDictCache(['business_stage','sales_phase','loss_reason','awaken_reason', 'business_type', 'transaction_probability', 'sources_of_business', 'task_remind_type', 'task_remind_mode'])
+queryDictDetailByCodes()
+const useDict = useDictStore()
+
+const stepIndex = computed(()=>{
+    let data = useDict.getDict('business_stage')
+    if(!data || data.length == 0)return
+    return data.findIndex(i=>i.value == detailData?.value?.salesPhaseDictValue)
+
+})
+
+const salesPhase = computed(() => {
+    return useDict.getDict('sales_phase')
+})
+const business_stage = computed(() => {
+    return useDict.getDict('business_stage')
+})
+
+const getProductPriceRef = ref()
+const perPrefix = ref(queryData.from)
+const quotationListRef = ref()
+
+const navList = [
+    {label: '概况信息', value: 'infomation', show: auth.hasPermi('business:detail:tab_gkxx')},
+    {label: '联系人', value: 'liaison' , show: auth.hasPermi('business:detail:tab_lxr')},
+    {label: '跟进记录', value: 'follow' , show: auth.hasPermi('business:detail:tab_gjjl')},
+    {label: '任务记录', value: 'task' , show: auth.hasPermi('business:detail:tab_rwjl')},
+    {label: '关联订单', value: 'order' , show: auth.hasPermi('business:detail:tab_gldd')},
+    {label: '产品报价', value: 'quotation', show: auth.hasPermi('business:detail:tab_cpbj') },
+    {label: '寄样记录', value: 'sendRecord' , show: auth.hasPermi('business:detail:tab_jyjl')},
+    // {label: '费用记录', value: 'cost' , show: auth.hasPermi('business:detail:tab_fyjl')},
+    {label: '相关附件', value: 'resourceList' , show: auth.hasPermi('business:detail:tab_xgfj')},
+    {label: '操作日志', value: 'operationLog' , show: auth.hasPermi('business:detail:tab_czrz')}
+];
+
+const navValue = ref<string>('infomation')
+const changeTab = (value: string) => {
+    navValue.value = value
+}
+
+const navIndex = ref<number>(0)
+const onlyFollow = ref<Boolean>(false)
+const loadDetail = ref<boolean>(false)
+
+
+// 获取线索详情数据
+const detailData = ref<AllBusinessVO| null>(null)
+
+
+//更改销售阶段
+const handleCommand = async (command: string) => {
+    let param:AllBusinessForm = {
+        id: detailData?.value?.id,
+        stage: command
+    }
+    await changeStage(param).finally(() => {
+        getDetailData()
+    });
+
+}
+//流失商机
+const lostVisible = ref(false)
+const lossButtonLoading = ref(false)
+const lossCancel = () => {
+    lostVisible.value = false
+    resetLoss()
+}
+const updateLossStatus = () => {
+    lostVisible.value = true
+}
+const lostForm = reactive<LostBusinessDTO>({
+    id:  queryData.id,
+    lossReasonDictValue: '',
+    awakenReasonDictValue: '',
+    hasLoss: undefined
+})
+const lossRules = reactive({
+    lossReasonDictValue:[{required: true, message: '请选择流失原因', trigger: 'change'}],
+    awakenReasonDictValue:[{required: true, message: '请选择唤醒原因', trigger: 'change'}],
+    awakenRemark:[{required: true, message: '请输入唤醒备注', trigger: 'blur'}],
+    lossRemark:[{required: true, message: '请输入流失备注', trigger: 'blur'}],
+})
+
+/** 表单重置 */
+const resetLoss = () => {
+    lossButtonLoading.value = false
+    Object.assign(lostForm, {
+        id:  queryData.id,
+        lossReasonDictValue: '',
+        awakenReasonDictValue: '',
+        hasLoss: ''
+    })
+    lossFormRef.value?.resetFields();
+}
+const lossFormRef = ref()
+const submitFormLoss = async () => {
+    lossButtonLoading.value = true
+
+    lossFormRef.value?.validate(async (valid: boolean) => {
+        if (valid) {
+            lostForm.hasLoss = detailData.value?.hasLoss == 1 ? 0 : 1
+            let res = await lostBusiness(lostForm)
+            resetLoss()
+            getDetailData()
+            lostVisible.value = false
+        }
+
+        lossButtonLoading.value = false
+    })
+
+}
+
+const getDetailData = () => {
+    loadDetail.value = true
+    let param: any = {
+        id: queryData.id,
+        preNextScopeMenuId: queryData.scopMenuId
+    }
+    getById(param).then(res => {
+        if (res.success) {
+            detailData.value = res.result || {} as AllBusinessVO;
+        }
+        loadDetail.value = false
+
+        useSetStore.setTitle(detailData.value.title)
+    })
+}
+const init = async ()=>{
+    getDetailData()
+}
+
+const route = useRoute();
+watch(route, (newRoute, oldRoute) => {
+    let queryDataNew: any = router.currentRoute.value.query
+    if (queryDataNew.id) {
+        queryData.id = queryDataNew.id
+        init()
+    }
+});
+
+
+const informationRef = ref()
+const goPreNexDetail = (id:string)=>{
+    if (!id) return
+    navValue.value = 'infomation'
+    informationRef.value.resetNavList()
+    setTimeout(() => {
+        router.push(`./detail?id=${id}&type=detail&scopMenuId=${queryData.scopMenuId}`)
+    }, 100)
+
+}
+const hasOrderForm = computed(() => {
+    return detailData.value?.hasOrderForm || detailData.value?.hasLoss == 1 || detailData.value?.stage == 4
+})
+const hasLossForm = computed(() => {
+    return !detailData.value?.hasLoss
+})
+
+
+// 操作按钮弹窗
+let operateBtn = ref([
+    // { label: '初步洽谈', value: 'intention' },
+    { label: '写新跟进', value: 'AddFollow', show: auth.hasPermi('business:detail:xxgj'), disabled: false},
+    { label: '新建任务', value: 'task', show: auth.hasPermi('business:detail:xjrw'), disabled: false },
+    { label: '转为订单', value: 'transforOrder', show: auth.hasPermi('business:detail:zwdd'), disabled: hasOrderForm  },
+    { label: '转移商机', value: 'turnBussnese', show: auth.hasPermi('business:detail:zysj'), disabled: false },
+    { label: '添加协作', value: 'addCollaborate', show: auth.hasPermi('business:detail:tjxz'), disabled: false },
+])
+const dialogComponent = shallowRef()
+const dialogRef = ref()
+const setItemRef = (el: any) => {
+    if (el) {
+        dialogRef.value = el
+    }
+}
+
+/** 修改按钮操作 */
+const handleUpdate = async () => {
+    router.push('./addbus?id=' + detailData.value?.id)
+}
+
+/** 删除操作 */
+const handleDelete = async () => {
+    if(!!!detailData.value) return
+
+    ElMessageBox.confirm(
+        '是否确认删除当前商机吗?',
+        '确认提示',
+        { type: 'warning', confirmButtonText: '提交' }
+    ).then(() => {
+        delAllBusiness(detailData?.value?.id).then(res => {
+            if(res.success) {
+                ElMessage.success('删除成功')
+                router.push('./allBusiness')
+            }else{
+                ElMessage.error('删除失败')
+            }
+        })
+    }).catch(() => {})
+}
+
+const refreshPriceData = () => {
+    quotationListRef.value.getQuotationList()
+}
+
+const onOk= (type: string) => {
+    if( 'detailTurnBussnese' == type) {
+        router.push(`/business_opportunity/${queryData.from}`)
+        return
+    }
+
+   if(type == 'addConcat' || 'addCollaborate' == type  ) {
+       getDetailData()
+   }
+}
+
+const openDialog = (val: string) => {
+
+    switch (val) {
+        case 'addConcat': // 转为客户
+            dialogComponent.value = Addcontact
+            break;
+        case 'AddFollow': // 写新跟进
+
+            dialogComponent.value = AddFollow
+            break;
+        case 'task': // 新建任务
+            dialogComponent.value = CreateTask
+            break;
+        case 'addFee': // 新建任务
+            // dialogComponent.value = CreateTask
+            break;
+        case 'addCollaborate': // 添加协作
+            dialogComponent.value = addCollaborate
+            break;
+        case 'turnBussnese': // 转移商机
+
+            dialogComponent.value = transferClue
+            break;
+        case 'editClue': // 编辑商机
+            router.push('./add?type=eite')
+            break;
+        case 'transforOrder': // 编辑线索
+            router.push('/order/add?type=add&businessId=' + detailData.value?.id + '&customerId=' + detailData.value.customerId)
+            break;
+        case 'productPrice': // 编辑线索
+            getProductPriceRef.value.open(detailData.value?.id, detailData.value.customerId)
+            break;
+        case 'delClue': // 删除线索
+            ElMessageBox.confirm(
+                '是否确认删除当前数据?',
+                '确认提示',
+                { type: 'warning', confirmButtonText: '提交' }
+            ).then(() => {
+                // delCustomer(id).then((res: AxiosResponse) => {
+                //     if(res.success) {
+                //         ElMessage.success('删除成功')
+                //         getData()
+                //     }else{
+                //         ElMessage.error('删除失败')
+                //     }
+                // })
+            }).catch(() => { })
+            break;
+    }
+    if(val == 'addFee' || val == 'convert') {
+        proxy?.$modal.msgWarning("功能暂未开放!敬请期待");
+        return
+    }
+    if (['AddFollow', 'task', 'addConcat', 'addCollaborate', 'turnBussnese'].includes(val)) {
+        nextTick(() => {
+            if(val == 'addCollaborate'){
+                dialogRef.value.open({
+                    userIdList: detailData.value.collaborator ? detailData.value.collaborator.split(',') : [],
+                    resourceType:30,
+                    resourceIdList:[detailData?.value?.id]
+                })
+            }
+            if(val=='turnBussnese') {
+                dialogRef.value.open({
+                    resourceType:30,
+                    typeName: '商机',
+                    idList:[detailData?.value?.id],
+                    tag: 'detailTurnBussnese'
+                })
+                return
+            }
+            if(val == 'addConcat') {
+                dialogRef.value.open(detailData?.value?.customerId, detailData?.value?.id, detailData?.value?.title)
+                return
+            }
+            if(val == 'task') {
+
+                dialogRef.value.open({
+                    type: 'add',
+                    obj: {
+                        resourceType: '30',
+                        resourceTypeId: detailData?.value?.id
+                    }
+
+                })
+                return
+            }
+            if(val == 'AddFollow') {
+                let param = {
+                    type: 'add',
+                    customerId: detailData?.value?.customerId,
+                    customerName: detailData?.value?.customerName,
+                    resourceType: 30,
+                    resourceTypeId: detailData?.value?.id,
+                    resourceName: detailData?.value?.title,
+                    resourceStageName: detailData?.value?.stageName,
+                    resourceStageDictValue: detailData?.value?.stage
+                }
+                dialogRef.value.open(param)
+                return
+            }
+            // dialogRef.value.open()
+        })
+    }
+
+}
+onMounted(()=>{
+    init()
+    toggleSideBar()
+
+})
+</script>
+
+<style lang="scss" scoped>
+.btn-grey{
+    cursor: no-drop;
+    opacity: 0.5;
+}
+.step{
+    width: 100%;
+    padding:15px 20px;
+    background: #fff;
+    margin-top: 15px;
+}
+</style>

+ 187 - 0
ui/sp-user-center/src/modules/sales/views/business/component/skuDetail.vue

@@ -0,0 +1,187 @@
+<template>
+    <el-dialog title="sku报价详情" v-model="visible" width="700px" append-to-body>
+        <el-row>
+            <el-col :span="24" style="border-right: 1px solid #e5e6e7; padding-left: 5px">
+                <el-descriptions column="1" label-width="120" border>
+                    <el-descriptions-item label="内部编号:">{{ detail.ssdSkuCode }}</el-descriptions-item>
+                    <el-descriptions-item label="规格:">
+                        <!--                        {{ detail.name }}-->
+                        <template v-if="detail.ssdSkuCode != '外部产品' && detail.ssdSkuCode != '其他费用'">
+                            <span class="ss">{{ detail.connectorType }}<small>|</small>{{ detail.productSize
+                                }}<small>|</small>{{ detail.connectorProtocol }}<small>|</small>{{ detail.coreChipType
+                                }}<small>|</small>{{ detail.productLevel }}<small>|</small>{{ detail.flashChipType
+                                }}<small>|</small>{{ detail.capacity }}</span>
+                        </template>
+                        <template v-else>
+                            <span>
+                                {{ detail.specificationRemarks }}
+                            </span>
+                        </template>
+                    </el-descriptions-item>
+                    <el-descriptions-item label="配置功能:">
+                        <template v-if="detail.ssdSkuCode != '外部产品' && detail.ssdSkuCode != '其他费用'">
+                            <label style="color:#999999;">逻辑销毁:</label><span style="color: #555555;">{{
+                                detail.forceOpenLogicDestroy ? '开通' : '不开通' }} </span><br>
+                            <label style="color:#999999;">物理销毁:</label><span style="color: #555555;">{{
+                                detail.forceOpenPhysicalDestroy ? '开通' : '不开通' }}</span><br>
+                            <label style="color:#999999;">AES加密:</label><span style="color: #555555;">{{
+                                detail.forceOpenAesEncrypt ? '开通' : '不开通' }}</span><br>
+                            <label style="color:#999999;">r-Backup:</label><span style="color: #555555;">{{
+                                detail.forceOpenRapidBackup ? '开通' : '不开通' }}</span>
+                        </template>
+                        <template v-else>
+                            <span>
+                                {{ detail.functionRemarks }}
+                            </span>
+                        </template>
+                    </el-descriptions-item>
+                    <el-descriptions-item label="数量:">{{ detail.productCount }}
+                    </el-descriptions-item>
+                    <el-descriptions-item label="国内不含税价格(¥):">
+                        <!--                        <span>总价(¥):{{ $formatNumber(detail.skuPriceCNYWithNoRate)}}</span><br>-->
+                        <!--                        <span>单价(¥):{{  $formatNumber(detail.unitPriceCNYWithNoRate)  }}</span>-->
+                        <template v-if="detail.ssdSkuCode != '外部产品' && detail.ssdSkuCode != '其他费用'">
+                            <p style="font-weight: 600">标准价格:</p>
+                            <p>总价:{{ $formatNumber(detail.skuPriceCNYWithNoRate) }}</p>
+                            <p>单价:{{ $formatNumber(detail.unitPriceCNYWithNoRate) }}</p>
+                            <p style="font-weight: 600"> 报价金额:</p>
+                            <p>总价:{{ $formatNumber(detail.skuPriceCNYWithNoRateActual) }}</p>
+                            <p>单价:{{ $formatNumber(detail.unitPriceCNYWithNoRateActual) }}</p>
+                        </template>
+                        <template v-else>
+                            <p style="font-weight: 600"> 报价金额:</p>
+                            <p>总价:{{ $formatNumber(detail.skuPriceCNYWithNoRateActual) }}</p>
+                            <p>单价:{{ $formatNumber(detail.unitPriceCNYWithNoRateActual) }}</p>
+                        </template>
+                    </el-descriptions-item>
+                    <el-descriptions-item label="国内含13%增值税价格(¥):">
+                        <!--                        <span>总价(¥):{{ $formatNumber(detail.skuPriceCNY) }}</span><br>-->
+                        <!--                        <span>单价(¥):{{ $formatNumber(detail.unitPriceCNY) }}</span>-->
+                        <template v-if="detail.ssdSkuCode != '外部产品' && detail.ssdSkuCode != '其他费用'">
+                            <p style="font-weight: 600">标准价格:</p>
+                            <p>总价:{{ $formatNumber(detail.skuPriceCNY) }}</p>
+                            <p>单价:{{ $formatNumber(detail.unitPriceCNY) }}</p>
+                            <p style="font-weight: 600">报价金额:</p>
+                            <p>总价:{{ $formatNumber(detail.skuPriceCNYActual) }}</p>
+                            <p>单价:{{ $formatNumber(detail.unitPriceCNYActual) }}</p>
+                        </template>
+                        <template v-else>
+                            <p style="font-weight: 600"> 报价金额:</p>
+                            <p>总价:{{ $formatNumber(detail.skuPriceCNYActual) }}</p>
+                            <p>单价:{{ $formatNumber(detail.unitPriceCNYActual) }}</p>
+                        </template>
+                    </el-descriptions-item>
+
+                    <el-descriptions-item :label="`国外价格($)(不含税,美元汇率${USDToCNYRate ? USDToCNYRate.toFixed(4) : ''}):`">
+                        <!--                        <span>总价($):{{ $formatNumber(detail.skuPriceUSD) }}</span><br>-->
+                        <!--                        <span>单价($):{{ $formatNumber(detail.unitPriceUSD) }}</span>-->
+                        <template v-if="detail.ssdSkuCode != '外部产品' && detail.ssdSkuCode != '其他费用'">
+                            <p style="font-weight: 600">标准价格:</p>
+                            <p>总价:{{ $formatNumber(detail.skuPriceUSD) }}</p>
+                            <p>单价:{{ $formatNumber(detail.unitPriceUSD) }}</p>
+                            <p style="font-weight: 600">报价金额:</p>
+                            <p>总价:{{ $formatNumber(detail.skuPriceUSDActual) }}</p>
+                            <p>单价:{{ $formatNumber(detail.unitPriceUSDActual) }}</p>
+                        </template>
+                        <template v-else>
+                            <p style="font-weight: 600"> 报价金额:</p>
+                            <p>总价:{{ $formatNumber(detail.skuPriceUSDActual) }}</p>
+                            <p>单价:{{ $formatNumber(detail.unitPriceUSDActual) }}</p>
+                        </template>
+                    </el-descriptions-item>
+                    <el-descriptions-item label="预计生产天数:">
+                        <span>{{ detail.deliveryDays ? detail.deliveryDays : '--' }}</span>
+                    </el-descriptions-item>
+
+                </el-descriptions>
+            </el-col>
+        </el-row>
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button @click="downPdf(1)">下载中文报价单</el-button>
+                <el-button type="primary" @click="downPdf(2)">下载英文报价单</el-button>
+                <el-button v-if="detail.ssdSkuCode != '外部产品' && detail.ssdSkuCode != '其他费用'" type="primary" block
+                    @click="downPdf(3)">下载规格书</el-button>
+            </div>
+        </template>
+    </el-dialog>
+
+</template>
+
+<script setup lang="ts">
+import {
+    downLodaQuotation,
+    downLodaQuotationById, getUSDtoCnyRate,
+    listProductType,
+    querySkuVoList,
+    ssdStep1GetSku
+} from '@/api/product/index'
+import downloadPlugins from '@/plugins/download'
+import { ProductTypeVo, SkuVo, SkuQueryVo, ProductTypeQuery, MakeOrderSkuDetailVO } from '@/api/product/types'
+const visible = ref(false)
+const deptCheckStrictly = ref(false)
+const deptOptions = ref([])
+const skuData = ref([])
+
+const productTypeListVo = ref<Array<ProductTypeVo>>()
+const skuVoList = ref<Array<SkuVo>>()
+
+const detail = ref<MakeOrderSkuDetailVO>()
+
+const open = (data) => {
+    console.log(data)
+    detail.value = data
+    visible.value = true
+    getRate()
+}
+const USDToCNYRate = ref(0)
+const getRate = () => {
+    getUSDtoCnyRate().then(res => {
+        USDToCNYRate.value = res.result
+    })
+
+}
+
+const getName = (url) => {
+    return url ? url.substring(url.lastIndexOf('/')) : '';
+}
+const downPdf = async (type) => {
+    if (type == 3) {
+        let url = detail.value.datasheetUrl
+        let a = document.createElement("a");
+        a.download = decodeURI(getName(url));
+        a.style.display = "none";
+        a.target = '_blank'
+        a.href = url;
+        document.body.appendChild(a);
+        a.click();
+        document.body.removeChild(a);
+        return
+    }
+    if (detail.value.productCount <= 0) {
+        ElMessage.error('请添加产品')
+        return
+    }
+
+    let param = {
+        skuDetailId: detail.value.id,
+        languageType: type
+    }
+    let time = new Date().getTime()
+    let name = type === 1 ? `报价单-${time}.docx` : `Quotation-${time}.docx`
+    await downLodaQuotationById(param, name)
+}
+
+
+defineExpose({
+    open
+})
+const downPdfCn = ()=>{
+
+
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 10 - 0
ui/sp-user-center/src/modules/sales/views/business/lossBusiness/index.vue

@@ -0,0 +1,10 @@
+<template>
+    <CommonBusiness
+        perPrefix="lossBusiness"
+        isLoss
+    ></CommonBusiness>
+</template>
+
+<script setup name="successBusiness" lang="ts">
+import CommonBusiness from '@/modules/sales/views/business/component/CommonBusiness.vue'
+</script>

+ 43 - 0
ui/sp-user-center/src/modules/sales/views/business/lossBusinessMenu.sql

@@ -0,0 +1,43 @@
+
+delete from menu where id=77 or pid = 77;
+delete from role_menu where menu_id in (77, 78, 79, 80, 81,82,83);
+-- 菜单 new SQL
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES ( 77,'流失商机', DEFAULT, '69', null, 'lossBusiness', 'business/lossBusiness/index', 'lossBusiness', 'list-check', 1, 'business:lossBusiness:list', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 9);
+
+-- 按钮 new SQL
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES (79, '成交商机查询', DEFAULT, 77, null, '', '', '', null, 3, 'business:lossBusiness:query', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+
+
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES (80, '成交商机新增', DEFAULT, 77, null, '', '', '', null, 3, 'business:lossBusiness:add', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES (81, '成交商机修改', DEFAULT, 77, null, '', '', '', null, 3, 'business:lossBusiness:edit', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+
+
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES (82, '成交商机删除', DEFAULT, 77, null, '', '', '', null, 3, 'business:lossBusiness:remove', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+
+
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES (83, '成交商机导出', DEFAULT, 77, null, '', '', '', null, 3, 'business:lossBusiness:export', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+
+-- business:${props.perPrefix}:zysjlist
+
+
+
+
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 77, 1, sysdate(), null, 0);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 79, 1, sysdate(), null, 0);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 80, 1, sysdate(), null, 0);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 81, 1, sysdate(), null, 0);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 82, 1, sysdate(), null, 0);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 83, 1, sysdate(), null, 0);

+ 10 - 0
ui/sp-user-center/src/modules/sales/views/business/mineBusiness/index.vue

@@ -0,0 +1,10 @@
+<template>
+    <CommonBusiness
+        perPrefix="mineBusiness"
+        :queryRangeType="10"
+    ></CommonBusiness>
+</template>
+
+<script setup name="mineBusiness" lang="ts">
+import CommonBusiness from '@/modules/sales/views/business/component/CommonBusiness.vue'
+</script>

+ 0 - 0
ui/sp-user-center/src/modules/sales/views/business/mineBusinessDetailMenu.sql


+ 32 - 0
ui/sp-user-center/src/modules/sales/views/business/mineBusinessMenu.sql

@@ -0,0 +1,32 @@
+
+
+-- 菜单 new SQL
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES (164953884266498, '我的商机', DEFAULT, '69', null, 'mineBusiness', 'business/mineBusiness/index', 'mineBusiness', null, 1, 'business:mineBusiness:list', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+
+-- 按钮 new SQL
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES (164953884266499, '我的商机查询', DEFAULT, 164953884266498, null, '', '', '', null, 3, 'business:mineBusiness:query', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES (164953884266500, '我的商机新增', DEFAULT, 164953884266498, null, '', '', '', null, 3, 'business:mineBusiness:add', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES (164953884266501, '我的商机修改', DEFAULT, 164953884266498, null, '', '', '', null, 3, 'business:mineBusiness:edit', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES (164953884266502, '我的商机删除', DEFAULT, 164953884266498, null, '', '', '', null, 3, 'business:mineBusiness:remove', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+INSERT INTO menu (id, menu_name, menu_code, pid, app_id, path, component, router_name, icon, menu_type, perms, is_cache, is_frame, visible, remark, create_by, owner_by, create_time, update_by, update_time, is_delete, enabled, sort)
+VALUES (164953884266503, '我的商机导出', DEFAULT, 164953884266498, null, '', '', '', null, 3, 'business:mineBusiness:export', 0, 0, false, ' ', 1, 0, sysdate(), null, null, 0, 1, 1);
+
+
+
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 164953884266498, 1, sysdate(), null, 0);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 164953884266499, 1, sysdate(), null, 0);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 164953884266500, 1, sysdate(), null, 0);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 164953884266501, 1, sysdate(), null, 0);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 164953884266502, 1, sysdate(), null, 0);
+insert into role_menu (is_delete, role_id, sort, update_time, menu_id, create_by, create_time, update_by, enabled)
+values (0, 1, 1, null, 164953884266503, 1, sysdate(), null, 0);

+ 10 - 0
ui/sp-user-center/src/modules/sales/views/business/myCollaborateBusiness/index.vue

@@ -0,0 +1,10 @@
+<template>
+    <CommonBusiness
+        perPrefix="myCollaborateBusiness"
+        :queryRangeType="40"
+    ></CommonBusiness>
+</template>
+
+<script setup name="myCollaborateBusiness" lang="ts">
+import CommonBusiness from '@/modules/sales/views/business/component/CommonBusiness.vue'
+</script>

+ 0 - 0
ui/sp-user-center/src/modules/sales/views/business/myCollaborateBusinessDetailMenu.sql


Деякі файли не було показано, через те що забагато файлів було змінено