瀏覽代碼

添加系统模块、课件相关模块

yuanliang 1 年之前
父節點
當前提交
de653d606f
共有 100 個文件被更改,包括 20947 次插入11 次删除
  1. 64 6
      src/router/constant.ts
  2. 227 0
      src/utils/constant.ts
  3. 278 1
      src/utils/filters.ts
  4. 77 1
      src/utils/searchArray.ts
  5. 111 0
      src/views/city-manage/api.ts
  6. 308 0
      src/views/city-manage/component/city-list.tsx
  7. 126 0
      src/views/city-manage/component/subsidy-list.tsx
  8. 56 0
      src/views/city-manage/index.tsx
  9. 249 0
      src/views/city-manage/modal/city-operation.tsx
  10. 11 0
      src/views/city-manage/modal/index.module.less
  11. 421 0
      src/views/city-manage/modal/subsidy-operation.tsx
  12. 373 0
      src/views/content-manage/api.ts
  13. 236 0
      src/views/content-manage/content-ad/ad-operation.tsx
  14. 332 0
      src/views/content-manage/content-ad/index.tsx
  15. 346 0
      src/views/content-manage/content-app-button/component/school.tsx
  16. 332 0
      src/views/content-manage/content-app-button/component/student.tsx
  17. 332 0
      src/views/content-manage/content-app-button/component/teacher.tsx
  18. 56 0
      src/views/content-manage/content-app-button/index.tsx
  19. 336 0
      src/views/content-manage/content-app-button/modal/app-button-operation.tsx
  20. 213 0
      src/views/content-manage/content-flash/flash-operation.tsx
  21. 331 0
      src/views/content-manage/content-flash/index.tsx
  22. 346 0
      src/views/content-manage/content-information/index.tsx
  23. 241 0
      src/views/content-manage/content-information/information-operation.tsx
  24. 308 0
      src/views/content-manage/content-notice/index.tsx
  25. 166 0
      src/views/content-manage/content-notice/notice-operation.tsx
  26. 151 0
      src/views/content-manage/help-center/component/help-center-category.tsx
  27. 318 0
      src/views/content-manage/help-center/component/help-center.tsx
  28. 55 0
      src/views/content-manage/help-center/index.tsx
  29. 102 0
      src/views/content-manage/help-center/modal/help-center-category-operation.tsx
  30. 163 0
      src/views/content-manage/help-center/modal/help-center-operation.tsx
  31. 466 0
      src/views/content-manage/music-manage/component/music-list.tsx
  32. 262 0
      src/views/content-manage/music-manage/component/tag-list.tsx
  33. 62 0
      src/views/content-manage/music-manage/index.tsx
  34. 20 0
      src/views/content-manage/music-manage/modal/index.module.less
  35. 976 0
      src/views/content-manage/music-manage/modal/music-operation.tsx
  36. 23 0
      src/views/content-manage/music-manage/modal/musicPreView.tsx
  37. 125 0
      src/views/content-manage/music-manage/modal/tag-operation.tsx
  38. 131 0
      src/views/educational-manage/api.ts
  39. 269 0
      src/views/educational-manage/component/category-list.tsx
  40. 450 0
      src/views/educational-manage/component/material-list.tsx
  41. 5 0
      src/views/educational-manage/educationalData.ts
  42. 8 0
      src/views/educational-manage/index.module.less
  43. 54 0
      src/views/educational-manage/index.tsx
  44. 107 0
      src/views/educational-manage/model/addCategory.tsx
  45. 285 0
      src/views/educational-manage/model/addMaterial.tsx
  46. 11 0
      src/views/educational-manage/model/index.module.less
  47. 245 0
      src/views/educational-manage/model/selectMusicSheet.tsx
  48. 114 0
      src/views/knowledge-manage/api.ts
  49. 18 0
      src/views/knowledge-manage/knowledge-detail/index.module.less
  50. 232 0
      src/views/knowledge-manage/knowledge-detail/index.tsx
  51. 288 0
      src/views/knowledge-manage/knowledge-detail/model/addMaterialKnowledge.tsx
  52. 151 0
      src/views/knowledge-manage/knowledge-list/component/knowledgeModel.tsx
  53. 396 0
      src/views/knowledge-manage/knowledge-list/index.tsx
  54. 23 0
      src/views/knowledge-manage/knowledgeTypeData.ts
  55. 46 0
      src/views/login-log/api.ts
  56. 87 0
      src/views/login-log/component/login-device.tsx
  57. 93 0
      src/views/login-log/component/login-log.tsx
  58. 110 0
      src/views/login-log/component/login-statistics.tsx
  59. 63 0
      src/views/login-log/index.tsx
  60. 138 0
      src/views/login-log/interface-log.tsx
  61. 3 3
      src/views/menu-manage/index.tsx
  62. 46 0
      src/views/music-categrory/api.ts
  63. 152 0
      src/views/music-categrory/components/eidt-categroy.tsx
  64. 340 0
      src/views/music-categrory/index.tsx
  65. 9 0
      src/views/sms-code-message/api.ts
  66. 210 0
      src/views/sms-code-message/index.tsx
  67. 737 0
      src/views/system-manage/api.ts
  68. 221 0
      src/views/system-manage/app-version/index.tsx
  69. 54 0
      src/views/system-manage/app-version/operation.ts
  70. 221 0
      src/views/system-manage/app-version/version-operation.tsx
  71. 290 0
      src/views/system-manage/device-num-manage/index.tsx
  72. 485 0
      src/views/system-manage/employee-manage/employee-operation.tsx
  73. 53 0
      src/views/system-manage/employee-manage/employee-tab.tsx
  74. 285 0
      src/views/system-manage/employee-manage/index.tsx
  75. 273 0
      src/views/system-manage/menu-manage/index.tsx
  76. 315 0
      src/views/system-manage/menu-manage/menu-operation.tsx
  77. 752 0
      src/views/system-manage/param-settings/component/attendance-rule.tsx
  78. 209 0
      src/views/system-manage/param-settings/component/finance-rule.tsx
  79. 42 0
      src/views/system-manage/param-settings/component/index.module.less
  80. 130 0
      src/views/system-manage/param-settings/component/leave-category-operation.tsx
  81. 161 0
      src/views/system-manage/param-settings/component/leave-category.tsx
  82. 110 0
      src/views/system-manage/param-settings/component/member-fee.tsx
  83. 255 0
      src/views/system-manage/param-settings/component/message-operation.tsx
  84. 180 0
      src/views/system-manage/param-settings/component/message-setting.tsx
  85. 401 0
      src/views/system-manage/param-settings/component/other-rule.tsx
  86. 168 0
      src/views/system-manage/param-settings/component/payment-operation.tsx
  87. 167 0
      src/views/system-manage/param-settings/component/payment-setting.tsx
  88. 91 0
      src/views/system-manage/param-settings/index.tsx
  89. 111 0
      src/views/system-manage/platform-suggestion/index.tsx
  90. 226 0
      src/views/system-manage/protocol-manage/add-protocol.tsx
  91. 266 0
      src/views/system-manage/protocol-manage/index.tsx
  92. 251 0
      src/views/system-manage/role-mange/index.tsx
  93. 299 0
      src/views/system-manage/role-mange/role-operation.tsx
  94. 174 0
      src/views/system-manage/song-manage/index.tsx
  95. 122 0
      src/views/system-manage/song-manage/song-operation.tsx
  96. 281 0
      src/views/system-manage/song-setting/index.tsx
  97. 389 0
      src/views/system-manage/song-setting/song-operation.tsx
  98. 169 0
      src/views/system-manage/station-manage/index.tsx
  99. 113 0
      src/views/system-manage/station-manage/station-operation.tsx
  100. 263 0
      src/views/system-manage/system-apply/apply-operation.tsx

+ 64 - 6
src/router/constant.ts

@@ -1,9 +1,67 @@
 export const RedirectName = ''
 export const asyncRoutes = {
-  ErrorPage: () => import('@/views/exception/404.vue'),
-  Layout: () => import('@/layout/index.vue'),
-  ParentLayout: () => import('@/layout/parentLayout.vue'),
-  // setMenu: () => import('@/views/menu-manage/index'),
-  homeData: () => import('@/views/test/index'),
-  menuManage: () => import('@/views/menu-manage/index') // 菜单管理
+    ErrorPage: () => import('@/views/exception/404.vue'),
+    Layout: () => import('@/layout/index.vue'),
+    ParentLayout: () => import('@/layout/parentLayout.vue'),
+    // setMenu: () => import('@/views/menu-manage/index'),
+    home: () => import('@/views/test/index'),
+    loginLog: () => import('@/views/login-log/index'), // 登录日志
+    interfaceLog: () => import('@/views/login-log/interface-log'), // 操作日志
+    cityManage: () => import('@/views/city-manage/index'), // 城市管理
+    menuManage: () => import('@/views/system-manage/menu-manage/index'), // 菜单管理
+    systemApply: () => import('@/views/system-manage/system-apply/index'), // 系统应用
+    roleManage: () => import('@/views/system-manage/role-mange/index'), // 角色管理
+    stationManage: () => import('@/views/system-manage/station-manage/index'), // 岗位管理
+    employeeManage: () => import('@/views/system-manage/employee-manage/employee-tab'), // 员工管理
+    protocolManage: () => import('@/views/system-manage/protocol-manage/index'), // 协议管理
+    paramSettings: () => import('@/views/system-manage/param-settings/index'), // 参数设置
+    songManage: () => import('@/views/system-manage/song-manage/index'), // 声部管理
+    songSetting: () => import('@/views/system-manage/song-setting/index'), // 声部基础配置
+    appVersion: () => import('@/views/system-manage/app-version/index'), // app版本控制
+    systemHoliday: () => import('@/views/system-manage/system-holiday/index'), // 节假日设置
+    platformSuggestion: () => import('@/views/system-manage/platform-suggestion/index'), // 平台建议
+    // schoolManage: () => import('@/views/school-manage/index'), // 学校管理
+    // schoolOperation: () => import('@/views/school-manage/component/school-operation'), // 添加学校
+    // schoolDetail: () => import('@/views/school-manage/school-detail/index'), // 学校详情
+    contentFlash: () => import('@/views/content-manage/content-flash/index'), // 闪页管理
+    contentNotice: () => import('@/views/content-manage/content-notice/index'), // 公告管理
+    contentInformation: () => import('@/views/content-manage/content-information/index'), // 资讯管理
+    contentAppButton: () => import('@/views/content-manage/content-app-button/index'), // app 按钮管理
+    contentAd: () => import('@/views/content-manage/content-ad/index'), // 广告管理
+    helpCenter: () => import('@/views/content-manage/help-center/index'), // 帮助中心
+    musicManage: () => import('@/views/content-manage/music-manage/index'), // 曲谱管理
+    // musicgroupManage: () => import('@/views/music-group-manage/index'), // 乐团列表
+    // singleMusicGroupManage: () => import('@/views/music-group-manage/single-index'), // 乐团列表 - single
+    // studyGuidanceManage: () => import('@/views/study-guidance-manage/index'), // 伴学老师管理
+    // studyGuidanceManage: () => import('@/views/study-guidance-manage/index-catagory'), // 伴学老师管理
+    // studyGuidanceDetail: () => import('@/views/study-guidance-manage/study-guidance-detail'), // 伴学老师详情
+    // studyGuidanceStatistics: () => import('@/views/study-guidance-manage/statistics'), //
+    // teacherEvaluationRecord: () => import('@/views/study-guidance-manage/teacher-evaluation-record'), // 伴学老师评测详情
+    // teacherRecordInfo: () => import('@/views/study-guidance-manage/teacher-evaluation-record-info'),
+    // createGroup: () => import('@/views/music-group-manage/createGroup'),
+    educationalManage: () => import('@/views/educational-manage/index'), // 素材管理
+    knowledgeManage: () => import('@/views/knowledge-manage/knowledge-list/index'), //知识点管理
+    knowledgeDetail: () => import('@/views/knowledge-manage/knowledge-detail'), //知识点详情
+    afterClassTrainingManage: () => import('@/views/teaching-manage/after-class-training/index'), //课后训练
+    afterClassTrainingDetail: () => import('@/views/teaching-manage/after-class-training-detail/index'), //课后训练详情
+    // groupDetail: () => import('@/views/music-group-manage/groupDetail'), // 乐团详情
+    coursewareManage: () => import('@/views/teaching-manage/courseware-manage/index'), // 课件列表
+    teachingPlan: () => import('@/views/teaching-manage/teaching-plan/index'), // 教学计划列表
+    coursewareDetail: () => import('@/views/teaching-manage/courseware-manage/detail'), // 课件创建和配置
+    planDetail: () => import('@/views/teaching-manage/teaching-plan/plan-detail'), // 教学计划关联课件
+    // musicCategrory: () => import('@/views/music-categrory/index'),
+    // auditCenter: () => import('@/views/audit-center/index'), // 审核中心
+    // orderManage: () => import('@/views/order-manage/index'), // 交易管理
+    // studentEvaluationRecord: () => import('@/views/student-manage/student-evaluation-record'), // 学员管理评测详情
+    // studentManage: () => import('@/views/student-manage/index-catagory'), // 学员管理
+    // studentDetail: () => import('@/views/student-manage/student-detail'),
+    // operationManual: () => import('@/views/operation-manual/index'),
+    unitExamination: () => import('@/views/teaching-manage/unit-test/index'),
+    unitTestCreate: () => import('@/views/teaching-manage/unit-test/unit-test-index/editAndUpdate'),
+    // subsidyManage: () => import('@/views/subsidy-manage/index'),
+    // subsidyDetail: () => import('@/views/subsidy-manage/subsidyDetail'),
+    // practiceManage: () => import('@/views/practice-manage/index'),
+    deviceNumManage: () => import('@/views/system-manage/device-num-manage/index'),
+    smsCodeMessage: () => import('@/views/sms-code-message/index'), // 短信验证码
+
 }

+ 227 - 0
src/utils/constant.ts

@@ -3,3 +3,230 @@ export const schoolNature = {
   PRIVATE: '民办',
   PUBLIC: '公办'
 }
+
+// 岗位
+export const position = {
+  IM_SERVICE: 'IM客服',
+  REPAIR: '维修工',
+  STAFF: '职员'
+}
+
+// 系统员工状态
+export const employee = {
+  // CANCEL: '注销',
+  LOCKED: '冻结',
+  ACTIVATION: '正常'
+}
+// 学年制
+export const schoolSystem = {
+  fiveYearSystem: '五年制',
+  sixYearSystem: '六年制'
+}
+
+// 支付服务提供方
+export const openType = {
+  ORIGINAL: '原生微信支付宝',
+  ADAPAY: '汇付',
+  OTHER: '其它'
+}
+
+// 客户端类型
+export const clientType = {
+  BACKEND: '后台',
+  SCHOOL: '管理端',
+  TEACHER: '老师端',
+  STUDENT: '学生端',
+  REPAIR: '维修端'
+}
+
+// 协议类型
+export const protocolType = {
+  BUY_ORDER: '产品与服务协议',
+  WITHDRAW: '三方协议',
+  REGISTER: '使用协议(学生端)',
+  REGISTER_TEACHER: '使用协议(伴学端/管理端)',
+  LABOR_TEACHER: '劳务协议'
+}
+
+// 消息组
+export const messageGroup = {
+  SYSTEM: '系统消息',
+  COURSE: '课程信息'
+}
+
+// 平台建议
+export const suggestionType = {
+  APP: '软件反馈',
+  SMART_PRACTICE: '智能陪练'
+}
+
+// 内容分类
+export const contentCategory = {
+  HOT_CONSULTATION: '热门资讯',
+  OPEN_SCREEN_AD: '开屏广告',
+  FLASH_PAGE: '闪页管理',
+  ROTATION_CHART: '轮播图管',
+  MUSIC: '乐理章节'
+}
+
+export const musicType = {
+  DELIVERY: '交付团',
+  PROMOTION: '晋升团'
+}
+// 交付团类型
+export const orchestraTypes = {
+  SINGLE_DELIVERY: '单交付团',
+  MULTIPLE_DELIVERY: '多交付常规团',
+  MULTIPLE_DELIVERY_SCHOOL: '多交付校团'
+}
+
+// 乐团类型
+export const orchestraType = {
+  DELIVERY: '交付团',
+  PROMOTION: '晋升团'
+}
+// 乐团状态
+export const musicStatus = {
+  // SUBJECT_CONFIG: '声部配置',
+  INITIATION_SURVEY: '启蒙调查',
+  PRE_REGISTER: '家长会调查',
+  REGISTER: '乐团注册',
+  DOING: '乐团交付',
+  DONE: '进行中',
+  CLOSE: '关闭'
+}
+
+// 播放类型
+export const audioType = {
+  MP3: 'MP3',
+  MIDI: 'MIDI'
+}
+
+// 伴奏类型
+export const accompanimentType = {
+  HOMEMODE: '自制伴奏',
+  COMMON: '普通伴奏'
+}
+
+// 课程类型
+export const courseEmnu = {
+  PERCUSSION_SINGLE: '打击乐',
+  FLUTE_SINGLE: '长笛',
+  SAX_SINGLE: '萨克斯',
+  CLARINET_SINGLE: '单簧管',
+  TRUMPET_SINGLE: '小号',
+  TROMBONE_SINGLE: '长号',
+  HORN_SINGLE: '圆号',
+  BARITONE_TUBA_SINGLE: '上低音号-大号',
+  MUSIC_THEORY: '乐理',
+  INSTRUMENTAL_ENSEMBLE: '合奏',
+  EUPHONIUM_SINGLE: '上低音号',
+  TUBA_SINGLE: '大号'
+  // TEST_CLARINET: '测试竖笛'
+}
+
+// 学生状态
+export const studentStatusEmnu = {
+  REGISTER: '报名',
+  LEARNING: '在读',
+  OUTOF_ORCHESTRA: '退团'
+}
+
+// 订单状态
+export const orderStatus = {
+  WAIT_PAY: '待支付',
+  PAYING: '支付中',
+  PAID: '已付款',
+  TIMEOUT: '订单超时',
+  FAIL: '支付失败',
+  CLOSED: '订单关闭',
+  REFUNDING: '退款中',
+  REFUNDED: '已退款'
+}
+
+// 订单类型
+export const orderType = {
+  VIP: '开通会员',
+  ORCHESTRA: '乐团注册'
+}
+
+// 交易状态
+export const reportStatus = {
+  SUCCESSED: '交易成功',
+  PENDDING: '退费中',
+  FAILED: '交易失败'
+}
+
+// 交易状态
+export const tradeStatus = {
+  SUCCESSED: '交易成功',
+  PENDDING: '交易中',
+  FAILED: '交易失败'
+}
+
+// 审核状态
+export const refundStatus = {
+  PASS: '审核通过',
+  REJECT: '审核拒绝',
+  ING: '审核中',
+  CLOSED: '已关闭'
+}
+
+// 课程状态
+export const courseStatus = {
+  NOT_START: '未开始',
+  ING: '进行中',
+  COMPLETE: '已结束'
+}
+
+// 学生考勤状态
+export const attendanceStatus = {
+  LATE: '迟到',
+  NORMAL: '正常',
+  LEAVE: '请假',
+  TRUANCY: '旷课',
+  UNCALLED: '未点名'
+}
+
+// 老师老师考勤
+export const teacherAttendanceStatus = {
+  NORMAL: '正常',
+  EXCEPTION: '异常',
+  LATE: '迟到',
+  EARLY: '早退',
+  LEAVE: '请假',
+  TRUANCY: '旷课',
+  NO_SIGN: '未签到',
+  NO_SIGN_OUT: '未签退',
+  UNCALLED: '未点名',
+  LOCATION_EXCEPTION: '定位异常'
+}
+
+// 审核状态
+export const authStatus = {
+  DOING: '审核中',
+  PASS: '通过',
+  UNPASS: '不通过'
+}
+
+// 结算状态
+export const withdrawalStatus = {
+  WAIT: '待结算',
+  SETTLED: '已结算',
+  PART_SETTLED: '部分结算',
+  SETTLE_FAIL: '结算失败',
+  SETTLEING: '结算中'
+}
+// 补助类型
+export const salaryType = {
+  MANAGE: '管理补助',
+  COURSE: '课程补助',
+  TRAINING: '练习补助'
+} as any
+
+// 补助类型
+export const heardLevelType = {
+  BEGINNER: '入门级',
+  ADVANCED: '进阶级',
+  PERFORMER: '大师级'
+} as any

+ 278 - 1
src/utils/filters.ts

@@ -1,10 +1,287 @@
 import * as constant from './constant'
 
 // 岗位管理
-export const filterPosition = (key: 'PRIVATE' | 'PUBLIC' | '') => {
+export const filterPosition = (key: 'IM_SERVICE' | 'REPAIR' | 'STAFF' | '') => {
+  if (key && constant.position && constant.position[key]) {
+    return constant.position[key]
+  } else {
+    return key
+  }
+}
+
+// 员工状态
+export const filterEmployee = (key: 'LOCKED' | 'ACTIVATION' | '') => {
+  if (key && constant.employee && constant.employee[key]) {
+    return constant.employee[key]
+  } else {
+    return key
+  }
+}
+
+// 办学性质
+export const filterSchoolNature = (key: 'PRIVATE' | 'PUBLIC' | '') => {
   if (key && constant.schoolNature && constant.schoolNature[key]) {
     return constant.schoolNature[key]
   } else {
     return key
   }
 }
+
+// 学年制
+export const filterSchoolSystem = (key: 'fiveYearSystem' | 'sixYearSystem' | '') => {
+  if (key && constant.schoolSystem && constant.schoolSystem[key]) {
+    return constant.schoolSystem[key]
+  } else {
+    return key
+  }
+}
+
+// 支付服务提供方
+export const filterOpenType = (key: 'ORIGINAL' | 'ADAPAY' | 'OTHER' | '') => {
+  if (key && constant.openType && constant.openType[key]) {
+    return constant.openType[key]
+  } else {
+    return key
+  }
+}
+
+// 客户端类型
+export const filterClientType = (
+  key: 'BACKEND' | 'SCHOOL' | 'TEACHER' | 'STUDENT' | 'REPAIR' | ''
+) => {
+  if (key && constant.clientType && constant.clientType[key]) {
+    return constant.clientType[key]
+  } else {
+    return key
+  }
+}
+
+// 协议类型
+export const filterProtocolType = (key: 'BUY_ORDER' | 'WITHDRAW' | 'REGISTER' | '') => {
+  if (key && constant.protocolType && constant.protocolType[key]) {
+    return constant.protocolType[key]
+  } else {
+    return key
+  }
+}
+
+// 消息组
+export const filterMessageGroup = (key: 'SYSTEM' | 'COURSE' | '') => {
+  if (key && constant.messageGroup && constant.messageGroup[key]) {
+    return constant.messageGroup[key]
+  } else {
+    return key
+  }
+}
+
+// 平台建议
+export const filterSuggestionType = (key: 'APP' | 'SMART_PRACTICE' | '') => {
+  if (key && constant.suggestionType && constant.suggestionType[key]) {
+    return constant.suggestionType[key]
+  } else {
+    return key
+  }
+}
+
+// 内容分类
+export const filterContentCategory = (
+  key: 'HOT_CONSULTATION' | 'OPEN_SCREEN_AD' | 'FLASH_PAGE' | 'ROTATION_CHART' | 'MUSIC' | ''
+) => {
+  if (key && constant.contentCategory && constant.contentCategory[key]) {
+    return constant.contentCategory[key]
+  } else {
+    return key
+  }
+}
+
+// 交付团类型  filterGroupType
+export const filterGroupType = (
+  key: 'SINGLE_DELIVERY' | 'MULTIPLE_DELIVERY' | 'MULTIPLE_DELIVERY_SCHOOL'
+) => {
+  if (key && constant.orchestraTypes && constant.orchestraTypes[key]) {
+    return constant.orchestraTypes[key]
+  }
+}
+
+// 乐团类型
+export const filterOrchestraType = (key: 'DELIVERY' | 'PROMOTION') => {
+  if (key && constant.orchestraType && constant.orchestraType[key]) {
+    return constant.orchestraType[key]
+  }
+}
+
+// 乐团状态  SUBJECT_CONFIG
+export const filterGroupStatus = (
+  key: 'INITIATION_SURVEY' | 'PRE_REGISTER' | 'REGISTER' | 'DOING' | 'DONE' | 'CLOSE'
+) => {
+  if (key && constant.musicStatus && constant.musicStatus[key]) {
+    return constant.musicStatus[key]
+  }
+}
+
+// 播放类型
+export const filterAudioType = (key: 'MP3' | 'MIDI' | '') => {
+  if (key && constant.audioType && constant.audioType[key]) {
+    return constant.audioType[key]
+  } else {
+    return key
+  }
+}
+
+// 伴奏类型
+export const filterAccompanimentType = (key: 'HOMEMODE' | 'COMMON' | '') => {
+  if (key && constant.accompanimentType && constant.accompanimentType[key]) {
+    return constant.accompanimentType[key]
+  } else {
+    return key
+  }
+}
+
+// 课程类型
+export const filterCourseEmnu = (
+  key:
+    | 'PERCUSSION_SINGLE'
+    | 'FLUTE_SINGLE'
+    | 'SAX_SINGLE'
+    | 'CLARINET_SINGLE'
+    | 'TRUMPET_SINGLE'
+    | 'TROMBONE_SINGLE'
+    | 'HORN_SINGLE'
+    | 'BARITONE_TUBA_SINGLE'
+    | 'EUPHONIUM_SINGLE'
+    | 'TUBA_SINGLE'
+    | 'MUSIC_THEORY'
+    | 'INSTRUMENTAL_ENSEMBLE'
+    | ''
+) => {
+  if (key && constant.courseEmnu && constant.courseEmnu[key]) {
+    return constant.courseEmnu[key]
+  } else {
+    return key
+  }
+}
+
+// 课程状态  courseStatus
+export const filterCourseStatus = (key: 'NOT_START' | 'ING' | 'COMPLETE') => {
+  if (key && constant.courseStatus && constant.courseStatus[key]) {
+    return constant.courseStatus[key]
+  } else {
+    return key
+  }
+}
+// 学生状态
+
+export const filterStudentStatusEmnu = (key: 'REGISTER' | 'LEARNING' | 'OUTOF_ORCHESTRA') => {
+  if (key && constant.studentStatusEmnu && constant.studentStatusEmnu[key]) {
+    return constant.studentStatusEmnu[key]
+  } else {
+    return key
+  }
+}
+
+// 订单状态
+export const filterOrderStatus = (
+  key:
+    | 'WAIT_PAY'
+    | 'PAYING'
+    | 'PAID'
+    | 'TIMEOUT'
+    | 'FAIL'
+    | 'CLOSED'
+    | 'REFUNDING'
+    | 'REFUNDED'
+    | ''
+) => {
+  if (key && constant.orderStatus && constant.orderStatus[key]) {
+    return constant.orderStatus[key]
+  } else {
+    return key
+  }
+}
+
+// 订单类型
+export const filterOrderType = (key: 'VIP' | 'ORCHESTRA' | '') => {
+  if (key && constant.orderType && constant.orderType[key]) {
+    return constant.orderType[key]
+  } else {
+    return key
+  }
+}
+
+// 审核交易状态
+export const filterReportStatus = (key: 'SUCCESSED' | 'FAILED' | 'PENDDING' | '') => {
+  if (key && constant.reportStatus && constant.reportStatus[key]) {
+    return constant.reportStatus[key]
+  } else {
+    return key
+  }
+}
+
+// 交易状态
+export const filterTradeStatus = (key: 'SUCCESSED' | 'FAILED' | 'PENDDING' | '') => {
+  if (key && constant.tradeStatus && constant.tradeStatus[key]) {
+    return constant.tradeStatus[key]
+  } else {
+    return key
+  }
+}
+
+// 交易状态
+export const filterRefundStatus = (key: 'PASS' | 'REJECT' | 'ING' | 'CLOSED' | '') => {
+  if (key && constant.refundStatus && constant.refundStatus[key]) {
+    return constant.refundStatus[key]
+  } else {
+    return key
+  }
+}
+
+// 学生考勤类型  attendanceStatus
+export const filterAttendanceStatus = (
+  key: 'LATE' | 'NORMAL' | 'LEAVE' | 'TRUANCY' | 'UNCALLED' | ''
+) => {
+  if (key && constant.attendanceStatus && constant.attendanceStatus[key]) {
+    return constant.attendanceStatus[key]
+  } else {
+    return key
+  }
+}
+
+// 老师考勤类型
+export const filterTeacherAttendanceStatus = (
+  key: 'NORMAL' | 'EXCEPTION' | 'LATE' | 'EARLY' | 'TRUANCY' | 'NO_SIGN' | 'LOCATION_EXCEPTION' | ''
+) => {
+  if (key && constant.teacherAttendanceStatus && constant.teacherAttendanceStatus[key]) {
+    return constant.teacherAttendanceStatus[key]
+  } else {
+    return key
+  }
+}
+
+// 审核状态
+export const filterAuthStatus = (key: 'DOING' | 'PASS' | 'UNPASS') => {
+  if (key && constant.authStatus && constant.authStatus[key]) {
+    return constant.authStatus[key]
+  } else {
+    return key
+  }
+}
+
+// 结算状态
+export const filterWithdrawalStatus = (
+  key: 'WAIT' | 'SETTLED' | 'PART_SETTLED' | 'SETTLE_FAIL'
+) => {
+  if (key && constant.withdrawalStatus && constant.withdrawalStatus[key]) {
+    return constant.withdrawalStatus[key]
+  } else {
+    return key
+  }
+}
+
+// 补助类型 salaryType
+export const filterSalaryType = (key: 'MANAGE' | 'COURSE' | 'TRAINING') => {
+  if (key && constant.salaryType && constant.salaryType[key]) {
+    return constant.salaryType[key]
+  } else {
+    return key
+  }
+}

+ 77 - 1
src/utils/searchArray.ts

@@ -11,5 +11,81 @@ export function getValueForKey(obj: any) {
   return arr
 }
 
-/** 办学性质 */
+// 岗位管理
+export const positionArray = getValueForKey(constant.position)
+
+// 员工状态
+export const employeeArray = getValueForKey(constant.employee)
+
+// 办学性质
 export const schoolNatureArray = getValueForKey(constant.schoolNature)
+
+// 学年制
+export const schoolSystemArray = getValueForKey(constant.schoolSystem)
+
+// 支付服务提供方
+export const openTypeArray = getValueForKey(constant.openType)
+
+// 客户端类型
+export const clientTypeArray = getValueForKey(constant.clientType)
+
+// 协议类型
+export const protocolTypeArray = getValueForKey(constant.protocolType)
+
+// 消息组
+export const messageGroupArray = getValueForKey(constant.messageGroup)
+
+// 平台建议
+export const suggestionTypeArray = getValueForKey(constant.suggestionType)
+
+// 内容分类
+export const contentCategoryArray = getValueForKey(constant.contentCategory)
+
+// 乐团类型
+export const musicTypeArray = getValueForKey(constant.musicType)
+export const orchestraTypesArray = getValueForKey(constant.orchestraTypes)
+
+// 乐团状态
+export const musicStatusArray = getValueForKey(constant.musicStatus)
+
+// 播放类型
+export const audioTypeArray = getValueForKey(constant.audioType)
+
+// 伴奏类型
+export const accompanimentTypeArray = getValueForKey(constant.accompanimentType)
+
+// 课程类型
+export const courseEmnuArray = getValueForKey(constant.courseEmnu)
+
+// 课程类型
+export const courseStatusArray = getValueForKey(constant.courseStatus)
+
+// 学生状态
+export const studentStatusEmnuArray = getValueForKey(constant.studentStatusEmnu)
+
+// 订单状态
+export const orderStatusArray = getValueForKey(constant.orderStatus)
+
+// 订单类型
+export const orderTypeArray = getValueForKey(constant.orderType)
+
+// 审核交易状态
+export const reportStatusArray = getValueForKey(constant.reportStatus)
+
+// 交易状态
+export const tradeStatusArray = getValueForKey(constant.tradeStatus)
+
+// 审核状态
+export const refundStatusArray = getValueForKey(constant.refundStatus)
+
+// 学生考勤状态
+export const attendanceStatusArray = getValueForKey(constant.attendanceStatus)
+
+// 老师考勤状态  teacherAttendanceStatus
+export const ateacherAttendanceStatusArray = getValueForKey(constant.teacherAttendanceStatus)
+
+// 审核状态
+export const authStatusArray = getValueForKey(constant.authStatus)
+
+// 结算状态
+export const withdrawalStatusArray = getValueForKey(constant.withdrawalStatus)

+ 111 - 0
src/views/city-manage/api.ts

@@ -0,0 +1,111 @@
+
+import request from '@/utils/request/index'
+
+/**
+ * @description: 乐团补助标准列表
+ */
+export const orchestraSubsidyStandardPage = (params: object) => {
+  return request({
+    url: '/cbs-app/orchestraSubsidyStandard/page',
+    method: 'post',
+    data: params,
+  } as any)
+}
+
+/**
+ * @description: 乐团补助标准添加
+ */
+export const orchestraSubsidyStandardSave = (params: object) => {
+  return request({
+    url: '/cbs-app/orchestraSubsidyStandard/save',
+    method: 'post',
+    data: params,
+  } as any)
+}
+
+/**
+ * @description: 乐团补助标准修改
+ */
+export const orchestraSubsidyStandardUpdate = (params: object) => {
+  return request({
+    url: '/cbs-app/orchestraSubsidyStandard/update',
+    method: 'post',
+    data: params,
+  } as any)
+}
+
+/**
+ * @description: 城市收费设置列表
+ */
+export const cityFeeSettingPage = (params: object) => {
+  return request({
+    url: '/cbs-app/cityFeeSetting/page',
+    method: 'post',
+    data: params,
+  } as any)
+}
+
+/**
+ * @description: 城市收费设置添加
+ */
+export const cityFeeSettingSave = (params: object) => {
+  return request({
+    url: '/cbs-app/cityFeeSetting/save',
+    method: 'post',
+    data: params,
+  } as any)
+}
+
+/**
+ * @description: 城市收费设置修改
+ */
+export const cityFeeSettingUpdate = (params: object) => {
+  return request({
+    url: '/cbs-app/cityFeeSetting/update',
+    method: 'post',
+    data: params,
+  } as any)
+}
+
+/**
+ * @description: 获取区域表
+ */
+export const sysAreaPage = (params: object) => {
+  return request({
+    url: '/cbs-app/sysArea/page',
+    method: 'post',
+    data: params,
+  })
+}
+
+/**
+ * @description: 获取区域详情
+ */
+export const sysAreaDetail = (id: number) => {
+  return request({
+    url: `/cbs-app//sysArea/detail/${id}`,
+    method: 'get'
+  })
+}
+
+/**
+ * @description: 根据code获取区域详情
+ */
+export const sysAreaDetailCode = (id: number | string) => {
+  return request({
+    url: `/cbs-app/sysArea/queryByCode/${id}`,
+    method: 'get',
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 查询全部区域
+ */
+export const queryAllProvince = () => {
+  return request({
+    url: `/cbs-app/sysArea/queryAllProvince`,
+    method: 'get',
+    requestType: 'form'
+  } as any)
+}

+ 308 - 0
src/views/city-manage/component/city-list.tsx

@@ -0,0 +1,308 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  NButton,
+  NCascader,
+  NDataTable,
+  NDescriptions,
+  NDescriptionsItem,
+  NFormItem,
+  NInput,
+  NModal,
+  NSpace,
+  NTreeSelect
+} from 'naive-ui'
+import type { TreeSelectOption } from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import CityOperation from '../modal/city-operation'
+import { cityFeeSettingPage, sysAreaPage, sysAreaDetailCode, queryAllProvince } from '../api'
+import numeral from 'numeral'
+export default defineComponent({
+  name: 'city-list',
+  setup() {
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        cityCode: ''
+      },
+      dataList: [] as any,
+      visiableCity: false,
+      cityType: 'add',
+      cityData: {} as any,
+      areaList: [] as any
+    })
+
+    const columns = () => {
+      return [
+        {
+          title: '城市信息',
+          key: 'id',
+          render(row: any) {
+            return (
+              <NDescriptions labelPlacement="left" column={1}>
+                <NDescriptionsItem label="城市名称">{row.cityName}</NDescriptionsItem>
+                <NDescriptionsItem label="城市编号">{row.id}</NDescriptionsItem>
+                {/* <NDescriptionsItem label="补助标准">
+                  {row.orchestraSubsidyStandardName}
+                </NDescriptionsItem> */}
+              </NDescriptions>
+            )
+          }
+        },
+
+        {
+          title: '团练宝零售价',
+          key: 'vipPrice',
+          render(row: any) {
+            return (
+              <NDescriptions labelPlacement="left" column={1}>
+                <NDescriptionsItem label="团练宝零售价">
+                  {numeral(row.vipPrice).format('0,0.00')}元/6个月
+                </NDescriptionsItem>
+                <NDescriptionsItem label="团练宝团购价">
+                  {' '}
+                  {numeral(row.vipGroupPrice).format('0,0.00')}元/6个月
+                </NDescriptionsItem>
+                <NDescriptionsItem label="团练宝原价">
+                  {' '}
+                  {numeral(row.vipOriginalPrice).format('0,0.00')}元/6个月
+                </NDescriptionsItem>
+              </NDescriptions>
+            )
+          }
+          // render(row: any) {
+          //   return `${numeral(row.vipPrice).format('0,0.00')}元/6个月`
+          // }
+        },
+        // {
+        //   title: '团练宝团购价',
+        //   key: 'vipGroupPrice',
+        //   render(row: any) {
+        //     return `${numeral(row.vipGroupPrice).format('0,0.00')}元/6个月`
+        //   }
+        // },
+        // {
+        //   title: '团练宝原价',
+        //   key: 'vipOriginalPrice',
+        //   render(row: any) {
+        //     return `${numeral(row.vipOriginalPrice).format('0,0.00')}元/6个月`
+        //   }
+        // },
+        // {
+        //   title: '补助标准',
+        //   key: 'orchestraSubsidyStandardName'
+        // },
+        {
+          title: '更新人',
+          key: 'operatorName',
+          render(row: any) {
+            return (
+              <NDescriptions labelPlacement="left" column={1}>
+                <NDescriptionsItem label="更新人">{row.operatorName}</NDescriptionsItem>
+                <NDescriptionsItem label="更新时间">{row.updateTime}</NDescriptionsItem>
+              </NDescriptions>
+            )
+          }
+        },
+        // {
+        //   title: '更新时间',
+        //   key: 'updateTime'
+        // },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="cityFeeSetting/update1597887290159779842"
+                  onClick={() => {
+                    state.visiableCity = true
+                    state.cityType = 'edit'
+                    state.cityData = row
+                  }}
+                >
+                  修改
+                </NButton>
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await cityFeeSettingPage({ ...state.pagination, ...state.searchForm })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    // 获取所在城市
+    const getAreaAll = async () => {
+      try {
+        const { data } = await queryAllProvince()
+        ;(data || []).forEach((child: any) => {
+          child.isLeaf = false
+          child.areas.forEach((city: any) => {
+            city.isLeaf = true
+            city.areas = null
+          })
+        })
+        state.areaList = data
+      } catch {}
+    }
+
+    // 加载列表
+    const handleLoad = async (option: TreeSelectOption) => {
+      try {
+        const res = await sysAreaPage({
+          page: 1,
+          rows: 999,
+          parentId: option.id
+        })
+        const data = res.data.rows || []
+
+        const tempList = [] as any
+        data.forEach((item: any) => {
+          tempList.push({ key: item.code, label: item.name, id: item.id, isLeaf: true })
+        })
+        option.children = tempList
+      } catch {
+        //
+      }
+    }
+
+    onMounted(async () => {
+      getAreaAll()
+      getList()
+
+      if (state.searchForm.cityCode) {
+        try {
+          const codeDetail = await sysAreaDetailCode(state.searchForm.cityCode)
+
+          const res = await sysAreaPage({
+            page: 1,
+            rows: 999,
+            parentId: codeDetail.data.parentOrganId
+          })
+          const data = res.data.rows || []
+
+          const tempList = [] as any
+          data.forEach((item: any) => {
+            tempList.push({ key: item.code, label: item.name, id: item.id, isLeaf: true })
+          })
+
+          state.areaList.forEach((area: any) => {
+            if (area.id === codeDetail.data.parentOrganId) {
+              area.children = tempList
+            }
+          })
+        } catch {
+          //
+        }
+      }
+    })
+
+    return () => (
+      <>
+        <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          onSubmit={onSubmit}
+          onSetModel={(val: any) => (state.searchForm = val)}
+        >
+          <NFormItem path="cityCode">
+            <NCascader
+              placeholder="请选择城市"
+              v-model:value={state.searchForm.cityCode}
+              options={state.areaList}
+              checkStrategy="child"
+              childrenField="areas"
+              expandTrigger="hover"
+              valueField="code"
+              labelField="name"
+              clearable
+              filterable
+            />
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+
+        <NSpace style={{ paddingBottom: '12px' }}>
+          <NButton
+            type="primary"
+            //v-auth="cityFeeSetting/save1597887194890358786"
+            onClick={() => {
+              state.visiableCity = true
+              state.cityType = 'add'
+              state.cityData = {}
+            }}
+          >
+            添加城市
+          </NButton>
+        </NSpace>
+        <NDataTable loading={state.loading} columns={columns()} data={state.dataList}></NDataTable>
+        <Pagination
+          v-model:page={state.pagination.page}
+          v-model:pageSize={state.pagination.rows}
+          v-model:pageTotal={state.pagination.pageTotal}
+          onList={getList}
+          sync
+          saveKey="city-list"
+        ></Pagination>
+
+        <NModal
+          v-model:show={state.visiableCity}
+          preset="dialog"
+          showIcon={false}
+          title={state.cityType === 'add' ? '添加城市' : '修改城市'}
+          style={{ width: '500px' }}
+        >
+          <CityOperation
+            type={state.cityType}
+            data={state.cityData}
+            areaList={state.areaList}
+            onGetList={getList}
+            onClose={() => (state.visiableCity = false)}
+          />
+        </NModal>
+      </>
+    )
+  }
+})

+ 126 - 0
src/views/city-manage/component/subsidy-list.tsx

@@ -0,0 +1,126 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import { NButton, NDataTable, NFormItem, NInput, NModal, NSpace } from 'naive-ui'
+import { defineComponent, reactive, onMounted } from 'vue'
+import SubsidyOperation from '../modal/subsidy-operation'
+import { orchestraSubsidyStandardPage } from '../api'
+
+export default defineComponent({
+  name: 'subsidy-list',
+  setup() {
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      dataList: [] as any,
+      visiableSubsidy: false,
+      subsidyOperation: 'add',
+      subsidyData: {} as any
+    })
+
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '补助标准名称',
+          key: 'subsidyStandardName'
+        },
+        {
+          title: '更新人',
+          key: 'operatorName'
+        },
+        {
+          title: '更新时间',
+          key: 'updateTime'
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="orchestraSubsidyStandard/update1597887579789053953"
+                  onClick={() => {
+                    state.visiableSubsidy = true
+                    state.subsidyOperation = 'edit'
+                    state.subsidyData = row
+                  }}
+                >
+                  修改
+                </NButton>
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await orchestraSubsidyStandardPage({ ...state.pagination })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <>
+        <NSpace style={{ paddingBottom: '12px' }}>
+          <NButton
+            type="primary"
+            //v-auth="orchestraSubsidyStandard/save1597887486016999426"
+            onClick={() => {
+              state.visiableSubsidy = true
+              state.subsidyOperation = 'add'
+              state.subsidyData = {}
+            }}
+          >
+            添加补助标准
+          </NButton>
+        </NSpace>
+
+        <NDataTable loading={state.loading} columns={columns()} data={state.dataList}></NDataTable>
+        <Pagination
+          v-model:page={state.pagination.page}
+          v-model:pageSize={state.pagination.rows}
+          v-model:pageTotal={state.pagination.pageTotal}
+          onList={getList}
+          sync
+          saveKey="subsidy-list"
+        ></Pagination>
+
+        <NModal
+          v-model:show={state.visiableSubsidy}
+          preset="dialog"
+          showIcon={false}
+          title={state.subsidyOperation === 'add' ? '新增补助标准' : '修改补助标准'}
+          style={{ width: '800px' }}
+        >
+          <SubsidyOperation
+            type={state.subsidyOperation}
+            data={state.subsidyData}
+            onClose={() => (state.visiableSubsidy = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </>
+    )
+  }
+})

+ 56 - 0
src/views/city-manage/index.tsx

@@ -0,0 +1,56 @@
+import { NTabPane, NTabs } from 'naive-ui'
+import { defineComponent, h, reactive, resolveDynamicComponent } from 'vue'
+import CityList from './component/city-list'
+import SubsidyList from './component/subsidy-list'
+import { getTabsCache, setTabsCaches } from '@/hooks/use-async'
+import { useRoute } from 'vue-router'
+export default defineComponent({
+  name: 'city-manage',
+  setup() {
+    const state = reactive({
+      tabName: 'CityList' as 'CityList' | 'SubsidyList'
+    })
+    const route = useRoute()
+    getTabsCache((val: any) => {
+      if (val.form.tabName) {
+        state.tabName = val.form.tabName
+      }
+    })
+    const setTabs = (val: any) => {
+      setTabsCaches(val, 'tabName', route)
+    }
+    return () => {
+      // const Component = resolveDynamicComponent(state.componentName)
+      return (
+        <div class="system-menu-container">
+          <h2>城市管理</h2>
+
+          <div class={['section-container']} style="padding-top: 20px">
+            <CityList />
+            {/* <NTabs
+              type="line"
+              size="large"
+              v-model:value={state.tabName}
+              onUpdate:value={(val: any) => setTabs(val)}
+            >
+              <NTabPane
+                name="CityList"
+                tab="城市列表"
+                v-auth="cityFeeSetting/page1597885815002091522"
+              >
+                <CityList />
+              </NTabPane>
+              <NTabPane
+                name="SubsidyList"
+                tab="补助标准"
+                v-auth="orchestraSubsidyStandard/page1597886618878201858"
+              >
+                <SubsidyList />
+              </NTabPane>
+            </NTabs> */}
+          </div>
+        </div>
+      )
+    }
+  }
+})

+ 249 - 0
src/views/city-manage/modal/city-operation.tsx

@@ -0,0 +1,249 @@
+import { NForm, NFormItem, NInput, NSelect, NSpace, NButton, useMessage, NCascader } from 'naive-ui'
+import type { TreeSelectOption } from 'naive-ui'
+import { defineComponent, onMounted, PropType, reactive, ref } from 'vue'
+import { cityFeeSettingSave, cityFeeSettingUpdate, orchestraSubsidyStandardPage } from '../api'
+
+export default defineComponent({
+  name: 'city-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    },
+    areaList: {
+      type: Array as PropType<Array<TreeSelectOption>>,
+      default: () => []
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      cityCode: null,
+      provinceCode: null, // 省号
+      vipPrice: null,
+      vipGroupPrice: null,
+      vipOriginalPrice: null,
+      orchestraSubsidyStandardId: null,
+      vipDurationMonth: 6 // 固定半年
+    })
+
+    const btnLoading = ref(false)
+    const formsRef = ref()
+    const subsidyList = ref<any>([])
+    const message = useMessage()
+
+    // 获取补助标准列表
+    const getSubsidy = async () => {
+      try {
+        const res = await orchestraSubsidyStandardPage({
+          page: 1,
+          rows: 100
+        })
+        const data = res.data.rows || []
+        const tempList = [] as any
+        data.forEach((item: any) => {
+          tempList.push({ label: item.subsidyStandardName, value: item.id })
+        })
+        subsidyList.value = tempList
+      } catch {
+        //
+      }
+    }
+
+    // 提交记录
+    const onSubmit = async () => {
+      formsRef.value.validate(async (error: any) => {
+        if (error) return
+        try {
+          console.log(forms, 'forms')
+          const ids = formatParentAreaId(forms.cityCode, props.areaList)
+          forms.provinceCode = ids[0]
+          btnLoading.value = true
+          if (props.type === 'add') {
+            await cityFeeSettingSave({ ...forms })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await cityFeeSettingUpdate({ ...forms, id: props.data.id })
+            message.success('修改成功')
+          }
+          emit('getList')
+          emit('close')
+          setTimeout(() => {
+            btnLoading.value = false
+          }, 100)
+        } catch {
+          //
+          btnLoading.value = false
+        }
+      })
+    }
+    // const formatParentId = (id: any, list: any, ids = [] as any) => {
+    //   for (const item of list) {
+    //     if (item.children && item.children.length > 0) {
+    //       const cIds: any = formatParentId(id, item.children, [...ids, item.code])
+    //       if (cIds.includes(id)) {
+    //         return cIds
+    //       }
+    //     }
+    //     if (item.code === id) {
+    //       return [...ids, id]
+    //     }
+    //   }
+    //   return ids
+    // }
+
+    const formatParentAreaId = (id: any, list: any, ids = [] as any) => {
+      for (const item of list) {
+        if (item.areas && item.areas.length > 0) {
+          const cIds: any = formatParentAreaId(id, item.areas, [...ids, item.code])
+          if (cIds.includes(id)) {
+            return cIds
+          }
+        }
+        if (item.code === id) {
+          return [...ids, id]
+        }
+      }
+      return ids
+    }
+
+    onMounted(async () => {
+      getSubsidy()
+
+      // 初始化数据
+      if (props.type === 'edit') {
+        console.log(props.data, 'data')
+        forms.cityCode = props.data.cityCode
+        forms.provinceCode = props.data.provinceCode
+        forms.vipPrice = props.data.vipPrice.toString()
+        forms.vipGroupPrice = props.data.vipGroupPrice.toString()
+        forms.vipOriginalPrice = props.data.vipOriginalPrice.toString()
+        forms.orchestraSubsidyStandardId = props.data.orchestraSubsidyStandardId
+      }
+    })
+
+    // 只能输入数字
+    const onlyAllowNumber = (value: string) =>
+      !value || /^(?:0.\d{0,3}|[0-9][0-9]{0,12}|[0-9]{1,10}.\d{0,3})$/.test(value)
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="auto">
+          <NFormItem
+            label="城市"
+            path="cityCode"
+            rule={[
+              {
+                required: true,
+                message: '请输入城市'
+              }
+            ]}
+          >
+            <NCascader
+              placeholder="请选择城市"
+              v-model:value={forms.cityCode}
+              options={props.areaList}
+              checkStrategy="child"
+              childrenField="areas"
+              valueField="code"
+              labelField="name"
+              expandTrigger="hover"
+              clearable
+            />
+          </NFormItem>
+          <NFormItem
+            label="团练宝零售价"
+            path="vipPrice"
+            rule={[
+              {
+                required: true,
+                message: '请输入团练宝零售价'
+              }
+            ]}
+          >
+            <NInput
+              v-model:value={forms.vipPrice}
+              placeholder="请输入团练宝零售价"
+              allowInput={onlyAllowNumber}
+              clearable
+            >
+              {{ suffix: () => '元/6个月' }}
+            </NInput>
+          </NFormItem>
+          <NFormItem
+            label="团练宝团购价"
+            path="vipGroupPrice"
+            rule={[
+              {
+                required: true,
+                message: '请输入团练宝团购价'
+              }
+            ]}
+          >
+            <NInput
+              v-model:value={forms.vipGroupPrice}
+              placeholder="请输入团练宝团购价"
+              allowInput={onlyAllowNumber}
+              clearable
+            >
+              {{ suffix: () => '元/6个月' }}
+            </NInput>
+          </NFormItem>
+          <NFormItem
+            label="团练宝原价"
+            path="vipOriginalPrice"
+            rule={[
+              {
+                required: true,
+                message: '请输入团练宝原价'
+              }
+            ]}
+          >
+            <NInput
+              v-model:value={forms.vipOriginalPrice}
+              placeholder="请输入团练宝原价"
+              allowInput={onlyAllowNumber}
+              clearable
+            >
+              {{ suffix: () => '元/6个月' }}
+            </NInput>
+          </NFormItem>
+          {/* <NFormItem
+            label="补助标准"
+            path="orchestraSubsidyStandardId"
+            rule={[
+              {
+                required: true,
+                message: '请选择补助标准'
+              }
+            ]}
+          >
+            <NSelect
+              v-model:value={forms.orchestraSubsidyStandardId}
+              options={subsidyList.value}
+              placeholder="请选择补助标准"
+              clearable
+            ></NSelect>
+          </NFormItem> */}
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton
+            type="primary"
+            onClick={() => onSubmit()}
+            loading={btnLoading.value}
+            disabled={btnLoading.value}
+          >
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 11 - 0
src/views/city-manage/modal/index.module.less

@@ -0,0 +1,11 @@
+.section {
+  background: #f7f7f7;
+  padding: 12px 12px 0;
+  border-radius: 6px;
+
+  margin-bottom: 12px;
+
+  &:last-child {
+    margin-bottom: 0;
+  }
+}

+ 421 - 0
src/views/city-manage/modal/subsidy-operation.tsx

@@ -0,0 +1,421 @@
+import { AddOutline, TrashOutline } from '@vicons/ionicons5'
+import {
+  NForm,
+  NFormItem,
+  NInput,
+  NSpace,
+  NButton,
+  NGrid,
+  NGi,
+  NInputGroup,
+  NIcon,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, PropType, reactive, ref } from 'vue'
+import styles from './index.module.less'
+import { orchestraSubsidyStandardSave, orchestraSubsidyStandardUpdate } from '../api'
+
+export default defineComponent({
+  name: 'city-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {
+        return {}
+      }
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const baseConfig = {
+      minStudentNum: null as any,
+      maxStudentNum: null,
+      singleSubsidy: null,
+      ensembleSubsidy: null,
+      musicTheorySubsidy: null,
+      manageSubsidy: null
+    }
+
+    const btnLoading = ref(false)
+    const forms = reactive({
+      subsidyStandardName: '',
+      config: [{ ...baseConfig }]
+    })
+    const formsRef = ref()
+    const dialog = useDialog()
+    const message = useMessage()
+
+    // 初始化数据
+    if (props.type === 'edit') {
+      forms.subsidyStandardName = props.data.subsidyStandardName
+      forms.config = JSON.parse(props.data.configJson)
+    }
+
+    // 只能输入数字
+    const onlyAllowNumber = (value: string) => !value || /^\d+$/.test(value)
+
+    //
+    const minValidator = (rule: any, value: string) => {
+      const t = rule.field.split(']')
+      const index = t[0].split('[')[1]
+      const prevMinStudentNum = forms.config[index - 1]?.maxStudentNum
+      const maxStudentNum = forms.config[index]?.maxStudentNum
+      return new Promise<void>((resolve, reject) => {
+        if (!value) {
+          reject(Error('请输入最小值'))
+        } else if (
+          ((Number(prevMinStudentNum) || 0) >= Number(value) ||
+            (Number(value) > Number(maxStudentNum) && maxStudentNum)) &&
+          index != '0'
+        ) {
+          reject(Error('数值输入有误'))
+        } else if (value == '0') {
+          reject(Error('数值必须大于0'))
+        } else {
+          resolve()
+        }
+      })
+    }
+
+    //
+    const maxValidator = (rule: any, value: any) => {
+      const t = rule.field.split(']')
+      const index = t[0].split('[')[1]
+      // const prevMinStudentNum = forms.config[index - 1]?.maxStudentNum
+      const minStudentNum = forms.config[index]?.minStudentNum
+      return new Promise<void>((resolve, reject) => {
+        if (!value) {
+          reject(Error('请输入最大值'))
+        } else if ((Number(minStudentNum) || 0) > Number(value)) {
+          reject(Error('数值输入有误'))
+        } else if (value == '0') {
+          reject(Error('数值必须大于0'))
+        } else {
+          resolve()
+        }
+      })
+    }
+
+    // 删除
+    const onRemoveItem = (index: number) => {
+      dialog.warning({
+        title: '提示',
+        content: '是否确定删除?',
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: () => {
+          forms.config.splice(index, 1)
+        }
+      })
+    }
+
+    const onSubmit = async () => {
+      formsRef.value?.validate(async (error: any) => {
+        // console.log(error)
+        if (error) return
+
+        let checkNumber = true
+        forms.config.forEach((config: any, index: number) => {
+          // 从第二条数据开始判断
+          const currentMaxNumber = Number(config.maxStudentNum)
+          const nextConfig = forms.config[index + 1]
+          console.log(nextConfig)
+          // 判断是否有下一个
+          if (nextConfig) {
+            const nextMinNumber = Number(nextConfig.minStudentNum)
+            console.log(currentMaxNumber, nextMinNumber)
+            checkNumber = currentMaxNumber + 1 === nextMinNumber
+          }
+        })
+        if (!checkNumber) {
+          message.error('阶梯配置有误')
+          return
+        }
+
+        try {
+          btnLoading.value = true
+          if (props.type === 'add') {
+            await orchestraSubsidyStandardSave({
+              subsidyStandardName: forms.subsidyStandardName,
+              configJson: JSON.stringify(forms.config)
+            })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await orchestraSubsidyStandardUpdate({
+              id: props.data.id,
+              subsidyStandardName: forms.subsidyStandardName,
+              configJson: JSON.stringify(forms.config)
+            })
+            message.success('修改成功')
+          }
+          btnLoading.value = false
+          emit('close')
+          emit('getList')
+        } catch {
+          //
+        }
+      })
+    }
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm
+          model={forms}
+          ref={formsRef}
+          label-placement="top"
+          label-width="auto"
+          // requireMarkPlacement="left"
+        >
+          <NGrid cols={3} xGap={12}>
+            <NGi>
+              <NFormItem
+                label="补助标准名称"
+                path="subsidyStandardName"
+                rule={[
+                  {
+                    required: true,
+                    message: '请输入补助标准名称',
+                    trigger: ['input', 'blur']
+                  }
+                ]}
+              >
+                <NInput
+                  v-model:value={forms.subsidyStandardName}
+                  placeholder="请输入补助标准名称"
+                ></NInput>
+              </NFormItem>
+            </NGi>
+          </NGrid>
+
+          {forms.config &&
+            forms.config.map((item: any, index: number) => (
+              <div class={styles.section}>
+                <NGrid cols={1}>
+                  <NGi>
+                    <NSpace justify="space-between" style="width: 100%; padding-bottom: 8px;">
+                      <div style={{ fontSize: '16px', fontWeight: 'bold' }}>阶梯{index + 1}</div>
+                      <NButton
+                        quaternary
+                        circle
+                        disabled={index === 0 ? true : false}
+                        onClick={() => onRemoveItem(index)}
+                      >
+                        {{
+                          icon: () => (
+                            <NIcon size={20}>
+                              <TrashOutline />
+                            </NIcon>
+                          )
+                        }}
+                      </NButton>
+                    </NSpace>
+                  </NGi>
+                </NGrid>
+                <NGrid cols={3} xGap={12}>
+                  <NGi>
+                    <NInputGroup>
+                      <NFormItem
+                        label="学生人数范围"
+                        path={`config[${index}].minStudentNum`}
+                        rule={[
+                          {
+                            required: true,
+                            trigger: ['input', 'blur'],
+                            validator: minValidator
+                          }
+                        ]}
+                      >
+                        <NInput
+                          v-model:value={item.minStudentNum}
+                          clearable
+                          placeholder="最小值"
+                          disabled={index > 0 ? true : false}
+                          allowInput={onlyAllowNumber}
+                        >
+                          {{ suffix: () => '人' }}
+                        </NInput>
+                      </NFormItem>
+                      <span style="padding: 0 8px;display: flex;align-items: center;">至</span>
+                      <NFormItem
+                        path={`config[${index}].maxStudentNum`}
+                        rule={[
+                          {
+                            required: true,
+                            trigger: ['input', 'blur'],
+                            validator: maxValidator
+                          }
+                        ]}
+                      >
+                        <NInput
+                          v-model:value={item.maxStudentNum}
+                          clearable
+                          placeholder="最大值"
+                          allowInput={onlyAllowNumber}
+                          onUpdateValue={(val: any) => {
+                            const config = forms.config[index + 1]
+                            if (config) {
+                              config.minStudentNum = val ? (Number(val) + 1).toString() : null
+                            }
+                          }}
+                        >
+                          {{ suffix: () => '人' }}
+                        </NInput>
+                      </NFormItem>
+                    </NInputGroup>
+                  </NGi>
+                  <NGi>
+                    <NFormItem
+                      label="单技训练补助"
+                      path={`config[${index}].singleSubsidy`}
+                      rule={[
+                        {
+                          required: true,
+                          message: '请输入单技训练补助',
+                          trigger: ['input', 'blur']
+                        }
+                      ]}
+                    >
+                      <NInput
+                        v-model:value={item.singleSubsidy}
+                        clearable
+                        placeholder="请输入单技训练补助"
+                        allowInput={onlyAllowNumber}
+                      >
+                        {{ suffix: () => '元/次' }}
+                      </NInput>
+                    </NFormItem>
+                  </NGi>
+                  <NGi>
+                    <NFormItem
+                      label="合奏训练补助"
+                      path={`config[${index}].ensembleSubsidy`}
+                      rule={[
+                        {
+                          required: true,
+                          message: '请输入合奏训练补助',
+                          trigger: ['input', 'blur']
+                        }
+                      ]}
+                    >
+                      <NInput
+                        v-model:value={item.ensembleSubsidy}
+                        clearable
+                        placeholder="请输入合奏训练补助"
+                        allowInput={onlyAllowNumber}
+                      >
+                        {{ suffix: () => '元/次' }}
+                      </NInput>
+                    </NFormItem>
+                  </NGi>
+
+                  <NGi>
+                    <NFormItem
+                      label="乐理训练补助"
+                      path={`config[${index}].musicTheorySubsidy`}
+                      rule={[
+                        {
+                          required: true,
+                          message: '请输入乐理训练补助',
+                          trigger: ['input', 'blur']
+                        }
+                      ]}
+                    >
+                      <NInput
+                        v-model:value={item.musicTheorySubsidy}
+                        clearable
+                        placeholder="请输入乐理训练补助"
+                        allowInput={onlyAllowNumber}
+                      >
+                        {{ suffix: () => '元/次' }}
+                      </NInput>
+                    </NFormItem>
+                  </NGi>
+                  <NGi>
+                    <NFormItem
+                      label="管理补助"
+                      path={`config[${index}].manageSubsidy`}
+                      rule={[
+                        {
+                          required: true,
+                          message: '请输入管理补助',
+                          trigger: ['input', 'blur']
+                        }
+                      ]}
+                    >
+                      <NInput
+                        v-model:value={item.manageSubsidy}
+                        clearable
+                        placeholder="请输入管理补助"
+                        allowInput={onlyAllowNumber}
+                      >
+                        {{ suffix: () => '元/次' }}
+                      </NInput>
+                    </NFormItem>
+                  </NGi>
+                  <NGi>
+                    <NFormItem
+                      label="练习奖励"
+                      path={`config[${index}].trainingSubsidy`}
+                      rule={[
+                        {
+                          required: true,
+                          message: '请输入练习奖励',
+                          trigger: ['input', 'blur']
+                        }
+                      ]}
+                    >
+                      <NInput
+                        v-model:value={item.trainingSubsidy}
+                        clearable
+                        placeholder="请输入练习奖励"
+                        allowInput={onlyAllowNumber}
+                      >
+                        {{ suffix: () => '元/人' }}
+                      </NInput>
+                    </NFormItem>
+                  </NGi>
+                </NGrid>
+              </div>
+            ))}
+
+          <div style={{ margin: '12px 0' }}>
+            <NButton
+              block
+              type="default"
+              onClick={() => {
+                let minNumber = null as any
+                if (forms.config.length > 0) {
+                  minNumber = forms.config[forms.config.length - 1].maxStudentNum
+                }
+                forms.config.push({
+                  ...baseConfig,
+                  minStudentNum: minNumber ? (Number(minNumber) + 1).toString() : null
+                })
+              }}
+            >
+              {{ default: () => '添加标准', icon: () => <AddOutline /> }}
+            </NButton>
+          </div>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton
+            type="primary"
+            onClick={onSubmit}
+            loading={btnLoading.value}
+            disabled={btnLoading.value}
+          >
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 373 - 0
src/views/content-manage/api.ts

@@ -0,0 +1,373 @@
+import request from '@/utils/request/index'
+
+/**
+ * @description: 帮助中心列表
+ */
+export const helpCenterContentPage = (params: object) => {
+  return request({
+    url: '/cbs-app/helpCenterContent/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 帮助中心-启用/停用
+ */
+export const helpCenterContentStatus = (params: any) => {
+  return request({
+    url: '/cbs-app/helpCenterContent/status/' + params.id,
+    method: 'post'
+  } as any)
+}
+
+
+/**
+ * @description: 帮助中心添加
+ */
+export const helpCenterContentSave = (params: object) => {
+  return request({
+    url: '/cbs-app/helpCenterContent/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 帮助中心修改
+ */
+export const helpCenterContentUpdate = (params: object) => {
+  return request({
+    url: '/cbs-app/helpCenterContent/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 帮助中心删除
+ */
+export const helpCenterContentRemove = (params: object) => {
+  return request({
+    url: '/cbs-app/helpCenterContent/remove',
+    method: 'post',
+    data: params,
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 帮助中心分类列表
+ */
+export const helpCenterCatalogPage = (params: object) => {
+  return request({
+    url: '/cbs-app/helpCenterCatalog/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 帮助中心分类添加
+ */
+export const helpCenterCatalogSave = (params: object) => {
+  return request({
+    url: '/cbs-app/helpCenterCatalog/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 帮助中心分类更新
+ */
+export const helpCenterCatalogUpdate = (params: object) => {
+  return request({
+    url: '/cbs-app/helpCenterCatalog/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 帮助中心分类删除
+ */
+export const helpCenterCatalogRemove = (params: object) => {
+  return request({
+    url: '/cbs-app/helpCenterCatalog/remove',
+    method: 'post',
+    data: params,
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 资讯列表
+ */
+export const sysNewsInformationPage = (params: object) => {
+  return request({
+    url: '/cbs-app/sysNewsInformation/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 资讯添加
+ */
+export const sysNewsInformationSave = (params: object) => {
+  return request({
+    url: '/cbs-app/sysNewsInformation/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 资讯更新
+ */
+export const sysNewsInformationUpdate = (params: object) => {
+  return request({
+    url: '/cbs-app/sysNewsInformation/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 资讯删除
+ */
+export const sysNewsInformationRemove = (params: object) => {
+  return request({
+    url: '/cbs-app/sysNewsInformation/remove',
+    method: 'post',
+    data: params,
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 公告列表
+ */
+export const sysNoticePage = (params: object) => {
+  return request({
+    url: '/cbs-app/sysNotice/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 公告添加
+ */
+export const sysNoticeSave = (params: object) => {
+  return request({
+    url: '/cbs-app/sysNotice/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 公告更新
+ */
+export const sysNoticeUpdate = (params: object) => {
+  return request({
+    url: '/cbs-app/sysNotice/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 公告删除
+ */
+export const sysNoticeRemove = (params: object) => {
+  return request({
+    url: '/cbs-app/sysNotice/remove',
+    method: 'post',
+    data: params,
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 公告启用/停用
+ */
+export const sysNoticeStatus = (params?: any) => {
+  return request({
+    url: '/cbs-app/sysNotice/status/' + params.id,
+    method: 'post',
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: APP按钮管理列表
+ */
+export const sysMenuButtonPage = (params: object) => {
+  return request({
+    url: '/cbs-app/sysMenuButton/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: APP按钮管理添加
+ */
+export const sysMenuButtonSave = (params: object) => {
+  return request({
+    url: '/cbs-app/sysMenuButton/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: APP按钮管理更新
+ */
+export const sysMenuButtonUpdate = (params: object) => {
+  return request({
+    url: '/cbs-app/sysMenuButton/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: APP按钮管理删除
+ */
+export const sysMenuButtonRemove = (params: object) => {
+  return request({
+    url: '/cbs-app/sysMenuButton/remove',
+    method: 'post',
+    data: params,
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 乐谱标签列表
+ */
+export const musicTagPage = (params: object) => {
+  return request({
+    url: '/cbs-app/musicTag/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 乐谱标签添加
+ */
+export const musicTagSave = (params: object) => {
+  return request({
+    url: '/cbs-app/musicTag/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 乐谱标签更新
+ */
+export const musicTagUpdate = (params: object) => {
+  return request({
+    url: '/cbs-app/musicTag/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 乐谱标签删除
+ */
+export const musicTagRemove = (params: object) => {
+  return request({
+    url: '/cbs-app/musicTag/remove',
+    method: 'post',
+    data: params,
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 乐谱标签启用/停用
+ */
+export const musicTagState = (params: any) => {
+  return request({
+    url: '/cbs-app/musicTag/state/' + params.id,
+    method: 'post',
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 乐谱列表
+ */
+export const musicSheetPage = (params: object) => {
+  return request({
+    url: '/cbs-app/musicSheet/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 乐谱添加
+ */
+export const musicSheetSave = (params: object) => {
+  return request({
+    url: '/cbs-app/musicSheet/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 乐谱更新
+ */
+export const musicSheetUpdate = (params: object) => {
+  return request({
+    url: '/cbs-app/musicSheet/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 乐谱删除
+ */
+export const musicSheetRemove = (params: object) => {
+  return request({
+    url: '/cbs-app/musicSheet/remove',
+    method: 'post',
+    data: params,
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 乐谱详情
+ */
+export const musicSheetDetail = (params?: any) => {
+  return request({
+    url: '/cbs-app/musicSheet/detail/' + params.id,
+    method: 'get',
+    requestType: 'form'
+  } as any)
+}
+
+
+/**
+ * @description: 乐谱-启用-停用
+ */
+export const musicSheetStatus = (params?: any) => {
+  return request({
+    url: '/cbs-app/musicSheet/status',
+    method: 'post',
+    requestType: 'form',
+    data: params
+  } as any)
+}

+ 236 - 0
src/views/content-manage/content-ad/ad-operation.tsx

@@ -0,0 +1,236 @@
+import {
+  NForm,
+  NFormItem,
+  NInput,
+  NSelect,
+  NSpace,
+  NButton,
+  useMessage,
+  NCascader,
+  NDatePicker
+} from 'naive-ui'
+import type { TreeSelectOption } from 'naive-ui'
+import { defineComponent, onMounted, PropType, provide, reactive, ref } from 'vue'
+import UploadFile from '@/components/upload-file'
+import { sysNewsInformationSave, sysNewsInformationUpdate } from '../api'
+import dayjs from 'dayjs'
+import { getTimes } from '@/utils/dateUtil'
+export default defineComponent({
+  name: 'ad-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    },
+    clientTypeList: {
+      type: Array as PropType<Array<TreeSelectOption>>,
+      default: () => []
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      type: 'OPEN_SCREEN_AD',
+      clientType: null,
+      title: null,
+      coverImage: null,
+      linkUrl: null,
+      showTime: null,
+      operationTime: null as any,
+      onlineTime: null,
+      offlineTime: null,
+      status: false
+    })
+
+    const btnLoading = ref(false)
+    const formsRef = ref()
+    const message = useMessage()
+
+    // 提交记录
+    const onSubmit = async () => {
+      formsRef.value.validate(async (error: any) => {
+        if (error) return
+        try {
+          const { operationTime, ...res } = forms
+          let onlineTime = '',
+            offlineTime = ''
+          if (operationTime) {
+            onlineTime = dayjs(operationTime[0]).format('YYYY-MM-DD HH:mm:ss')
+            offlineTime = dayjs(operationTime[1]).format('YYYY-MM-DD HH:mm:ss')
+          }
+
+          btnLoading.value = true
+          if (props.type === 'add') {
+            await sysNewsInformationSave({
+              ...res,
+              ...getTimes(operationTime, ['onlineTime', 'offlineTime'], 'YYYY-MM-DD')
+            })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await sysNewsInformationUpdate({
+              ...res,
+              ...getTimes(operationTime, ['onlineTime', 'offlineTime'], 'YYYY-MM-DD'),
+              id: props.data.id
+            })
+            message.success('修改成功')
+          }
+          emit('getList')
+          emit('close')
+        } catch {
+          //
+        }
+        setTimeout(() => {
+          btnLoading.value = false
+        }, 100)
+      })
+    }
+
+    onMounted(async () => {
+      // 初始化数据
+      if (props.type === 'edit') {
+        const data = props.data
+        forms.title = data.title
+        forms.coverImage = data.coverImage
+        forms.linkUrl = data.linkUrl
+        forms.showTime = data.showTime
+        if (data.onlineTime && data.offlineTime) {
+          forms.operationTime = [
+            dayjs(data.onlineTime).valueOf(),
+            dayjs(data.offlineTime).valueOf()
+          ]
+        }
+        forms.clientType = data.clientType
+        forms.status = data.status
+      }
+    })
+
+    // 只能输入数字
+    const onlyAllowNumber = (value: string) => !value || /^\d+$/.test(value)
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="auto">
+          <NFormItem
+            label="客户端"
+            path="clientType"
+            rule={[
+              {
+                required: true,
+                message: '请选择客户端'
+              }
+            ]}
+          >
+            <NSelect
+              options={props.clientTypeList}
+              placeholder="请选择客户端"
+              v-model:value={forms.clientType}
+            />
+          </NFormItem>
+          <NFormItem
+            label="广告标题"
+            path="title"
+            rule={[
+              {
+                required: true,
+                message: '请输入广告标题'
+              }
+            ]}
+          >
+            <NInput
+              v-model:value={forms.title}
+              placeholder="请输入广告标题"
+              clearable
+              showCount
+              maxlength={15}
+            />
+          </NFormItem>
+          <NFormItem
+            label="显示时长"
+            path="showTime"
+            rule={[
+              {
+                required: true,
+                message: '请输入显示时长'
+              }
+            ]}
+          >
+            <NInput
+              v-model:value={forms.showTime}
+              placeholder="请输入显示时长"
+              clearable
+              allowInput={onlyAllowNumber}
+            >
+              {{ suffix: () => '秒' }}
+            </NInput>
+          </NFormItem>
+          <NFormItem
+            label="生效时间"
+            path="operationTime"
+            rule={[
+              {
+                required: true,
+                message: '请输入生效时间'
+              }
+            ]}
+          >
+            <NDatePicker
+              v-model:value={forms.operationTime}
+              type="daterange"
+              startPlaceholder="生效开始时间"
+              endPlaceholder="生效结束时间"
+              clearable
+              isDateDisabled={(ts: number) => {
+                return ts < dayjs(dayjs().format('YYYY-MM-DD')).valueOf()
+              }}
+              style={{ width: '100%', textAlign: 'left' }}
+            />
+          </NFormItem>
+          <NFormItem label="跳转链接" path="linkUrl">
+            <NInput v-model:value={forms.linkUrl} placeholder="请输入跳转链接" clearable />
+          </NFormItem>
+          <NFormItem
+            label="广告图片"
+            path="coverImage"
+            rule={[
+              {
+                required: true,
+                message: '请输入广告图片',
+                trigger: ['input', 'blur']
+              }
+            ]}
+          >
+            <UploadFile
+              v-model:fileList={forms.coverImage}
+              accept=".jpg,.jpeg,.png"
+              cropper
+              bucketName="news-info"
+              options={{
+                enlarge: 2,
+                autoCropWidth: 258, //默认生成截图框宽度
+                autoCropHeight: 355 //默认生成截图框高度
+              }}
+              tips="请上传尺寸为516*710,大小1M以内的JPG、PNG图片"
+            />
+          </NFormItem>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton
+            type="primary"
+            onClick={() => onSubmit()}
+            loading={btnLoading.value}
+            disabled={btnLoading.value}
+          >
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 332 - 0
src/views/content-manage/content-ad/index.tsx

@@ -0,0 +1,332 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  NButton,
+  NDataTable,
+  NDescriptions,
+  NDescriptionsItem,
+  NFormItem,
+  NImage,
+  NInput,
+  NModal,
+  NSelect,
+  NSpace,
+  NTag,
+  NTooltip,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { sysNewsInformationPage, sysNewsInformationRemove, sysNewsInformationUpdate } from '../api'
+import AdOperation from './ad-operation'
+import { clientTypeArray } from '@/utils/searchArray'
+import { filterClientType } from '@/utils/filters'
+import TheTooltip from '@/components/TheTooltip'
+// import StationOperation from './station-operation'
+
+export default defineComponent({
+  name: 'content-ad',
+  setup() {
+    const dialog = useDialog()
+    const message = useMessage()
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0,
+        type: 'OPEN_SCREEN_AD'
+      },
+      searchForm: {
+        clientType: null,
+        status: null,
+        keyword: null
+      },
+      clientTypeList: [] as any,
+      dataList: [] as any,
+      visiableAd: false,
+      adOperation: 'add',
+      adData: {} as any
+    })
+
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id',
+          render: (row: any) => (
+            <>
+              <NDescriptions labelPlacement="left" column={1}>
+                <NDescriptionsItem label="广告标题">
+                  <TheTooltip content={row.title} />{' '}
+                </NDescriptionsItem>
+                <NDescriptionsItem label="广告编号">{row.id}</NDescriptionsItem>
+              </NDescriptions>
+            </>
+          )
+        },
+        // {
+        //   title: '广告标题',
+        //   key: 'title'
+        // },
+        {
+          title: '广告图/视频',
+          key: 'coverImage',
+          width: 120,
+          render(row: any) {
+            return <NImage width={70} height={70} src={row.coverImage} />
+          }
+        },
+        {
+          title: '广告链接',
+          key: 'linkUrl',
+          render(row: any) {
+            return <TheTooltip content={row.linkUrl} />
+          }
+        },
+        {
+          title: '生效时间',
+          key: 'onlineTime',
+          render(row: any) {
+            return (
+              <>
+                <p>{row.onlineTime}</p>
+                <p>{row.offlineTime}</p>
+              </>
+            )
+          }
+        },
+        {
+          title: '状态',
+          key: 'status',
+          render(row: any) {
+            return (
+              <NTag type={row.status ? 'primary' : 'default'}>{row.status ? '启用' : '停用'}</NTag>
+            )
+          }
+        },
+        {
+          title: '客户端',
+          key: 'clientType',
+          render(row: any) {
+            return filterClientType(row.clientType)
+          }
+        },
+        {
+          title: '操作人',
+          key: 'updateUser'
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="sysNewsInformation/update1599958605834342402"
+                  onClick={() => {
+                    state.visiableAd = true
+                    state.adOperation = 'edit'
+                    state.adData = row
+                  }}
+                >
+                  修改
+                </NButton>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="sysNewsInformation/update1599958777301684226"
+                  onClick={() => onChangeStatus(row)}
+                >
+                  {row.status ? '停用' : '启用'}
+                </NButton>
+                {!row.status && (
+                  <NButton
+                    type="primary"
+                    size="small"
+                    text
+                    onClick={() => onRmove(row)}
+                    //v-auth="sysNewsInformation/remove1599958687598104577"
+                  >
+                    删除
+                  </NButton>
+                )}
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const onRmove = (row: any): void => {
+      dialog.warning({
+        title: '警告',
+        content: `删除"${row.roleName}",是否继续?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysNewsInformationRemove({ id: row.id })
+            getList()
+            message.success('删除成功')
+          } catch {}
+        }
+      })
+    }
+
+    const onChangeStatus = (row: any) => {
+      const statusStr = row.status ? '停用' : '启用'
+      dialog.warning({
+        title: '警告',
+        content: `是否${statusStr}?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysNewsInformationUpdate({
+              id: row.id,
+              status: row.status ? false : true,
+              title: row.title
+            })
+            getList()
+            message.success(`${statusStr}成功`)
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await sysNewsInformationPage({ ...state.pagination, ...state.searchForm })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    onMounted(() => {
+      clientTypeArray.forEach((item: any) => {
+        if (item.value != 'BACKEND') {
+          state.clientTypeList.push(item)
+        }
+      })
+      getList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <h2>广告管理</h2>
+        <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          onSubmit={onSubmit}
+          label-width=""
+          onSetModel={(val: any) => (state.searchForm = val)}
+        >
+          <NFormItem label="标题" path="keyword">
+            <NInput clearable v-model:value={state.searchForm.keyword} placeholder="请输入标题" />
+          </NFormItem>
+          <NFormItem label="状态" path="status">
+            <NSelect
+              clearable
+              v-model:value={state.searchForm.status}
+              placeholder="请选择状态"
+              options={
+                [
+                  {
+                    label: '启用',
+                    value: 1
+                  },
+                  {
+                    label: '停用',
+                    value: 0
+                  }
+                ] as any
+              }
+            />
+          </NFormItem>
+          <NFormItem label="客户端" path="cityCode">
+            <NSelect
+              v-model={[state.searchForm.clientType, 'value']}
+              placeholder="请选择客户端"
+              options={state.clientTypeList}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="sysNewsInformation/save1599958516768296961"
+              onClick={() => {
+                state.visiableAd = true
+                state.adOperation = 'add'
+                state.adData = {}
+              }}
+            >
+              添加广告
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+          ></Pagination>
+        </div>
+        <NModal
+          v-model:show={state.visiableAd}
+          preset="dialog"
+          showIcon={false}
+          title={state.adOperation === 'add' ? '新增广告' : '修改广告'}
+          style={{ width: '600px' }}
+        >
+          <AdOperation
+            type={state.adOperation}
+            data={state.adData}
+            clientTypeList={state.clientTypeList}
+            onClose={() => (state.visiableAd = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </div>
+    )
+  }
+})

+ 346 - 0
src/views/content-manage/content-app-button/component/school.tsx

@@ -0,0 +1,346 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  NButton,
+  NDataTable,
+  NFormItem,
+  NImage,
+  NInput,
+  NModal,
+  NSelect,
+  NSpace,
+  NTag,
+  NTooltip,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { sysMenuButtonPage, sysMenuButtonRemove, sysMenuButtonUpdate } from '../../api'
+import AppButtonOperation from '../modal/app-button-operation'
+
+export default defineComponent({
+  name: 'content-flash',
+  setup() {
+    const dialog = useDialog()
+    const message = useMessage()
+    const state = reactive({
+      loading: false,
+      pagination: {
+        clientType: 'SCHOOL',
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        keyword: null,
+        status: null,
+        auditFlag: null,
+        order: null as any
+      },
+      dataList: [] as any,
+      visiableApp: false,
+      appOperation: 'add',
+      appData: {} as any
+    })
+    const orderRef = reactive({
+      title: '排序值',
+      key: 'order'
+      // sorter: true,
+      // sortOrder: false
+    } as any)
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '按钮名称',
+          key: 'title'
+        },
+
+        {
+          title: '按钮图',
+          key: 'buttonImage',
+          render(row: any) {
+            return <NImage width={44} height={44} src={row.buttonImage} />
+          }
+        },
+        {
+          title: '跳转链接',
+          key: 'linkUrl',
+          render(row: any) {
+            return (
+              <NTooltip style="max-width: 400px;">
+                {{
+                  default: () => row.linkUrl,
+                  trigger: () => (
+                    <span style="overflow: hidden;display: inline-block;max-width: 200px;white-space: nowrap;text-overflow: ellipsis;">
+                      {row.linkUrl}
+                    </span>
+                  )
+                }}
+              </NTooltip>
+            )
+          }
+        },
+        orderRef,
+        {
+          title: '状态',
+          key: 'status',
+          render(row: any) {
+            return (
+              <NTag type={row.status ? 'primary' : 'default'}>{row.status ? '启用' : '停用'}</NTag>
+            )
+          }
+        },
+
+        {
+          title: '审核版本',
+          key: 'auditFlag',
+          render(row: any) {
+            return (
+              <NTag type={row.auditFlag ? 'primary' : 'default'}>
+                {row.auditFlag ? '是' : '否'}
+              </NTag>
+            )
+          }
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                {!row.status && (
+                  <NButton
+                    type="primary"
+                    size="small"
+                    text
+                    //v-auth="sysMenuButton/update1600386947867840521"
+                    onClick={() => {
+                      state.visiableApp = true
+                      state.appOperation = 'edit'
+                      state.appData = row
+                    }}
+                  >
+                    修改
+                  </NButton>
+                )}
+
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="sysMenuButton/update1600386947867840523"
+                  onClick={() => onChangeStatus(row)}
+                >
+                  {row.status ? '停用' : '启用'}
+                </NButton>
+                {!row.status && (
+                  <NButton
+                    type="primary"
+                    size="small"
+                    text
+                    onClick={() => onRmove(row)}
+                    //v-auth="sysMenuButton/remove1600386947867840522"
+                  >
+                    删除
+                  </NButton>
+                )}
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+    // const handleSorterChange = (sroter: any) => {
+    //   if (!sroter.order) {
+    //     state.searchForm.order = '' as string
+
+    //     orderRef.sortOrder = false
+    //   } else {
+    //     orderRef.sortOrder = sroter.order
+    //     state.searchForm.order = sroter.order == 'ascend' ? 'ASC' : 'DESC'
+    //   }
+    //   getList()
+    // }
+
+    const onRmove = (row: any): void => {
+      dialog.warning({
+        title: '警告',
+        content: `删除"${row.title}",是否继续?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysMenuButtonRemove({ id: row.id })
+            getList()
+            message.success('删除成功')
+          } catch {}
+        }
+      })
+    }
+
+    const onChangeStatus = (row: any) => {
+      const statusStr = row.status ? '停用' : '启用'
+      dialog.warning({
+        title: '警告',
+        content: `是否${statusStr}?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysMenuButtonUpdate({
+              id: row.id,
+              status: row.status ? false : true
+            })
+            getList()
+            message.success(`${statusStr}成功`)
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await sysMenuButtonPage({ ...state.pagination, ...state.searchForm })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          onSubmit={onSubmit}
+          saveKey="app-button-school"
+          onSetModel={(val: any) => (state.searchForm = val)}
+        >
+          <NFormItem label="关键字" path="keyword">
+            <NInput
+              placeholder="请输入按钮名称"
+              v-model:value={state.searchForm.keyword}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="状态" path="status">
+            <NSelect
+              v-model:value={state.searchForm.status}
+              clearable
+              options={
+                [
+                  {
+                    label: '启用',
+                    value: 1
+                  },
+                  {
+                    label: '停用',
+                    value: 0
+                  }
+                ] as any
+              }
+              placeholder="请选择状态"
+            />
+          </NFormItem>
+          <NFormItem label="审核版本" path="auditFlag">
+            <NSelect
+              v-model:value={state.searchForm.auditFlag}
+              clearable
+              options={
+                [
+                  {
+                    label: '是',
+                    value: 1
+                  },
+                  {
+                    label: '否',
+                    value: 0
+                  }
+                ] as any
+              }
+              placeholder="请选择审核版本"
+            />
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="sysMenuButton/save1600386947867840520"
+              onClick={() => {
+                state.visiableApp = true
+                state.appOperation = 'add'
+                state.appData = {}
+              }}
+            >
+              添加按钮
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+            saveKey="app-button-school"
+          ></Pagination>
+        </div>
+        <NModal
+          v-model:show={state.visiableApp}
+          preset="dialog"
+          showIcon={false}
+          title={state.appOperation === 'add' ? '新增按钮' : '修改按钮'}
+          style={{ width: '550px' }}
+        >
+          <AppButtonOperation
+            type={state.appOperation}
+            data={state.appData}
+            clientType="SCHOOL"
+            onClose={() => (state.visiableApp = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </div>
+    )
+  }
+})

+ 332 - 0
src/views/content-manage/content-app-button/component/student.tsx

@@ -0,0 +1,332 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  NButton,
+  NDataTable,
+  NFormItem,
+  NImage,
+  NInput,
+  NModal,
+  NSelect,
+  NSpace,
+  NTag,
+  NTooltip,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { sysMenuButtonPage, sysMenuButtonRemove, sysMenuButtonUpdate } from '../../api'
+import AppButtonOperation from '../modal/app-button-operation'
+
+export default defineComponent({
+  name: 'content-flash',
+  setup() {
+    const dialog = useDialog()
+    const message = useMessage()
+    const state = reactive({
+      loading: false,
+      pagination: {
+        clientType: 'STUDENT',
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        keyword: null,
+        status: null,
+        auditFlag: null
+      },
+      dataList: [] as any,
+      visiableApp: false,
+      appOperation: 'add',
+      appData: {} as any
+    })
+
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '按钮名称',
+          key: 'title'
+        },
+        {
+          title: '按钮图',
+          key: 'buttonImage',
+          render(row: any) {
+            return <NImage width={44} height={44} src={row.buttonImage} />
+          }
+        },
+        {
+          title: '跳转链接',
+          key: 'linkUrl',
+          render(row: any) {
+            return (
+              <NTooltip style="max-width: 400px;">
+                {{
+                  default: () => row.linkUrl,
+                  trigger: () => (
+                    <span style="overflow: hidden;display: inline-block;max-width: 200px;white-space: nowrap;text-overflow: ellipsis;">
+                      {row.linkUrl}
+                    </span>
+                  )
+                }}
+              </NTooltip>
+            )
+          }
+        },
+        {
+          title: '排序值',
+          key: 'order'
+          // sorter: true,
+          // sortOrder: false
+        },
+        {
+          title: '状态',
+          key: 'status',
+          render(row: any) {
+            return (
+              <NTag type={row.status ? 'primary' : 'default'}>{row.status ? '启用' : '停用'}</NTag>
+            )
+          }
+        },
+        {
+          title: '审核版本',
+          key: 'auditFlag',
+          render(row: any) {
+            return (
+              <NTag type={row.auditFlag ? 'primary' : 'default'}>
+                {row.auditFlag ? '是' : '否'}
+              </NTag>
+            )
+          }
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                {!row.status && (
+                  <NButton
+                    type="primary"
+                    size="small"
+                    text
+                    //v-auth="sysMenuButton/update1600386770473947138"
+                    onClick={() => {
+                      state.visiableApp = true
+                      state.appOperation = 'edit'
+                      state.appData = row
+                    }}
+                  >
+                    修改
+                  </NButton>
+                )}
+
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="sysMenuButton/update1600386947867840513"
+                  onClick={() => onChangeStatus(row)}
+                >
+                  {row.status ? '停用' : '启用'}
+                </NButton>
+                {!row.status && (
+                  <NButton
+                    type="primary"
+                    size="small"
+                    text
+                    onClick={() => onRmove(row)}
+                    //v-auth="sysMenuButton/remove1600386859657433090"
+                  >
+                    删除
+                  </NButton>
+                )}
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const onRmove = (row: any): void => {
+      dialog.warning({
+        title: '警告',
+        content: `删除"${row.title}",是否继续?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysMenuButtonRemove({ id: row.id })
+            getList()
+            message.success('删除成功')
+          } catch {}
+        }
+      })
+    }
+
+    const onChangeStatus = (row: any) => {
+      const statusStr = row.status ? '停用' : '启用'
+      dialog.warning({
+        title: '警告',
+        content: `是否${statusStr}?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysMenuButtonUpdate({
+              id: row.id,
+              status: row.status ? false : true
+            })
+            getList()
+            message.success(`${statusStr}成功`)
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await sysMenuButtonPage({ ...state.pagination, ...state.searchForm })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          onSubmit={onSubmit}
+          saveKey="app-button-student"
+          onSetModel={(val: any) => (state.searchForm = val)}
+        >
+          <NFormItem label="关键字" path="keyword">
+            <NInput
+              placeholder="请输入按钮名称"
+              v-model:value={state.searchForm.keyword}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="状态" path="status">
+            <NSelect
+              v-model:value={state.searchForm.status}
+              clearable
+              options={
+                [
+                  {
+                    label: '启用',
+                    value: 1
+                  },
+                  {
+                    label: '停用',
+                    value: 0
+                  }
+                ] as any
+              }
+              placeholder="请选择状态"
+            />
+          </NFormItem>
+          <NFormItem label="审核版本" path="auditFlag">
+            <NSelect
+              v-model:value={state.searchForm.auditFlag}
+              clearable
+              options={
+                [
+                  {
+                    label: '是',
+                    value: 1
+                  },
+                  {
+                    label: '否',
+                    value: 0
+                  }
+                ] as any
+              }
+              placeholder="请选择审核版本"
+            />
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="sysMenuButton/save1600386656187551746"
+              onClick={() => {
+                state.visiableApp = true
+                state.appOperation = 'add'
+                state.appData = {}
+              }}
+            >
+              添加按钮
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+            saveKey="app-button-student"
+          ></Pagination>
+        </div>
+        <NModal
+          v-model:show={state.visiableApp}
+          preset="dialog"
+          showIcon={false}
+          title={state.appOperation === 'add' ? '新增按钮' : '修改按钮'}
+          style={{ width: '550px' }}
+        >
+          <AppButtonOperation
+            type={state.appOperation}
+            data={state.appData}
+            clientType="STUDENT"
+            onClose={() => (state.visiableApp = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </div>
+    )
+  }
+})

+ 332 - 0
src/views/content-manage/content-app-button/component/teacher.tsx

@@ -0,0 +1,332 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  NButton,
+  NDataTable,
+  NFormItem,
+  NImage,
+  NInput,
+  NModal,
+  NSelect,
+  NSpace,
+  NTag,
+  NTooltip,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { sysMenuButtonPage, sysMenuButtonRemove, sysMenuButtonUpdate } from '../../api'
+import AppButtonOperation from '../modal/app-button-operation'
+
+export default defineComponent({
+  name: 'content-flash',
+  setup() {
+    const dialog = useDialog()
+    const message = useMessage()
+    const state = reactive({
+      loading: false,
+      pagination: {
+        clientType: 'TEACHER',
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        keyword: null,
+        status: null,
+        auditFlag: null
+      },
+      dataList: [] as any,
+      visiableApp: false,
+      appOperation: 'add',
+      appData: {} as any
+    })
+
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '按钮名称',
+          key: 'title'
+        },
+        {
+          title: '按钮图',
+          key: 'buttonImage',
+          render(row: any) {
+            return <NImage width={44} height={44} src={row.buttonImage} />
+          }
+        },
+        {
+          title: '跳转链接',
+          key: 'linkUrl',
+          render(row: any) {
+            return (
+              <NTooltip style="max-width: 400px;">
+                {{
+                  default: () => row.linkUrl,
+                  trigger: () => (
+                    <span style="overflow: hidden;display: inline-block;max-width: 200px;white-space: nowrap;text-overflow: ellipsis;">
+                      {row.linkUrl}
+                    </span>
+                  )
+                }}
+              </NTooltip>
+            )
+          }
+        },
+        {
+          title: '排序值',
+          key: 'order'
+          // sorter: true,
+          // sortOrder: false
+        },
+        {
+          title: '状态',
+          key: 'status',
+          render(row: any) {
+            return (
+              <NTag type={row.status ? 'primary' : 'default'}>{row.status ? '启用' : '停用'}</NTag>
+            )
+          }
+        },
+        {
+          title: '审核版本',
+          key: 'auditFlag',
+          render(row: any) {
+            return (
+              <NTag type={row.auditFlag ? 'primary' : 'default'}>
+                {row.auditFlag ? '是' : '否'}
+              </NTag>
+            )
+          }
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                {!row.status && (
+                  <NButton
+                    type="primary"
+                    size="small"
+                    text
+                    //v-auth="sysMenuButton/update1600386947867840517"
+                    onClick={() => {
+                      state.visiableApp = true
+                      state.appOperation = 'edit'
+                      state.appData = row
+                    }}
+                  >
+                    修改
+                  </NButton>
+                )}
+
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="sysMenuButton/update1600386947867840519"
+                  onClick={() => onChangeStatus(row)}
+                >
+                  {row.status ? '停用' : '启用'}
+                </NButton>
+                {!row.status && (
+                  <NButton
+                    type="primary"
+                    size="small"
+                    text
+                    onClick={() => onRmove(row)}
+                    //v-auth="sysMenuButton/remove1600386947867840518"
+                  >
+                    删除
+                  </NButton>
+                )}
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const onRmove = (row: any): void => {
+      dialog.warning({
+        title: '警告',
+        content: `删除"${row.title}",是否继续?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysMenuButtonRemove({ id: row.id })
+            getList()
+            message.success('删除成功')
+          } catch {}
+        }
+      })
+    }
+
+    const onChangeStatus = (row: any) => {
+      const statusStr = row.status ? '停用' : '启用'
+      dialog.warning({
+        title: '警告',
+        content: `是否${statusStr}?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysMenuButtonUpdate({
+              id: row.id,
+              status: row.status ? false : true
+            })
+            getList()
+            message.success(`${statusStr}成功`)
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await sysMenuButtonPage({ ...state.pagination, ...state.searchForm })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          onSubmit={onSubmit}
+          saveKey="app-button-teacher"
+          onSetModel={(val: any) => (state.searchForm = val)}
+        >
+          <NFormItem label="关键字" path="keyword">
+            <NInput
+              placeholder="请输入按钮名称"
+              v-model:value={state.searchForm.keyword}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="状态" path="status">
+            <NSelect
+              v-model:value={state.searchForm.status}
+              clearable
+              options={
+                [
+                  {
+                    label: '启用',
+                    value: 1
+                  },
+                  {
+                    label: '停用',
+                    value: 0
+                  }
+                ] as any
+              }
+              placeholder="请选择状态"
+            />
+          </NFormItem>
+          <NFormItem label="审核版本" path="auditFlag">
+            <NSelect
+              v-model:value={state.searchForm.auditFlag}
+              clearable
+              options={
+                [
+                  {
+                    label: '是',
+                    value: 1
+                  },
+                  {
+                    label: '否',
+                    value: 0
+                  }
+                ] as any
+              }
+              placeholder="请选择审核版本"
+            />
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="sysMenuButton/save1600386947867840516"
+              onClick={() => {
+                state.visiableApp = true
+                state.appOperation = 'add'
+                state.appData = {}
+              }}
+            >
+              添加按钮
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+            saveKey="app-button-teacher"
+          ></Pagination>
+        </div>
+        <NModal
+          v-model:show={state.visiableApp}
+          preset="dialog"
+          showIcon={false}
+          title={state.appOperation === 'add' ? '新增按钮' : '修改按钮'}
+          style={{ width: '550px' }}
+        >
+          <AppButtonOperation
+            type={state.appOperation}
+            data={state.appData}
+            clientType="TEACHER"
+            onClose={() => (state.visiableApp = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </div>
+    )
+  }
+})

+ 56 - 0
src/views/content-manage/content-app-button/index.tsx

@@ -0,0 +1,56 @@
+import { NTabPane, NTabs } from 'naive-ui'
+import { defineComponent, h, reactive, resolveDynamicComponent } from 'vue'
+import Student from './component/student'
+import Teacher from './component/teacher'
+import School from './component/school'
+import { getTabsCache, setTabsCaches } from '@/hooks/use-async'
+import { useRoute } from 'vue-router'
+export default defineComponent({
+  name: 'city-manage',
+  setup() {
+    const state = reactive({
+      tabName: 'Student' as 'Student' | 'Teacher' | 'School'
+    })
+    const route = useRoute()
+    getTabsCache((val: any) => {
+      if (val.form.tabName) {
+        state.tabName = val.form.tabName
+      }
+    })
+    const setTabs = (val: any) => {
+      setTabsCaches(val, 'tabName', route)
+    }
+    return () => {
+      return (
+        <div class="system-menu-container">
+          <h2>APP按钮管理</h2>
+
+          <div class={['section-container']} style="padding-top: 0">
+            <NTabs
+              type="line"
+              size="large"
+              v-model:value={state.tabName}
+              onUpdate:value={(val: any) => setTabs(val)}
+            >
+              <NTabPane name="Student" tab="学生端"
+                        //v-auth="sysMenuButton/page1600386298522472450"
+              >
+                <Student />
+              </NTabPane>
+              <NTabPane name="Teacher" tab="老师端"
+                        //v-auth="sysMenuButton/page1600386947867840514"
+              >
+                <Teacher />
+              </NTabPane>
+              <NTabPane name="School" tab="管理端"
+                        //v-auth="sysMenuButton/page1600386947867840515"
+              >
+                <School />
+              </NTabPane>
+            </NTabs>
+          </div>
+        </div>
+      )
+    }
+  }
+})

+ 336 - 0
src/views/content-manage/content-app-button/modal/app-button-operation.tsx

@@ -0,0 +1,336 @@
+import {
+  NForm,
+  NFormItem,
+  NInput,
+  NSelect,
+  NSpace,
+  NButton,
+  useMessage,
+  NCascader,
+  NInputNumber,
+  NIcon,
+  NTooltip
+} from 'naive-ui'
+import { defineComponent, onMounted, PropType, reactive, ref } from 'vue'
+import { sysMenuButtonSave, sysMenuButtonUpdate } from '../../api'
+import UploadFile from '@/components/upload-file'
+import { QuestionCircleFilled, QuestionCircleOutlined } from '@vicons/antd'
+
+export default defineComponent({
+  name: 'city-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    clientType: {
+      type: String,
+      default: 'STUDENT'
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      title: null,
+      linkUrl: null,
+      order: null,
+      buttonImage: null,
+      type: 'APP',
+      homePage: null as any, // 是否首页展示
+      clientType: props.clientType || null,
+      auditFlag: null as any,
+      defaultButton: null as any, // 是否默认按钮(用于添加管理员默认添加按钮)
+      lockFlag: null as any, // 是否锁定-主要是管理端使用
+      manageButton: null as any // 辅助管理-主要是管理端使用
+    })
+
+    const btnLoading = ref(false)
+    const formsRef = ref()
+    const message = useMessage()
+
+    // 提交记录
+    const onSubmit = async () => {
+      formsRef.value.validate(async (error: any) => {
+        if (error) return
+        try {
+          btnLoading.value = true
+          if (props.type === 'add') {
+            await sysMenuButtonSave({ ...forms })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await sysMenuButtonUpdate({ ...forms, id: props.data.id })
+            message.success('修改成功')
+          }
+          emit('getList')
+          emit('close')
+        } catch {
+          //
+        }
+        setTimeout(() => {
+          btnLoading.value = false
+        }, 100)
+      })
+    }
+
+    // 初始化数据
+    if (props.type === 'edit') {
+      const data = props.data
+      forms.title = data.title
+      forms.linkUrl = data.linkUrl
+      forms.order = data.order
+      forms.buttonImage = data.buttonImage
+      forms.auditFlag = data.auditFlag ? 1 : 0
+      forms.homePage = data.homePage ? 1 : 0
+      forms.defaultButton = data.defaultButton ? 1 : 0
+      forms.lockFlag = data.lockFlag ? 1 : 0
+      forms.manageButton = data.manageButton ? 1 : 0
+    }
+
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="auto">
+          <NFormItem
+            label="标题"
+            path="title"
+            rule={[
+              {
+                required: true,
+                message: '请输入标题',
+                trigger: ['input', 'blur']
+              }
+            ]}
+          >
+            <NInput
+              v-model:value={forms.title}
+              placeholder="请输入标题"
+              clearable
+              showCount
+              maxlength={14}
+            />
+          </NFormItem>
+          <NFormItem label="跳转链接" path="linkUrl">
+            <NInput v-model:value={forms.linkUrl} placeholder="请输入跳转链接" clearable />
+          </NFormItem>
+          <NFormItem
+            label="排序值"
+            path="order"
+            rule={[
+              {
+                required: true,
+                message: '请输入排序值'
+              }
+            ]}
+          >
+            <NInputNumber
+              v-model:value={forms.order}
+              placeholder="请输入排序值"
+              clearable
+              min={0}
+              style={{ width: '100%' }}
+            />
+          </NFormItem>
+          <NFormItem
+            label="是否审核"
+            path="auditFlag"
+            rule={[
+              {
+                required: true,
+                message: '请选择是否审核'
+              }
+            ]}
+          >
+            <NSelect
+              v-model:value={forms.auditFlag}
+              placeholder="请选择是否审核"
+              options={[
+                {
+                  label: '是',
+                  value: 1
+                },
+                {
+                  label: '否',
+                  value: 0
+                }
+              ]}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem
+            label="首页展示"
+            path="homePage"
+            rule={[
+              {
+                required: true,
+                message: '请选择首页展示'
+              }
+            ]}
+          >
+            <NSelect
+              v-model:value={forms.homePage}
+              placeholder="请选择首页展示"
+              options={[
+                {
+                  label: '是',
+                  value: 1
+                },
+                {
+                  label: '否',
+                  value: 0
+                }
+              ]}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem
+            label="是否默认"
+            path="defaultButton"
+            rule={[
+              {
+                required: true,
+                message: '请选择是否默认'
+              }
+            ]}
+          >
+            {/* 刘俊驰要求是是0 否是1 */}
+            <NSelect
+              v-model:value={forms.defaultButton}
+              placeholder="请选择是否默认"
+              disabled={forms.manageButton === 1}
+              options={[
+                {
+                  label: '是',
+                  value: 1
+                },
+                {
+                  label: '否',
+                  value: 0
+                }
+              ]}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem
+            label="是否锁定"
+            path="lockFlag"
+            rule={[
+              {
+                required: true,
+                message: '请选择是否锁定'
+              }
+            ]}
+          >
+            <NSelect
+              v-model:value={forms.lockFlag}
+              placeholder="请选择是否锁定"
+              options={[
+                {
+                  label: '是',
+                  value: 1
+                },
+                {
+                  label: '否',
+                  value: 0
+                }
+              ]}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem
+            // label="辅助管理"
+            path="manageButton"
+            rule={[
+              {
+                required: true,
+                message: '请选择辅助管理'
+              }
+            ]}
+          >
+            {{
+              label: () => (
+                <span style={{ display: 'flex', alignItems: 'center' }}>
+                  管理配置
+                  <NTooltip>
+                    {{
+                      trigger: () => (
+                        <NIcon size={16} color="#FF8057">
+                          <QuestionCircleFilled />
+                        </NIcon>
+                      ),
+                      default: () => '总管理员是否可将该按钮权限配置给辅助管理员'
+                    }}
+                  </NTooltip>
+                </span>
+              ),
+              default: () => (
+                <NSelect
+                  v-model:value={forms.manageButton}
+                  placeholder="请选择管理配置"
+                  options={[
+                    {
+                      label: '是',
+                      value: 0
+                    },
+                    {
+                      label: '否',
+                      value: 1
+                    }
+                  ]}
+                  onUpdateValue={(val: any) => {
+                    // 5737 优化:app按钮管理--管理配置选【否】时,强制 是否默认值 为【否】,如果管理配置选【是】时,则不限制
+                    if (val === 1) {
+                      forms.defaultButton = 0
+                    }
+                  }}
+                  clearable
+                />
+              )
+            }}
+          </NFormItem>
+          <NFormItem
+            label="按钮图"
+            path="buttonImage"
+            rule={[
+              {
+                required: true,
+                message: '请上传按钮图',
+                trigger: ['input', 'blur']
+              }
+            ]}
+          >
+            <UploadFile
+              v-model:fileList={forms.buttonImage}
+              accept=".jpg,.jpeg,.png"
+              cropper
+              bucketName="news-info"
+              options={{
+                enlarge: 1, //  图片放大倍数
+                autoCropWidth: 88, //默认生成截图框宽度
+                autoCropHeight: 88 //默认生成截图框高度
+              }}
+              size={1}
+              tips="请上传请上传尺寸为88*88大小1M以内的JPG、PNG图片"
+            />
+          </NFormItem>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton
+            type="primary"
+            onClick={() => onSubmit()}
+            loading={btnLoading.value}
+            disabled={btnLoading.value}
+          >
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 213 - 0
src/views/content-manage/content-flash/flash-operation.tsx

@@ -0,0 +1,213 @@
+import {
+  NForm,
+  NFormItem,
+  NInput,
+  NSelect,
+  NSpace,
+  NButton,
+  useMessage,
+  NCascader,
+  NDatePicker
+} from 'naive-ui'
+import type { TreeSelectOption } from 'naive-ui'
+import { defineComponent, onMounted, PropType, provide, reactive, ref } from 'vue'
+import UploadFile from '@/components/upload-file'
+import { sysNewsInformationSave, sysNewsInformationUpdate } from '../api'
+import dayjs from 'dayjs'
+import { getTimes } from '@/utils/dateUtil'
+export default defineComponent({
+  name: 'flash-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    },
+    clientTypeList: {
+      type: Array as PropType<Array<TreeSelectOption>>,
+      default: () => []
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      type: 'FLASH_PAGE',
+      clientType: null,
+      title: null,
+      coverImage: null,
+      linkUrl: null,
+      operationTime: null as any,
+      onlineTime: null,
+      offlineTime: null,
+      status: false
+    })
+
+    const btnLoading = ref(false)
+    const formsRef = ref()
+    const message = useMessage()
+
+    // 提交记录
+    const onSubmit = async () => {
+      formsRef.value.validate(async (error: any) => {
+        if (error) return
+        try {
+          const { operationTime, ...res } = forms
+          let onlineTime = '',
+            offlineTime = ''
+          if (operationTime) {
+            onlineTime = dayjs(operationTime[0]).format('YYYY-MM-DD HH:mm:ss')
+            offlineTime = dayjs(operationTime[1]).format('YYYY-MM-DD HH:mm:ss')
+          }
+
+          btnLoading.value = true
+          if (props.type === 'add') {
+            await sysNewsInformationSave({
+              ...res,
+              ...getTimes(operationTime, ['onlineTime', 'offlineTime'], 'YYYY-MM-DD')
+            })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await sysNewsInformationUpdate({
+              ...res,
+              ...getTimes(operationTime, ['onlineTime', 'offlineTime'], 'YYYY-MM-DD'),
+              id: props.data.id
+            })
+            message.success('修改成功')
+          }
+          emit('getList')
+          emit('close')
+        } catch {
+          //
+        }
+        setTimeout(() => {
+          btnLoading.value = false
+        }, 100)
+      })
+    }
+
+    onMounted(async () => {
+      // 初始化数据
+      if (props.type === 'edit') {
+        const data = props.data
+        forms.title = data.title
+        forms.coverImage = data.coverImage
+        forms.linkUrl = data.linkUrl
+        if (data.onlineTime && data.offlineTime) {
+          forms.operationTime = [
+            dayjs(data.onlineTime).valueOf(),
+            dayjs(data.offlineTime).valueOf()
+          ]
+        }
+        forms.clientType = data.clientType
+        forms.status = data.status
+      }
+    })
+
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="auto">
+          <NFormItem
+            label="客户端"
+            path="clientType"
+            rule={[
+              {
+                required: true,
+                message: '请选择客户端'
+              }
+            ]}
+          >
+            <NSelect
+              options={props.clientTypeList}
+              placeholder="请选择客户端"
+              v-model:value={forms.clientType}
+            />
+          </NFormItem>
+          <NFormItem
+            label="闪页标题"
+            path="title"
+            rule={[
+              {
+                required: true,
+                message: '请输入闪页标题'
+              }
+            ]}
+          >
+            <NInput
+              v-model:value={forms.title}
+              placeholder="请输入闪页标题"
+              clearable
+              showCount
+              maxlength={15}
+            />
+          </NFormItem>
+          <NFormItem
+            label="生效时间"
+            path="operationTime"
+            rule={[
+              {
+                required: true,
+                message: '请输入生效时间'
+              }
+            ]}
+          >
+            <NDatePicker
+              v-model:value={forms.operationTime}
+              type="daterange"
+              startPlaceholder="生效开始时间"
+              endPlaceholder="生效结束时间"
+              clearable
+              isDateDisabled={(ts: number) => {
+                return ts < dayjs(dayjs().format('YYYY-MM-DD')).valueOf()
+              }}
+              style={{ width: '100%', textAlign: 'left' }}
+            />
+          </NFormItem>
+          <NFormItem label="跳转连接" path="linkUrl">
+            <NInput v-model:value={forms.linkUrl} placeholder="请输入跳转连接" clearable />
+          </NFormItem>
+          <NFormItem
+            label="闪页图片"
+            path="coverImage"
+            rule={[
+              {
+                required: true,
+                message: '请输入闪页图片',
+                trigger: ['input', 'blur']
+              }
+            ]}
+          >
+            <UploadFile
+              v-model:fileList={forms.coverImage}
+              accept=".jpg,.jpeg,.png"
+              cropper
+              bucketName="news-info"
+              options={{
+                enlarge: 2,
+                autoCropWidth: 258, //默认生成截图框宽度
+                autoCropHeight: 355 //默认生成截图框高度
+              }}
+              tips="请上传尺寸为516*710,大小1M以内的JPG、PNG图片"
+            />
+          </NFormItem>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton
+            type="primary"
+            onClick={() => onSubmit()}
+            loading={btnLoading.value}
+            disabled={btnLoading.value}
+          >
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 331 - 0
src/views/content-manage/content-flash/index.tsx

@@ -0,0 +1,331 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  NButton,
+  NDataTable,
+  NDescriptions,
+  NDescriptionsItem,
+  NFormItem,
+  NImage,
+  NInput,
+  NModal,
+  NSelect,
+  NSpace,
+  NTag,
+  NTooltip,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { sysNewsInformationPage, sysNewsInformationRemove, sysNewsInformationUpdate } from '../api'
+import FlashOperation from './flash-operation'
+import { clientTypeArray } from '@/utils/searchArray'
+import { filterClientType } from '@/utils/filters'
+import TheTooltip from '@/components/TheTooltip'
+
+export default defineComponent({
+  name: 'content-flash',
+  setup() {
+    const dialog = useDialog()
+    const message = useMessage()
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        type: 'FLASH_PAGE',
+        pageTotal: 0
+      },
+      searchForm: {
+        clientType: null,
+        status: null,
+        keyword: null
+      },
+      clientTypeList: [] as any,
+      dataList: [] as any,
+      visiableFlash: false,
+      flashOperation: 'add',
+      flashData: {} as any
+    })
+
+    const columns = () => {
+      return [
+        {
+          title: '基本信息',
+          key: 'id',
+          render: (row: any) => (
+            <>
+              <NDescriptions labelPlacement="left" column={1}>
+                <NDescriptionsItem label="闪页标题">
+                  {' '}
+                  <TheTooltip content={row.title} />{' '}
+                </NDescriptionsItem>
+                <NDescriptionsItem label="闪页编号">{row.id}</NDescriptionsItem>
+              </NDescriptions>
+            </>
+          )
+        },
+        // {
+        //   title: '闪页标题',
+        //   key: 'title'
+        // },
+        {
+          title: '闪页图',
+          key: 'coverImage',
+          render(row: any) {
+            return <NImage width={70} height={70} src={row.coverImage} />
+          }
+        },
+        {
+          title: '跳转连接',
+          key: 'linkUrl',
+          render(row: any) {
+            return <TheTooltip content={row.linkUrl} />
+          }
+        },
+        {
+          title: '生效时间',
+          key: 'onlineTime',
+          render(row: any) {
+            return (
+              <>
+                <p>{row.onlineTime}</p>
+                <p>{row.offlineTime}</p>
+              </>
+            )
+          }
+        },
+        {
+          title: '状态',
+          key: 'status',
+          render(row: any) {
+            return (
+              <NTag type={row.status ? 'primary' : 'default'}>{row.status ? '启用' : '停用'}</NTag>
+            )
+          }
+        },
+        {
+          title: '客户端',
+          key: 'clientType',
+          render(row: any) {
+            return filterClientType(row.clientType)
+          }
+        },
+        {
+          title: '操作人',
+          key: 'updateUser'
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="sysNewsInformation/update1599695301404631042"
+                  onClick={() => {
+                    state.visiableFlash = true
+                    state.flashOperation = 'edit'
+                    state.flashData = row
+                  }}
+                >
+                  修改
+                </NButton>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="sysNewsInformation/remove1599695493080129537"
+                  onClick={() => onChangeStatus(row)}
+                >
+                  {row.status ? '停用' : '启用'}
+                </NButton>
+                {!row.status && (
+                  <NButton
+                    type="primary"
+                    size="small"
+                    text
+                    onClick={() => onRmove(row)}
+                    //v-auth="sysNewsInformation/remove1599695493080129537"
+                  >
+                    删除
+                  </NButton>
+                )}
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const onRmove = (row: any): void => {
+      dialog.warning({
+        title: '警告',
+        content: `删除"${row.title}",是否继续?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysNewsInformationRemove({ id: row.id })
+            getList()
+            message.success('删除成功')
+          } catch {}
+        }
+      })
+    }
+
+    const onChangeStatus = (row: any) => {
+      const statusStr = row.status ? '停用' : '启用'
+      dialog.warning({
+        title: '警告',
+        content: `是否${statusStr}?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysNewsInformationUpdate({
+              id: row.id,
+              status: row.status ? false : true,
+              title: row.title
+            })
+            getList()
+            message.success(`${statusStr}成功`)
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await sysNewsInformationPage({ ...state.pagination, ...state.searchForm })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    onMounted(() => {
+      clientTypeArray.forEach((item: any) => {
+        if (item.value != 'BACKEND') {
+          state.clientTypeList.push(item)
+        }
+      })
+      getList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <h2>闪页管理</h2>
+        <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          onSubmit={onSubmit}
+          label-width=""
+          onSetModel={(val: any) => (state.searchForm = val)}
+        >
+          <NFormItem label="标题" path="keyword">
+            <NInput clearable v-model:value={state.searchForm.keyword} placeholder="请输入标题" />
+          </NFormItem>
+          <NFormItem label="状态" path="status">
+            <NSelect
+              clearable
+              v-model:value={state.searchForm.status}
+              placeholder="请选择状态"
+              options={
+                [
+                  {
+                    label: '启用',
+                    value: 1
+                  },
+                  {
+                    label: '停用',
+                    value: 0
+                  }
+                ] as any
+              }
+            />
+          </NFormItem>
+          <NFormItem label="客户端" path="clientType">
+            <NSelect
+              clearable
+              v-model={[state.searchForm.clientType, 'value']}
+              placeholder="请选择客户端"
+              options={state.clientTypeList}
+            />
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="sysNewsInformation/save1599695226544693250"
+              onClick={() => {
+                state.visiableFlash = true
+                state.flashOperation = 'add'
+                state.flashData = {}
+              }}
+            >
+              添加闪页
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+          ></Pagination>
+        </div>
+        <NModal
+          v-model:show={state.visiableFlash}
+          title={state.flashOperation === 'add' ? '新增闪页' : '修改闪页'}
+          preset="dialog"
+          showIcon={false}
+          style={{ width: '600px' }}
+        >
+          <FlashOperation
+            type={state.flashOperation}
+            data={state.flashData}
+            clientTypeList={state.clientTypeList}
+            onClose={() => (state.visiableFlash = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </div>
+    )
+  }
+})

+ 346 - 0
src/views/content-manage/content-information/index.tsx

@@ -0,0 +1,346 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  NButton,
+  NDataTable,
+  NDescriptions,
+  NDescriptionsItem,
+  NFormItem,
+  NImage,
+  NInput,
+  NModal,
+  NSelect,
+  NSpace,
+  NTag,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { sysNewsInformationPage, sysNewsInformationRemove, sysNewsInformationUpdate } from '../api'
+import InformationOperation from './information-operation'
+import { clientTypeArray } from '@/utils/searchArray'
+import { filterClientType } from '@/utils/filters'
+import TheTooltip from '@/components/TheTooltip'
+import { sysEmployeePage } from '@/views/system-manage/api'
+// import StationOperation from './station-operation'
+
+export default defineComponent({
+  name: 'content-ad',
+  setup() {
+    const dialog = useDialog()
+    const message = useMessage()
+    const state = reactive({
+      loading: false,
+      pagination: {
+        type: 'HOT_CONSULTATION',
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        keyword: null,
+        status: null,
+        updateBy: null,
+        clientType: null
+      },
+      clientTypeList: [] as any,
+      dataList: [] as any,
+      visiableInfo: false,
+      infoOperation: 'add',
+      infoData: {} as any,
+      staffList: [] as any
+    })
+
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id',
+          render: (row: any) => (
+            <>
+              <NDescriptions labelPlacement="left" column={1}>
+                <NDescriptionsItem label="资讯标题">
+                  <TheTooltip content={row.title} />{' '}
+                </NDescriptionsItem>
+                <NDescriptionsItem label="资讯编号">{row.id}</NDescriptionsItem>
+              </NDescriptions>
+            </>
+          )
+        },
+        // {
+        //   title: '资讯标题',
+        //   key: 'title'
+        // },
+        {
+          title: '资讯封面',
+          key: 'coverImage',
+          render(row: any) {
+            return <NImage width={87} height={60} src={row.coverImage} />
+          }
+        },
+        {
+          title: '状态',
+          key: 'status',
+          render(row: any) {
+            return (
+              <NTag type={row.status ? 'primary' : 'default'}>{row.status ? '启用' : '停用'}</NTag>
+            )
+          }
+        },
+        {
+          title: '客户端',
+          key: 'clientType',
+          render(row: any) {
+            return filterClientType(row.clientType)
+          }
+        },
+        {
+          title: '操作人',
+          key: 'updateUser'
+        },
+        {
+          title: '创建时间',
+          key: 'createTime'
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="sysNewsInformation/update1599958605834342402"
+                  onClick={() => {
+                    state.visiableInfo = true
+                    state.infoOperation = 'edit'
+                    state.infoData = row
+                  }}
+                >
+                  修改
+                </NButton>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="sysNewsInformation/update1599958777301684226"
+                  onClick={() => onChangeStatus(row)}
+                >
+                  {row.status ? '停用' : '启用'}
+                </NButton>
+                {!row.status && (
+                  <NButton
+                    type="primary"
+                    size="small"
+                    text
+                    onClick={() => onRmove(row)}
+                    //v-auth="sysNewsInformation/remove1599958687598104577"
+                  >
+                    删除
+                  </NButton>
+                )}
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const onRmove = (row: any): void => {
+      dialog.warning({
+        title: '警告',
+        content: `删除"${row.title}",是否继续?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysNewsInformationRemove({ id: row.id })
+            getList()
+            message.success('删除成功')
+          } catch {}
+        }
+      })
+    }
+
+    const onChangeStatus = (row: any) => {
+      const statusStr = row.status ? '停用' : '启用'
+      dialog.warning({
+        title: '警告',
+        content: `是否${statusStr}"${row.title}"?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysNewsInformationUpdate({
+              id: row.id,
+              status: row.status ? false : true,
+              title: row.title
+            })
+            getList()
+            message.success(`${statusStr}成功`)
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await sysNewsInformationPage({ ...state.pagination, ...state.searchForm })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    // 获取操作人
+    const getOperationList = async () => {
+      try {
+        const { data } = await sysEmployeePage({
+          page: 1,
+          rows: 999
+          // jobType: 'STAFF'
+        })
+
+        if (Array.isArray(data.rows)) {
+          state.staffList = data.rows.map((n: any) => ({ label: n.nickname, value: n.id }))
+        }
+      } catch {}
+    }
+
+    onMounted(() => {
+      clientTypeArray.forEach((item: any) => {
+        if (item.value != 'BACKEND' && item.value != 'SCHOOL' && item.value != 'REPAIR') {
+          state.clientTypeList.push(item)
+        }
+      })
+      getOperationList()
+      getList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <h2>资讯管理</h2>
+        <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          onSubmit={onSubmit}
+          label-width=""
+          onSetModel={(val: any) => (state.searchForm = val)}
+        >
+          <NFormItem label="关键字" path="keyword">
+            <NInput
+              v-model={[state.searchForm.keyword, 'value']}
+              placeholder="资讯标题"
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="状态" path="status">
+            <NSelect
+              v-model={[state.searchForm.status, 'value']}
+              placeholder="请选择状态"
+              clearable
+              options={
+                [
+                  {
+                    label: '启用',
+                    value: true
+                  },
+                  {
+                    label: '停用',
+                    value: false
+                  }
+                ] as any
+              }
+            />
+          </NFormItem>
+          <NFormItem label="客户端" path="clientType">
+            <NSelect
+              clearable
+              v-model={[state.searchForm.clientType, 'value']}
+              placeholder="请选择客户端"
+              options={state.clientTypeList}
+            />
+          </NFormItem>
+          <NFormItem label="操作人" path="clientType">
+            <NSelect
+              clearable
+              v-model={[state.searchForm.updateBy, 'value']}
+              placeholder="请选择操作人"
+              options={state.staffList}
+            />
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="sysNewsInformation/save1599958516768296961"
+              onClick={() => {
+                state.visiableInfo = true
+                state.infoOperation = 'add'
+                state.infoData = {}
+              }}
+            >
+              添加资讯
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+          ></Pagination>
+        </div>
+        <NModal
+          v-model:show={state.visiableInfo}
+          preset="dialog"
+          showIcon={false}
+          title={state.infoOperation === 'add' ? '新增资讯' : '修改资讯'}
+          style={{ width: '850px' }}
+        >
+          <InformationOperation
+            type={state.infoOperation}
+            data={state.infoData}
+            clientTypeList={state.clientTypeList}
+            onClose={() => (state.visiableInfo = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </div>
+    )
+  }
+})

+ 241 - 0
src/views/content-manage/content-information/information-operation.tsx

@@ -0,0 +1,241 @@
+import {
+  NForm,
+  NFormItem,
+  NInput,
+  NSelect,
+  NSpace,
+  NButton,
+  useMessage,
+  NCascader,
+  NDatePicker,
+  NGrid,
+  NFormItemGi,
+  NRadioGroup,
+  NRadio
+} from 'naive-ui'
+import type { TreeSelectOption } from 'naive-ui'
+import { defineComponent, onMounted, PropType, provide, reactive, ref } from 'vue'
+import UploadFile from '@/components/upload-file'
+import { sysNewsInformationSave, sysNewsInformationUpdate } from '../api'
+import dayjs from 'dayjs'
+import Editor from '@/components/editor'
+import UEditor from '@/components/u-editor'
+
+export default defineComponent({
+  name: 'ad-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    },
+    clientTypeList: {
+      type: Array as PropType<Array<TreeSelectOption>>,
+      default: () => []
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      type: 'HOT_CONSULTATION',
+      clientType: null,
+      title: null,
+      coverImage: null,
+      linkUrl: null,
+      linkType: null as any,
+      content: null,
+      summary: null,
+      status: false
+    })
+
+    const btnLoading = ref(false)
+    const formsRef = ref()
+    const message = useMessage()
+
+    // 提交记录
+    const onSubmit = async () => {
+      formsRef.value.validate(async (error: any) => {
+        if (error) return
+        try {
+          btnLoading.value = true
+          if (props.type === 'add') {
+            await sysNewsInformationSave({ ...forms })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await sysNewsInformationUpdate({ ...forms, id: props.data.id })
+            message.success('修改成功')
+          }
+          emit('getList')
+          emit('close')
+        } catch {
+          //
+        }
+        setTimeout(() => {
+          btnLoading.value = false
+        }, 100)
+      })
+    }
+
+    onMounted(async () => {
+      // 初始化数据
+      if (props.type === 'edit') {
+        const data = props.data
+        forms.title = data.title
+        forms.coverImage = data.coverImage
+        forms.linkUrl = data.linkUrl
+        forms.linkType = data.linkType
+        forms.clientType = data.clientType
+        forms.content = data.content
+        forms.summary = data.summary
+        forms.status = data.status
+      }
+    })
+
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="100">
+          <NGrid cols={2}>
+            <NFormItemGi
+              label="客户端"
+              path="clientType"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择客户端'
+                }
+              ]}
+            >
+              <NSelect
+                options={props.clientTypeList}
+                placeholder="请选择客户端"
+                v-model:value={forms.clientType}
+              />
+            </NFormItemGi>
+
+            <NFormItemGi
+              label="跳转方式"
+              path="linkType"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入跳转方式'
+                }
+              ]}
+            >
+              <NRadioGroup v-model:value={forms.linkType}>
+                <NRadio value={'IN'}>内容跳转</NRadio>
+                <NRadio value={'OUT'}>外部链接</NRadio>
+              </NRadioGroup>
+            </NFormItemGi>
+            <NFormItemGi
+              label="资讯标题"
+              path="title"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入资讯标题'
+                }
+              ]}
+            >
+              <NInput
+                v-model:value={forms.title}
+                placeholder="请输入资讯标题"
+                clearable
+                showCount
+                maxlength={15}
+              />
+            </NFormItemGi>
+            {forms.linkType === 'OUT' && (
+              <NFormItemGi
+                label="跳转链接"
+                path="linkUrl"
+                rule={[
+                  {
+                    required: true,
+                    message: '请输入跳转链接'
+                  },
+                  {
+                    pattern:
+                      /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/,
+                    message: '跳转链接输入有误'
+                  }
+                ]}
+              >
+                <NInput v-model:value={forms.linkUrl} placeholder="请输入跳转链接" clearable />
+              </NFormItemGi>
+            )}
+          </NGrid>
+          <NGrid cols={2}>
+            <NFormItemGi
+              label="资讯封面"
+              path="coverImage"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入资讯封面',
+                  trigger: ['input', 'blur']
+                }
+              ]}
+            >
+              <UploadFile
+                v-model:fileList={forms.coverImage}
+                accept=".jpg,.jpeg,.png"
+                cropper
+                size={5}
+                bucketName="news-info"
+                options={{
+                  enlarge: 2,
+                  autoCropWidth: 208, //默认生成截图框宽度
+                  autoCropHeight: 144 //默认生成截图框高度
+                }}
+                tips="请上传尺寸为208*144,JPG、PNG图片"
+              />
+            </NFormItemGi>
+            <NFormItemGi label="描述" path="summary">
+              <NInput
+                placeholder="请输入描述"
+                v-model:value={forms.summary}
+                type="textarea"
+                rows={3}
+              />
+            </NFormItemGi>
+          </NGrid>
+          {forms.linkType === 'IN' && (
+            <NGrid cols={1}>
+              <NFormItemGi
+                label="内容"
+                path="content"
+                rule={[
+                  {
+                    required: true,
+                    message: '请输入内容'
+                  }
+                ]}
+              >
+                <UEditor v-model:modelValue={forms.content} bucketName="news-info"></UEditor>
+                {/* <Editor v-model:value={forms.content} height={300} bucketName="news-info" /> */}
+              </NFormItemGi>
+            </NGrid>
+          )}
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton
+            type="primary"
+            onClick={() => onSubmit()}
+            loading={btnLoading.value}
+            disabled={btnLoading.value}
+          >
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 308 - 0
src/views/content-manage/content-notice/index.tsx

@@ -0,0 +1,308 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  NButton,
+  NDataTable,
+  NDescriptions,
+  NDescriptionsItem,
+  NFormItem,
+  NImage,
+  NInput,
+  NModal,
+  NSelect,
+  NSpace,
+  NTag,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { sysNoticePage, sysNoticeRemove, sysNoticeStatus } from '../api'
+import NoticeOperation from './notice-operation'
+import { clientTypeArray } from '@/utils/searchArray'
+import { filterClientType } from '@/utils/filters'
+import TheTooltip from '@/components/TheTooltip'
+// import StationOperation from './station-operation'
+
+export default defineComponent({
+  name: 'content-ad',
+  setup() {
+    const dialog = useDialog()
+    const message = useMessage()
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        keyword: null,
+        catalogType: null,
+        status: null
+      },
+      clientTypeList: [] as any,
+      dataList: [] as any,
+      visiableAd: false,
+      adOperation: 'add',
+      adData: {} as any
+    })
+
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id',
+          render: (row: any) => (
+            <>
+              <NDescriptions labelPlacement="left" column={1}>
+                <NDescriptionsItem label="公告标题">
+                  <TheTooltip content={row.title} />{' '}
+                </NDescriptionsItem>
+                <NDescriptionsItem label="公告编号">{row.id}</NDescriptionsItem>
+              </NDescriptions>
+            </>
+          )
+        },
+        // {
+        //   title: '公告标题',
+        //   key: 'title'
+        // },
+        // {
+        //   title: '发布时间',
+        //   key: 'releaseTime'
+        // },
+        {
+          title: '状态',
+          key: 'status',
+          render(row: any) {
+            return (
+              <NTag type={row.status ? 'primary' : 'default'}>{row.status ? '启用' : '停用'}</NTag>
+            )
+          }
+        },
+        {
+          title: '客户端',
+          key: 'catalogType',
+          render(row: any) {
+            return filterClientType(row.catalogType)
+          }
+        },
+        {
+          title: '操作人',
+          key: 'updateUser'
+        },
+        {
+          title: '创建时间',
+          key: 'createTime'
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="sysNotice/update1599958936240640001"
+                  onClick={() => {
+                    state.visiableAd = true
+                    state.adOperation = 'edit'
+                    state.adData = row
+                  }}
+                >
+                  修改
+                </NButton>
+
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="sysNotice/status1599959101026455553"
+                  onClick={() => onChangeStatus(row)}
+                >
+                  {row.status ? '停用' : '启用'}
+                </NButton>
+                {!row.status && (
+                  <NButton
+                    type="primary"
+                    size="small"
+                    text
+                    //v-auth="sysNotice/remove1599959008667881473"
+                    onClick={() => onRmove(row)}
+                  >
+                    删除
+                  </NButton>
+                )}
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const onRmove = (row: any): void => {
+      dialog.warning({
+        title: '警告',
+        content: `删除"${row.title}",是否继续?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysNoticeRemove({ id: row.id })
+            getList()
+            message.success('删除成功')
+          } catch {}
+        }
+      })
+    }
+
+    const onChangeStatus = (row: any) => {
+      const statusStr = row.status ? '停用' : '启用'
+      dialog.warning({
+        title: '警告',
+        content: `是否${statusStr}?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysNoticeStatus({
+              id: row.id
+            })
+            getList()
+            message.success(`${statusStr}成功`)
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await sysNoticePage({ ...state.pagination, ...state.searchForm })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    onMounted(() => {
+      clientTypeArray.forEach((item: any) => {
+        if (item.value != 'BACKEND') {
+          state.clientTypeList.push(item)
+        }
+      })
+      getList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <h2>公告管理</h2>
+        <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          onSubmit={onSubmit}
+          onSetModel={(val: any) => (state.searchForm = val)}
+        >
+          <NFormItem label="关键字" path="keyword">
+            <NInput v-model={[state.searchForm.keyword, 'value']} placeholder="公告编号/标题" />
+          </NFormItem>
+          <NFormItem label="状态" path="status">
+            <NSelect
+              clearable
+              v-model:value={state.searchForm.status}
+              placeholder="请选择状态"
+              options={
+                [
+                  {
+                    label: '启用',
+                    value: 1
+                  },
+                  {
+                    label: '停用',
+                    value: 0
+                  }
+                ] as any
+              }
+            />
+          </NFormItem>
+          <NFormItem label="客户端" path="catalogType">
+            <NSelect
+              v-model={[state.searchForm.catalogType, 'value']}
+              placeholder="请选择客户端"
+              options={state.clientTypeList}
+            />
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="sysNotice/save1599958862773211137"
+              onClick={() => {
+                state.visiableAd = true
+                state.adOperation = 'add'
+                state.adData = {}
+              }}
+            >
+              添加公告
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+          ></Pagination>
+        </div>
+        <NModal
+          v-model:show={state.visiableAd}
+          preset="dialog"
+          showIcon={false}
+          title={state.adOperation === 'add' ? '新增公告' : '修改公告'}
+          style={{ width: '750px' }}
+        >
+          <NoticeOperation
+            type={state.adOperation}
+            data={state.adData}
+            clientTypeList={state.clientTypeList}
+            onClose={() => (state.visiableAd = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </div>
+    )
+  }
+})

+ 166 - 0
src/views/content-manage/content-notice/notice-operation.tsx

@@ -0,0 +1,166 @@
+import {
+  NForm,
+  NFormItem,
+  NInput,
+  NSelect,
+  NSpace,
+  NButton,
+  useMessage,
+  NDatePicker
+} from 'naive-ui'
+import type { TreeSelectOption } from 'naive-ui'
+import { defineComponent, onMounted, PropType, reactive, ref } from 'vue'
+import { sysNoticeSave, sysNoticeUpdate } from '../api'
+import dayjs from 'dayjs'
+import Editor from '@/components/editor'
+
+export default defineComponent({
+  name: 'ad-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    },
+    clientTypeList: {
+      type: Array as PropType<Array<TreeSelectOption>>,
+      default: () => []
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      title: null,
+      catalogType: null,
+      // releaseTime: null as any,
+      content: null,
+      status: false
+    })
+
+    const btnLoading = ref(false)
+    const formsRef = ref()
+    const message = useMessage()
+
+    // 提交记录
+    const onSubmit = async () => {
+      formsRef.value.validate(async (error: any) => {
+        if (error) return
+        try {
+          let { ...res } = forms
+
+          btnLoading.value = true
+          if (props.type === 'add') {
+            await sysNoticeSave({ ...res })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await sysNoticeUpdate({ ...res, id: props.data.id })
+            message.success('修改成功')
+          }
+          emit('getList')
+          emit('close')
+        } catch {
+          //
+        }
+        setTimeout(() => {
+          btnLoading.value = false
+        }, 100)
+      })
+    }
+
+    // onMounted(async () => {
+    // 初始化数据
+    if (props.type === 'edit') {
+      const data = props.data
+      forms.title = data.title
+      forms.content = data.content
+      forms.catalogType = data.catalogType
+      forms.status = data.status
+      // forms.releaseTime = data.releaseTime ? dayjs(data.releaseTime).valueOf() : null
+    }
+    // })
+
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="auto">
+          <NFormItem
+            label="客户端"
+            path="catalogType"
+            rule={[
+              {
+                required: true,
+                message: '请选择客户端'
+              }
+            ]}
+          >
+            <NSelect
+              options={props.clientTypeList}
+              placeholder="请选择客户端"
+              v-model:value={forms.catalogType}
+            />
+          </NFormItem>
+          <NFormItem
+            label="公告标题"
+            path="title"
+            rule={[
+              {
+                required: true,
+                message: '请输入公告标题'
+              }
+            ]}
+          >
+            <NInput
+              v-model:value={forms.title}
+              placeholder="请输入公告标题"
+              clearable
+              showCount
+              maxlength={15}
+            />
+          </NFormItem>
+
+          {/* <NFormItem label="发布时间" path="releaseTime">
+            <NDatePicker
+              v-model:value={forms.releaseTime}
+              type="datetime"
+              placeholder="请选择发布时间"
+              clearable
+              style={{ width: '100%', textAlign: 'left' }}
+              isDateDisabled={(ts: number) => {
+                return ts < dayjs(dayjs().format('YYYY-MM-DD')).valueOf()
+              }}
+            />
+          </NFormItem> */}
+          <NFormItem
+            label="内容"
+            path="content"
+            rule={[
+              {
+                required: true,
+                message: '请输入内容',
+                trigger: ['change', 'blur']
+              }
+            ]}
+          >
+            <Editor v-model:value={forms.content} bucketName="news-info" />
+          </NFormItem>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton
+            type="primary"
+            onClick={() => onSubmit()}
+            loading={btnLoading.value}
+            disabled={btnLoading.value}
+          >
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 151 - 0
src/views/content-manage/help-center/component/help-center-category.tsx

@@ -0,0 +1,151 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import { NButton, NDataTable, NModal, NSpace, useDialog, useMessage } from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { helpCenterCatalogPage, helpCenterCatalogRemove } from '../../api'
+import HelpCenterCategoryOperation from '../modal/help-center-category-operation'
+
+export default defineComponent({
+  name: 'content-flash',
+  setup() {
+    const dialog = useDialog()
+    const message = useMessage()
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      dataList: [] as any,
+      visiableCategory: false,
+      categoryOperation: 'add',
+      categoryData: {} as any
+    })
+
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '名称',
+          key: 'name'
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="helpCenterCatalog/update1599694974123089922"
+                  onClick={() => {
+                    state.visiableCategory = true
+                    state.categoryOperation = 'edit'
+                    state.categoryData = row
+                  }}
+                >
+                  修改
+                </NButton>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  onClick={() => onRmove(row)}
+                  //v-auth="helpCenterCatalog/remove1599695049448595458"
+                >
+                  删除
+                </NButton>
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const onRmove = (row: any): void => {
+      console.log(row, 'row')
+      dialog.warning({
+        title: '警告',
+        content: `是否删除该数据?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await helpCenterCatalogRemove({ id: row.id })
+            getList()
+            message.success('删除成功')
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await helpCenterCatalogPage({ ...state.pagination })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="helpCenterCatalog/save1599694895257591809"
+              onClick={() => {
+                state.visiableCategory = true
+                state.categoryOperation = 'add'
+                state.categoryData = {}
+              }}
+            >
+              添加分类
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+            saveKey="help-center-category"
+          ></Pagination>
+        </div>
+        <NModal
+          v-model:show={state.visiableCategory}
+          preset="dialog"
+          showIcon={false}
+          title={state.categoryOperation === 'add' ? '新增帮助分类' : '修改帮助分类'}
+          style={{ width: '500px' }}
+        >
+          <HelpCenterCategoryOperation
+            type={state.categoryOperation}
+            data={state.categoryData}
+            onClose={() => (state.visiableCategory = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </div>
+    )
+  }
+})

+ 318 - 0
src/views/content-manage/help-center/component/help-center.tsx

@@ -0,0 +1,318 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  NButton,
+  NDataTable,
+  NFormItem,
+  NInput,
+  NModal,
+  NSelect,
+  NSpace,
+  NTag,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { filterClientType } from '@/utils/filters'
+import {
+  helpCenterCatalogPage,
+  helpCenterContentPage,
+  helpCenterContentRemove,
+  helpCenterContentStatus
+} from '../../api'
+import HelpCenterOperation from '../modal/help-center-operation'
+import { clientTypeArray } from '@/utils/searchArray'
+import TheTooltip from '@/components/TheTooltip'
+
+export default defineComponent({
+  name: 'content-flash',
+  setup() {
+    const dialog = useDialog()
+    const message = useMessage()
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        catalogType: null,
+        catalogId: null,
+        keyword: null,
+        status: null
+      },
+      clientTypeList: [] as any,
+      dataList: [] as any,
+      catalogList: [] as any,
+      visiableHelp: false,
+      helpOperation: 'add',
+      helpData: {} as any
+    })
+
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '标题',
+          key: 'title',
+          render(row: any) {
+            return <TheTooltip content={row.title} />
+          }
+        },
+        {
+          title: '分类',
+          key: 'catalogName',
+          render(row: any) {
+            return <TheTooltip content={row.catalogName} />
+          }
+        },
+        {
+          title: '客户端',
+          key: 'catalogType',
+          render(row: any) {
+            return filterClientType(row.catalogType)
+          }
+        },
+        {
+          title: '状态',
+          key: 'status',
+          render(row: any) {
+            return (
+              <NTag type={row.status ? 'info' : 'default'}>{row.status ? '启用' : '停用'}</NTag>
+            )
+          }
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="helpCenterContent/update1599694657725767681"
+                  onClick={() => {
+                    state.visiableHelp = true
+                    state.helpOperation = 'edit'
+                    state.helpData = row
+                  }}
+                >
+                  修改
+                </NButton>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="helpCenterContent/status1611349854722027521"
+                  onClick={() => onChangeStatus(row)}
+                >
+                  {row.status ? '停用' : '启用'}
+                </NButton>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  onClick={() => onRmove(row)}
+                  //v-auth="helpCenterContent/remove1599694768375701505"
+                >
+                  删除
+                </NButton>
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const onChangeStatus = (row: any) => {
+      const statusStr = row.status ? '停用' : '启用'
+      dialog.warning({
+        title: '警告',
+        content: `是否${statusStr}?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await helpCenterContentStatus({
+              id: row.id,
+              status: row.status ? false : true
+            })
+            getList()
+            message.success(`${statusStr}成功`)
+          } catch {}
+        }
+      })
+    }
+
+    const onRmove = (row: any): void => {
+      dialog.warning({
+        title: '警告',
+        content: `删除"${row.title}",是否继续?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await helpCenterContentRemove({ id: row.id })
+            getList()
+            message.success('删除成功')
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await helpCenterContentPage({ ...state.pagination, ...state.searchForm })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    const getHelpCenterCategory = async () => {
+      try {
+        const { data } = await helpCenterCatalogPage({ page: 1, rows: 999 })
+        state.catalogList = data.rows || []
+      } catch {}
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    onMounted(() => {
+      clientTypeArray.forEach((item: any) => {
+        if (item.value != 'BACKEND') {
+          state.clientTypeList.push(item)
+        }
+      })
+      getList()
+      getHelpCenterCategory()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          onSubmit={onSubmit}
+          label-width=""
+          onSetModel={(val: any) => (state.searchForm = val)}
+        >
+          <NFormItem label="标题" path="keyword">
+            <NInput clearable v-model:value={state.searchForm.keyword} placeholder="请输入标题" />
+          </NFormItem>
+          <NFormItem label="分类" path="catalogId">
+            <NSelect
+              clearable
+              v-model:value={state.searchForm.catalogId}
+              placeholder="请选择分类"
+              options={state.catalogList}
+              valueField="id"
+              labelField="name"
+            />
+          </NFormItem>
+          <NFormItem label="状态" path="status">
+            <NSelect
+              clearable
+              v-model:value={state.searchForm.status}
+              placeholder="请选择状态"
+              options={
+                [
+                  {
+                    label: '启用',
+                    value: 1
+                  },
+                  {
+                    label: '停用',
+                    value: 0
+                  }
+                ] as any
+              }
+            />
+          </NFormItem>
+          <NFormItem label="客户端" path="catalogType">
+            <NSelect
+              clearable
+              v-model={[state.searchForm.catalogType, 'value']}
+              placeholder="请选择客户端"
+              options={state.clientTypeList}
+            />
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="helpCenterContent/save1599694573994876929"
+              onClick={() => {
+                state.visiableHelp = true
+                state.helpOperation = 'add'
+                state.helpData = {}
+              }}
+            >
+              添加帮助
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+            saveKey="help-center"
+          ></Pagination>
+        </div>
+        <NModal
+          v-model:show={state.visiableHelp}
+          preset="dialog"
+          showIcon={false}
+          title={state.helpOperation === 'add' ? '新增帮助' : '修改帮助'}
+          style={{ width: '850px' }}
+        >
+          <HelpCenterOperation
+            type={state.helpOperation}
+            data={state.helpData}
+            catalogList={state.catalogList}
+            clientTypeList={state.clientTypeList}
+            onClose={() => (state.visiableHelp = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </div>
+    )
+  }
+})

+ 55 - 0
src/views/content-manage/help-center/index.tsx

@@ -0,0 +1,55 @@
+import { NTabPane, NTabs } from 'naive-ui'
+import { defineComponent, h, reactive, resolveDynamicComponent } from 'vue'
+import { useRoute } from 'vue-router'
+import HelpCenter from './component/help-center'
+import HelpCenterCategory from './component/help-center-category'
+import { getTabsCache, setTabsCaches } from '@/hooks/use-async'
+export default defineComponent({
+  name: 'city-manage',
+  setup() {
+    const state = reactive({
+      tabName: 'HelpCenter' as 'HelpCenter' | 'HelpCenterCategory'
+    })
+    const route = useRoute()
+    getTabsCache((val: any) => {
+      if (val.form.tabName) {
+        state.tabName = val.form.tabName
+      }
+    })
+    const setTabs = (val: any) => {
+      setTabsCaches(val, 'tabName', route)
+    }
+    return () => {
+      // const Component = resolveDynamicComponent(state.componentName)
+      return (
+        <div class="system-menu-container">
+          <h2>帮助中心</h2>
+
+          <div class={['section-container']} style="padding-top: 0">
+            <NTabs
+              type="line"
+              size="large"
+              v-model:value={state.tabName}
+              onUpdate:value={(val: any) => setTabs(val)}
+            >
+              <NTabPane
+                name="HelpCenter"
+                tab="帮助列表"
+                //v-auth="helpCenterContent/page1599594492578836481"
+              >
+                <HelpCenter />
+              </NTabPane>
+              <NTabPane
+                name="HelpCenterCategory"
+                tab="帮助分类"
+                //v-auth="helpCenterCatalog/page1599594656550957058"
+              >
+                <HelpCenterCategory />
+              </NTabPane>
+            </NTabs>
+          </div>
+        </div>
+      )
+    }
+  }
+})

+ 102 - 0
src/views/content-manage/help-center/modal/help-center-category-operation.tsx

@@ -0,0 +1,102 @@
+import { NForm, NFormItem, NInput, NSelect, NSpace, NButton, useMessage, NCascader } from 'naive-ui'
+import type { TreeSelectOption } from 'naive-ui'
+import { defineComponent, onMounted, PropType, reactive, ref } from 'vue'
+import { helpCenterCatalogSave, helpCenterCatalogUpdate } from '../../api'
+
+export default defineComponent({
+  name: 'city-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    },
+    areaList: {
+      type: Array as PropType<Array<TreeSelectOption>>,
+      default: () => []
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      name: null,
+      parentId: 0
+    })
+
+    const btnLoading = ref(false)
+    const formsRef = ref()
+    const subsidyList = ref<any>([])
+    const message = useMessage()
+
+    // 提交记录
+    const onSubmit = async () => {
+      formsRef.value.validate(async (error: any) => {
+        if (error) return
+        try {
+          btnLoading.value = true
+          if (props.type === 'add') {
+            await helpCenterCatalogSave({ ...forms })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await helpCenterCatalogUpdate({ ...forms, id: props.data.id })
+            message.success('修改成功')
+          }
+          emit('getList')
+          emit('close')
+          setTimeout(() => {
+            btnLoading.value = false
+          }, 100)
+        } catch {
+          //
+        }
+      })
+    }
+
+    onMounted(async () => {
+      // 初始化数据
+      if (props.type === 'edit') {
+        const data = props.data
+        forms.name = data.name
+        forms.parentId = data.parentId
+      }
+    })
+
+    // 只能输入数字
+    const onlyAllowNumber = (value: string) => !value || /^\d+$/.test(value)
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="auto">
+          <NFormItem
+            label="分类名称"
+            path="name"
+            rule={[
+              {
+                required: true,
+                message: '请输入分类名称'
+              }
+            ]}
+          >
+            <NInput v-model:value={forms.name} placeholder="请输入分类名称" clearable></NInput>
+          </NFormItem>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton
+            type="primary"
+            onClick={() => onSubmit()}
+            loading={btnLoading.value}
+            disabled={btnLoading.value}
+          >
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 163 - 0
src/views/content-manage/help-center/modal/help-center-operation.tsx

@@ -0,0 +1,163 @@
+import { NForm, NFormItem, NInput, NSelect, NSpace, NButton, useMessage, NCascader } from 'naive-ui'
+import type { TreeSelectOption } from 'naive-ui'
+import { defineComponent, onMounted, PropType, reactive, ref } from 'vue'
+import { helpCenterContentSave, helpCenterContentUpdate } from '../../api'
+import Editor from '@/components/editor'
+
+export default defineComponent({
+  name: 'city-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    },
+    catalogList: {
+      type: Array as PropType<Array<TreeSelectOption>>,
+      default: () => []
+    },
+    clientTypeList: {
+      type: Array as PropType<Array<string>>,
+      default: () => []
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      title: null,
+      catalogId: null,
+      content: null,
+      catalogType: null
+    })
+
+    const btnLoading = ref(false)
+    const formsRef = ref()
+    const message = useMessage()
+
+    // 提交记录
+    const onSubmit = async () => {
+      formsRef.value.validate(async (error: any) => {
+        if (error) return
+        try {
+          btnLoading.value = true
+          if (props.type === 'add') {
+            await helpCenterContentSave({ ...forms })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await helpCenterContentUpdate({ ...forms, id: props.data.id })
+            message.success('修改成功')
+          }
+          emit('getList')
+          emit('close')
+        } catch {
+          //
+        }
+        setTimeout(() => {
+          btnLoading.value = false
+        }, 100)
+      })
+    }
+
+    // 初始化数据
+    if (props.type === 'edit') {
+      const data = props.data
+      forms.title = data.title
+      forms.catalogId = data.catalogId
+      forms.content = data.content
+      forms.catalogType = data.catalogType
+    }
+
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="auto">
+          <NFormItem
+            label="标题"
+            path="title"
+            rule={[
+              {
+                required: true,
+                message: '请输入标题',
+                trigger: ['input', 'blur']
+              }
+            ]}
+          >
+            <NInput
+              v-model:value={forms.title}
+              placeholder="请输入标题"
+              clearable
+              showCount
+              maxlength={30}
+            />
+          </NFormItem>
+          <NFormItem
+            label="分类"
+            path="catalogId"
+            rule={[
+              {
+                required: true,
+                message: '请选择分类',
+                trigger: ['input', 'blur']
+              }
+            ]}
+          >
+            <NSelect
+              v-model:value={forms.catalogId}
+              options={props.catalogList}
+              placeholder="请选择分类"
+              valueField="id"
+              labelField="name"
+              clearable
+            ></NSelect>
+          </NFormItem>
+          <NFormItem
+            label="客户端"
+            path="catalogType"
+            rule={[
+              {
+                required: true,
+                message: '请选择客户端',
+                trigger: ['input', 'blur']
+              }
+            ]}
+          >
+            <NSelect
+              v-model:value={forms.catalogType}
+              options={props.clientTypeList as any}
+              placeholder="请选择客户端"
+              clearable
+            ></NSelect>
+          </NFormItem>
+          <NFormItem
+            label="内容"
+            path="content"
+            rule={[
+              {
+                required: true,
+                message: '请输入内容'
+              }
+            ]}
+          >
+            <Editor v-model:value={forms.content} bucketName="news-info" />
+          </NFormItem>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton
+            type="primary"
+            onClick={() => onSubmit()}
+            loading={btnLoading.value}
+            disabled={btnLoading.value}
+          >
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 466 - 0
src/views/content-manage/music-manage/component/music-list.tsx

@@ -0,0 +1,466 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  NButton,
+  NDataTable,
+  NFormItem,
+  NImage,
+  NInput,
+  NModal,
+  NSelect,
+  NSpace,
+  NTag,
+  useDialog,
+  useMessage,
+  NCascader,
+  NDescriptions,
+  NDescriptionsItem
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref, watch } from 'vue'
+import { musicSheetPage, musicSheetRemove, musicSheetStatus, musicTagPage } from '../../api'
+import { getMusicSheetCategories } from '@/views/music-categrory/api'
+import MusicOperation from '../modal/music-operation'
+import { subjectBasicConfigPage } from '@/views/system-manage/api'
+import { accompanimentTypeArray, audioTypeArray } from '@/utils/searchArray'
+import MusicPreView from '../modal/musicPreView'
+import TheTooltip from '@/components/TheTooltip'
+import { filterPointCategory } from '@/views/teaching-manage/unit-test'
+
+export default defineComponent({
+  name: 'content-flash',
+  props: ['searchId'],
+  setup(props, { emit }) {
+    const dialog = useDialog()
+    const message = useMessage()
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        keyword: null,
+        musicTag: null,
+        musicSubject: null,
+        audioType: null,
+        accompanimentType: null,
+        status: null,
+        topFlag: null,
+        musicSheetCategoriesId: props.searchId
+      },
+      dataList: [] as any,
+      subjectList: [] as any,
+      tagList: [] as any,
+      visiableMusic: false,
+      musicOperation: 'add',
+      musicData: {} as any,
+      musicSheetCategories: [] as any,
+      musicPreview: false,
+      musicScore: null as any
+    })
+
+    const columns = (): any => {
+      return [
+        {
+          title: '曲目名称',
+          key: 'id',
+          render: (row: any) => (
+            <>
+              <NDescriptions labelPlacement="left" column={1}>
+                <NDescriptionsItem label="曲目名称">
+                  <TheTooltip content={row.musicSheetName} />{' '}
+                </NDescriptionsItem>
+                <NDescriptionsItem label="曲目编号">{row.id}</NDescriptionsItem>
+              </NDescriptions>
+            </>
+          )
+        },
+        {
+          title: '曲目封面',
+          key: 'titleImg',
+          render(row: any) {
+            return <NImage width={60} height={60} src={row.titleImg} />
+          }
+        },
+        {
+          title: '曲目信息',
+          key: 'musicSheetCategoriesName',
+          render: (row: any) => (
+            <>
+              <NDescriptions labelPlacement="left" column={1}>
+                <NDescriptionsItem label="曲目分类">
+                  <TheTooltip content={row.musicSheetCategoriesName} />{' '}
+                </NDescriptionsItem>
+                <NDescriptionsItem label="伴奏类型">
+                  {row.accompanimentType === 'HOMEMODE' ? '自制伴奏' : '普通伴奏'}
+                </NDescriptionsItem>
+              </NDescriptions>
+            </>
+          )
+        },
+        {
+          title: '可用声部',
+          key: 'musicSubject',
+          render(row: any) {
+            return <NTag type="primary">{row.musicSheetType === 'CONCERT' ? '合奏' :row.musicSubject}</NTag>
+          }
+        },
+        {
+          title: '播放类型',
+          key: 'audioType',
+          render(row: any) {
+            return (
+              <>
+                {row.audioType === 'MP3' && <NTag type="primary">MP3</NTag>}
+                {row.audioType === 'MIDI' && <NTag type="default">MIDI</NTag>}
+              </>
+            )
+          }
+        },
+        {
+          title: '能否转简谱',
+          key: 'notation',
+          render(row: any) {
+            return (
+              <NTag type={row.notation ? 'primary' : 'default'}>{row.notation ? '是' : '否'}</NTag>
+            )
+          }
+        },
+        {
+          title: '状态',
+          key: 'status',
+          render(row: any) {
+            return (
+              <NTag type={row.status ? 'primary' : 'default'}>{row.status ? '启用' : '停用'}</NTag>
+            )
+          }
+        },
+        {
+          title: '排序',
+          key: 'sortNumber'
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          fixed: 'right',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  onClick={() => {
+                    state.musicPreview = true
+                    state.musicScore = row
+                  }}
+                >
+                  预览
+                </NButton>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="musicSheet/status1612431726029942786"
+                  onClick={() => onChangeStatus(row)}
+                >
+                  {row.status ? '停用' : '启用'}
+                </NButton>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="musicSheet/update1602302618558099458"
+                  onClick={() => {
+                    state.visiableMusic = true
+                    state.musicOperation = 'edit'
+                    state.musicData = row
+                  }}
+                >
+                  修改
+                </NButton>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  onClick={() => onRmove(row)}
+                  //v-auth="musicSheet/remove1602302689404088321"
+                >
+                  删除
+                </NButton>
+                {/* <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  onClick={() => onRmove(row)}
+                  v-auth="musicSheet/remove1599694768375701505"
+                >
+                  {row.topFlag ? '取消置顶' : '置顶'}
+                </NButton> */}
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const onChangeStatus = (row: any) => {
+      const statusStr = row.status ? '停用' : '启用'
+      dialog.warning({
+        title: '警告',
+        content: `是否${statusStr}?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await musicSheetStatus({
+              id: row.id
+            })
+            getList()
+            message.success(`${statusStr}成功`)
+          } catch {}
+        }
+      })
+    }
+
+    const onRmove = (row: any): void => {
+      dialog.warning({
+        title: '警告',
+        content: `删除"${row.musicSheetName}",是否继续?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await musicSheetRemove({ id: row.id })
+            getList()
+            message.success('删除成功')
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await musicSheetPage({ ...state.pagination, ...state.searchForm })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    // 获取标签
+    const getTagList = async () => {
+      try {
+        const { data } = await musicTagPage({ page: 1, rows: 999 })
+        const tempList = data.rows || []
+        tempList.forEach((item: any) => {
+          item.label = item.name
+          item.value = item.id
+        })
+        state.tagList = tempList
+      } catch {}
+    }
+
+    // 获取分类
+    const getMusicSheetCategorieList = async () => {
+      try {
+        const { data } = await getMusicSheetCategories({ enable: true })
+        state.musicSheetCategories = filterPointCategory(data, 'musicSheetCategoriesList')
+      } catch (e) {}
+    }
+
+    // 获取声部
+    const getSubjectList = async () => {
+      try {
+        const { data } = await subjectBasicConfigPage({ page: 1, rows: 999 })
+        const tempList = data.rows || []
+        tempList.forEach((item: any) => {
+          item.label = item.subjectName
+          item.value = item.subjectId
+        })
+        state.subjectList = tempList
+      } catch {}
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+    watch(
+      () => props.searchId,
+      (val) => {
+        console.log(val, 'searchId')
+      }
+    )
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    onMounted(() => {
+      // getTagList()
+      console.log()
+      if (props.searchId) {
+        state.searchForm.musicSheetCategoriesId = props.searchId || null
+      }
+
+      getSubjectList()
+      getList()
+      getMusicSheetCategorieList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          onSubmit={onSubmit}
+          saveKey="music-list"
+          onSetModel={(val: any) => (state.searchForm = val)}
+        >
+          <NFormItem label="关键词" path="keyword">
+            <NInput
+              placeholder="曲目编号/名称"
+              v-model:value={state.searchForm.keyword}
+              clearable
+            />
+          </NFormItem>
+          {/* <NFormItem label="标签" path="musicTag">
+            <NSelect
+              placeholder="请选择标签"
+              v-model:value={state.searchForm.musicTag}
+              options={state.tagList}
+              clearable
+            />
+          </NFormItem> */}
+          <NFormItem label="分类" path="musicSheetCategoriesId">
+            <NCascader
+              valueField="id"
+              labelField="name"
+              children-field="musicSheetCategoriesList"
+              placeholder="请选择分类"
+              v-model:value={state.searchForm.musicSheetCategoriesId}
+              options={state.musicSheetCategories}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="声部" path="musicSubject">
+            <NSelect
+              placeholder="请选择声部"
+              v-model:value={state.searchForm.musicSubject}
+              options={state.subjectList}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="播放类型" path="audioType">
+            <NSelect
+              placeholder="请选择播放类型"
+              v-model:value={state.searchForm.audioType}
+              options={audioTypeArray}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="状态" path="status">
+            <NSelect
+              v-model={[state.searchForm.status, 'value']}
+              placeholder="请选择状态"
+              clearable
+              options={
+                [
+                  {
+                    label: '启用',
+                    value: true
+                  },
+                  {
+                    label: '停用',
+                    value: false
+                  }
+                ] as any
+              }
+            />
+          </NFormItem>
+          {/* <NFormItem label="伴奏类型" path="accompanimentType">
+            <NSelect
+              placeholder="请选择伴奏类型"
+              v-model:value={state.searchForm.accompanimentType}
+              options={accompanimentTypeArray}
+              clearable
+            />
+          </NFormItem> */}
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="musicSheet/save1602302550719426561"
+              onClick={() => {
+                state.visiableMusic = true
+                state.musicOperation = 'add'
+                state.musicData = {}
+              }}
+            >
+              添加曲目
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+            saveKey="music-list"
+          ></Pagination>
+        </div>
+        <NModal
+          v-model:show={state.visiableMusic}
+          preset="dialog"
+          showIcon={false}
+          title={state.musicOperation === 'add' ? '新增曲目' : '修改曲目'}
+          style={{ width: '950px' }}
+        >
+          <MusicOperation
+            type={state.musicOperation}
+            data={state.musicData}
+            subjectList={state.subjectList}
+            musicSheetCategories={state.musicSheetCategories}
+            // tagList={state.tagList}
+            onClose={() => (state.visiableMusic = false)}
+            onGetList={getList}
+          />
+        </NModal>
+        <NModal
+          blockScroll={true}
+          v-model:show={state.musicPreview}
+          preset="dialog"
+          showIcon={false}
+          title={'曲目预览'}
+          style={{ width: 'auto' }}
+        >
+          <MusicPreView item={state.musicScore} />
+        </NModal>
+      </div>
+    )
+  }
+})

+ 262 - 0
src/views/content-manage/music-manage/component/tag-list.tsx

@@ -0,0 +1,262 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  NButton,
+  NDataTable,
+  NFormItem,
+  NInput,
+  NModal,
+  NSpace,
+  NTag,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { musicTagPage, musicTagRemove, musicTagState } from '../../api'
+import TagOperation from '../modal/tag-operation'
+
+export default defineComponent({
+  name: 'content-flash',
+  setup() {
+    const dialog = useDialog()
+    const message = useMessage()
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        keyword: null
+      },
+      dataList: [] as any,
+      visiableTag: false,
+      tagOperation: 'add',
+      tagData: {} as any
+    })
+
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '名称',
+          key: 'name'
+        },
+        // {
+        //   title: '平台乐谱启用/总数',
+        //   key: 'enablePlatformMusicSheetNum',
+        //   render(row: any) {
+        //     return `${row.enablePlatformMusicSheetNum}/${row.musicPlatformSheetNum}`
+        //   }
+        // },
+        // {
+        //   title: '老师乐谱启用/总数',
+        //   key: 'enableTeacherMusicSheetNum',
+        //   render(row: any) {
+        //     return `${row.enableTeacherMusicSheetNum}/${row.musicTeacherSheetNum}`
+        //   }
+        // },
+        {
+          title: '更新人',
+          key: 'updateUser'
+        },
+        {
+          title: '更新时间',
+          key: 'updateTime'
+        },
+        {
+          title: '排序',
+          key: 'sortNumber'
+        },
+        {
+          title: '状态',
+          key: 'state',
+          render(row: any) {
+            return (
+              <NTag type={row.state ? 'primary' : 'default'}>{row.state ? '启用' : '停用'}</NTag>
+            )
+          }
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  onClick={() => onChangeState(row)}
+                  //v-auth="musicTag/state1602302067191672833"
+                >
+                  {row.state ? '停用' : '启用'}
+                </NButton>
+
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="musicTag/update1602301883418243073"
+                  onClick={() => {
+                    state.visiableTag = true
+                    state.tagOperation = 'edit'
+                    state.tagData = row
+                  }}
+                >
+                  修改
+                </NButton>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  onClick={() => onRmove(row)}
+                  //v-auth="musicTag/remove1602301979954343937"
+                >
+                  删除
+                </NButton>
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const onChangeState = (row: any) => {
+      // musicTag/state
+      const statusStr = row.state ? '停用' : '启用'
+      dialog.warning({
+        title: '警告',
+        content: `是否${statusStr}?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await musicTagState({
+              id: row.id
+            })
+            getList()
+            message.success(`${statusStr}成功`)
+          } catch {}
+        }
+      })
+    }
+
+    const onRmove = (row: any): void => {
+      dialog.warning({
+        title: '警告',
+        content: `删除"${row.name}",是否继续?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await musicTagRemove({ id: row.id })
+            getList()
+            message.success('删除成功')
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await musicTagPage({ ...state.pagination, ...state.searchForm })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          onSubmit={onSubmit}
+          saveKey="tag-list"
+          onSetModel={(val: any) => (state.searchForm = val)}
+        >
+          <NFormItem label="名称" path="keyword">
+            <NInput placeholder="请输入名称" v-model:value={state.searchForm.keyword} clearable />
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="musicTag/save1602301806310158338"
+              onClick={() => {
+                state.visiableTag = true
+                state.tagOperation = 'add'
+                state.tagData = {}
+              }}
+            >
+              添加曲目标签
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+            saveKey="tag-list"
+          ></Pagination>
+        </div>
+        <NModal
+          v-model:show={state.visiableTag}
+          preset="dialog"
+          showIcon={false}
+          title={state.tagOperation === 'add' ? '新增曲目标签' : '修改曲目标签'}
+          style={{ width: '500px' }}
+        >
+          <TagOperation
+            type={state.tagOperation}
+            data={state.tagData}
+            onClose={() => (state.visiableTag = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </div>
+    )
+  }
+})

+ 62 - 0
src/views/content-manage/music-manage/index.tsx

@@ -0,0 +1,62 @@
+import { NTabPane, NTabs } from 'naive-ui'
+import { defineComponent, h, reactive, resolveDynamicComponent } from 'vue'
+import MusicList from './component/music-list'
+import TagList from './component/tag-list'
+import CategroryList from '@/views/music-categrory/index'
+import { useRoute } from 'vue-router'
+import { getTabsCache, setTabsCaches } from '@/hooks/use-async'
+export default defineComponent({
+  name: 'city-manage',
+  setup() {
+    const state = reactive({
+      tabName: 'MusicList' as 'MusicList' | 'TagList' | 'CategroryList',
+      searchId: null
+    })
+    const route = useRoute()
+    getTabsCache((val: any) => {
+      if (val.form.tabName) {
+        state.tabName = val.form.tabName
+      }
+    })
+    const setTabName = (val: any) => {
+      console.log('setTabName', val)
+      state.tabName = val.tabName
+      state.searchId = val.id
+    }
+    const setTabs = (val: any) => {
+      setTabsCaches(val, 'tabName', route)
+    }
+    return () => {
+      return (
+        <div class="system-menu-container">
+          <h2>乐谱管理</h2>
+
+          <div class={['section-container']} style="padding-top: 0">
+            <NTabs
+              type="line"
+              size="large"
+              v-model:value={state.tabName}
+              onUpdate:value={(val: any) => setTabs(val)}
+            >
+              <NTabPane name="MusicList" tab="曲目管理"
+                  //v-auth="musicSheet/page1602301588206350338"
+              >
+                <MusicList searchId={state.searchId} />
+              </NTabPane>
+              {/* <NTabPane name="TagList" tab="曲目标签管理" v-auth="musicTag/page1602301689389740033">
+                <TagList />
+              </NTabPane> */}
+              <NTabPane
+                name="CategroryList"
+                tab="曲目分类管理"
+                //v-auth="/musicCategrory1607664813521346561"
+              >
+                <CategroryList onSetTabName={setTabName} />
+              </NTabPane>
+            </NTabs>
+          </div>
+        </div>
+      )
+    }
+  }
+})

+ 20 - 0
src/views/content-manage/music-manage/modal/index.module.less

@@ -0,0 +1,20 @@
+.audioSection {
+  position: relative;
+  background: #f7f7f7;
+  padding-right: 20px;
+  border-radius: 12px;
+  padding-top: 24px;
+  margin-bottom: 12px;
+
+  .btnRemove {
+    position: absolute;
+    bottom: 12px;
+    right: 20px;
+  }
+}
+
+.formContainer{
+  max-height: 80vh;
+  overflow-y: auto;
+  padding: 0 10px;
+}

+ 976 - 0
src/views/content-manage/music-manage/modal/music-operation.tsx

@@ -0,0 +1,976 @@
+import {
+  NForm,
+  NFormItem,
+  NInput,
+  NSelect,
+  NSpace,
+  NButton,
+  useMessage,
+  NRadioGroup,
+  NRadio,
+  NGrid,
+  NFormItemGi,
+  NInputNumber,
+  NGi,
+  useDialog,
+  NCascader,
+  NAlert,
+  NInputGroup,
+  NInputGroupLabel
+} from 'naive-ui'
+import type { SelectOption } from 'naive-ui'
+import { defineComponent, onMounted, PropType, reactive, ref } from 'vue'
+import { musicSheetDetail, musicSheetSave, musicSheetUpdate } from '../../api'
+import UploadFile from '@/components/upload-file'
+import styles from './index.module.less'
+import deepClone from '@/utils/deep.clone'
+import axios from 'axios'
+
+/**
+ * 获取指定元素下一个Note元素
+ * @param ele 指定元素
+ * @param selectors 选择器
+ */
+const getNextNote = (ele: any, selectors: any) => {
+  let index = 0
+  const parentEle = ele.closest(selectors)
+  let pointer = parentEle
+  const measure = parentEle?.closest('measure')
+  let siblingNote = null
+  // 查找到相邻的第一个note元素
+  while (!siblingNote && index < (measure?.childNodes.length || 50)) {
+    index++
+    if (pointer?.nextElementSibling?.tagName === 'note') {
+      siblingNote = pointer?.nextElementSibling
+    }
+    pointer = pointer?.nextElementSibling
+  }
+  return siblingNote
+}
+
+export const onlyVisible = (xml: any, partIndex: any) => {
+  if (!xml) return ''
+  const xmlParse = new DOMParser().parseFromString(xml, 'text/xml')
+  const partList =
+    xmlParse.getElementsByTagName('part-list')?.[0]?.getElementsByTagName('score-part') || []
+  const parts = xmlParse.getElementsByTagName('part')
+  const visiblePartInfo = partList[partIndex]
+  if (visiblePartInfo) {
+    const id = visiblePartInfo.getAttribute('id')
+    Array.from(parts).forEach((part) => {
+      if (part && part.getAttribute('id') !== id) {
+        part.parentNode?.removeChild(part)
+        // 不等于第一行才添加避免重复添加
+      }
+
+      // 最后一个小节的结束线元素不在最后 调整
+      if (part && part.getAttribute('id') === id) {
+        const barlines = part.getElementsByTagName('barline')
+        const lastParent = barlines[barlines.length - 1]?.parentElement
+        if (lastParent?.lastElementChild?.tagName !== 'barline') {
+          const children: any[] = (lastParent?.children as any) || []
+          for (let el of children) {
+            if (el.tagName === 'barline') {
+              // 将结束线元素放到最后
+              lastParent?.appendChild(el)
+              break
+            }
+          }
+        }
+      }
+    })
+    Array.from(partList).forEach((part) => {
+      if (part && part.getAttribute('id') !== id) {
+        part.parentNode?.removeChild(part)
+      }
+    })
+    // 处理装饰音问题
+    const notes = xmlParse.getElementsByTagName('note')
+    const getNextvNoteDuration = (i: any) => {
+      let nextNote = notes[i + 1]
+      // 可能存在多个装饰音问题,取下一个非装饰音时值
+      for (let index = i; index < notes.length; index++) {
+        const note = notes[index]
+        if (!note.getElementsByTagName('grace')?.length) {
+          nextNote = note
+          break
+        }
+      }
+      const nextNoteDuration = nextNote?.getElementsByTagName('duration')[0]
+      return nextNoteDuration
+    }
+    Array.from(notes).forEach((note, i) => {
+      const graces = note.getElementsByTagName('grace')
+      if (graces && graces.length) {
+        note.appendChild(getNextvNoteDuration(i)?.cloneNode(true))
+      }
+    })
+  }
+  return new XMLSerializer().serializeToString(xmlParse)
+}
+
+const speedInfo = {
+  'rall.': 1.333333333,
+  'poco rit.': 1.333333333,
+  'rit.': 1.333333333,
+  'molto rit.': 1.333333333,
+  'molto rall': 1.333333333,
+  molto: 1.333333333,
+  lentando: 1.333333333,
+  allargando: 1.333333333,
+  morendo: 1.333333333,
+  'accel.': 0.8,
+  calando: 2,
+  'poco accel.': 0.8,
+  'gradually slowing': 1.333333333,
+  slowing: 1.333333333,
+  slow: 1.333333333,
+  slowly: 1.333333333,
+  faster: 1.333333333
+}
+
+/**
+ * 按照xml进行减慢速度的计算
+ * @param xml 始终按照第一分谱进行减慢速度的计算
+ */
+export function getGradualLengthByXml(xml: string) {
+  const firstPartXml = onlyVisible(xml, 0)
+  const xmlParse = new DOMParser().parseFromString(firstPartXml, 'text/xml')
+  const measures = Array.from(xmlParse.querySelectorAll('measure'))
+  const notes = Array.from(xmlParse.querySelectorAll('note'))
+  const words = Array.from(xmlParse.querySelectorAll('words'))
+  const metronomes = Array.from(xmlParse.querySelectorAll('metronome'))
+
+  const eles = []
+
+  for (const ele of [...words, ...metronomes]) {
+    const note = getNextNote(ele, 'direction')
+    // console.log(ele, note)
+    if (note) {
+      const measure = note?.closest('measure')
+      const measureNotes = Array.from(measure.querySelectorAll('note'))
+
+      const noteInMeasureIndex = Array.from(measure.childNodes)
+        .filter((item: any) => item.nodeName === 'note')
+        .findIndex((item) => item === note)
+
+      let allDuration = 0
+      let leftDuration = 0
+      for (let i = 0; i < measureNotes.length; i++) {
+        const n: any = measureNotes[i]
+        const duration = +(n.querySelector('duration')?.textContent || '0')
+        allDuration += duration
+        if (i < noteInMeasureIndex) {
+          leftDuration = allDuration
+        }
+      }
+      eles.push({
+        ele,
+        index: notes.indexOf(note),
+        noteInMeasureIndex,
+        textContent: ele.textContent,
+        measureIndex: measures.indexOf(measure), //,measure?.getAttribute('number')
+        type: ele.tagName,
+        allDuration,
+        leftDuration
+      })
+    }
+  }
+
+  // 结尾处手动插入一个音符节点
+  eles.push({
+    ele: notes[notes.length - 1],
+    index: notes.length,
+    noteInMeasureIndex: 0,
+    textContent: '',
+    type: 'metronome',
+    allDuration: 1,
+    leftDuration: 1,
+    measureIndex: measures.length
+  })
+
+  const gradualNotes: any[] = []
+  eles.sort((a, b) => a.index - b.index)
+  const keys = Object.keys(speedInfo).map((w) => w.toLocaleLowerCase())
+  let isLastNoteAndNotClosed = false
+  for (const ele of eles) {
+    const textContent: any = ele.textContent?.toLocaleLowerCase().trim()
+    if (ele === eles[eles.length - 1]) {
+      if (gradualNotes[gradualNotes.length - 1]?.length === 1) {
+        isLastNoteAndNotClosed = true
+      }
+    }
+
+    const isKeyWork = keys.find((k) => {
+      const ks = k.split(' ')
+      return textContent && ks.includes(textContent)
+    })
+    if (
+      ele.type === 'metronome' ||
+      (ele.type === 'words' && (textContent.startsWith('a tempo') || isKeyWork)) ||
+      isLastNoteAndNotClosed
+    ) {
+      const indexOf = gradualNotes.findIndex((item) => item.length === 1)
+      if (indexOf > -1 && ele.index > gradualNotes[indexOf]?.[0].start) {
+        gradualNotes[indexOf][1] = {
+          start: ele.index,
+          measureIndex: ele.measureIndex,
+          noteInMeasureIndex: ele.noteInMeasureIndex,
+          allDuration: ele.allDuration,
+          leftDuration: ele.leftDuration,
+          type: textContent
+        }
+      }
+    }
+    if (ele.type === 'words' && isKeyWork) {
+      gradualNotes.push([
+        {
+          start: ele.index,
+          measureIndex: ele.measureIndex,
+          noteInMeasureIndex: ele.noteInMeasureIndex,
+          allDuration: ele.allDuration,
+          leftDuration: ele.leftDuration,
+          type: textContent
+        }
+      ])
+    }
+  }
+  return gradualNotes
+}
+
+export default defineComponent({
+  name: 'city-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    },
+    tagList: {
+      type: Array as PropType<Array<SelectOption>>,
+      default: () => []
+    },
+    subjectList: {
+      type: Array as PropType<Array<SelectOption>>,
+      default: () => []
+    },
+    musicSheetCategories: {
+      type: Array as PropType<Array<SelectOption>>,
+      default: () => []
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      graduals: {} as any, // 渐变速度
+      audioType: 'MP3', // 播放类型
+      mp3Type: 'MP3', // 是否含节拍器
+      xmlFileUrl: null, // XML
+      midiUrl: null, // mid
+      metronomeUrl: null, // 伴奏 根据mp3Type 是否是为包含节拍器
+      musicSheetName: null, // 曲目名称
+      musicTag: [] as any, // 曲目标签
+      composer: null, // 音乐人
+      playSpeed: null, // 曲谱速度
+      showFingering: null as any, // 是否显示指法
+      canEvaluate: null as any, // 是否评测
+      musicSubject: null as any, // 可用声部
+      notation: null as any, // 能否转和简谱
+      auditVersion: null as any, // 审核版本
+      accompanimentType: null, // 伴奏类型
+      sortNumber: null, // 排序
+      titleImg: null, // 曲谱封面
+      remark: null, // 曲谱描述
+      background: [] as any, // 原音
+      musicSheetCategoriesId: null,
+      status: false,
+      musicSheetType: 'SINGLE' as 'SINGLE' | 'CONCERT'
+    })
+    const state = reactive({
+      tagList: [...props.tagList] as any, // 标签列表
+      xmlFirstSpeed: null as any, // 第一个音轨速度
+      partListNames: [] as any, // 所有音轨声部列表
+      musicSheetCategories: [...props.musicSheetCategories] as any
+    })
+    const gradualData = reactive({
+      list: [] as any[],
+      gradualRefs: [] as any[]
+    })
+
+    const btnLoading = ref(false)
+    const formsRef = ref()
+    const message = useMessage()
+    const dialog = useDialog()
+
+    // 提交记录
+    const onSubmit = async () => {
+      formsRef.value.validate(async (error: any) => {
+        console.log(error, 'error')
+        if (error) {
+          message.error(error[0]?.[0]?.message)
+          return
+        }
+        try {
+          const obj = {
+            ...forms,
+            musicTag: '-1',
+            extConfigJson: JSON.stringify({ gradualTimes: forms.graduals })
+          }
+          if (forms.audioType == 'MIDI') {
+            obj.background = []
+          }
+          btnLoading.value = true
+          if (props.type === 'add') {
+            await musicSheetSave({ ...obj })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await musicSheetUpdate({ ...obj, id: props.data.id })
+            message.success('修改成功')
+          }
+          emit('getList')
+          emit('close')
+        } catch (e) {
+          console.log(e)
+        }
+        setTimeout(() => {
+          btnLoading.value = false
+        }, 100)
+      })
+    }
+
+    // 上传XML,初始化音轨 音轨速度
+    const readFileInputEventAsArrayBuffer = (file: any) => {
+      const xmlRead = new FileReader()
+      xmlRead.onload = (res) => {
+        try {
+          gradualData.list = getGradualLengthByXml(res?.target?.result as any).filter(
+            (item: any) => item.length === 2
+          )
+        } catch (error) {}
+        state.partListNames = getPartListNames(res?.target?.result as any) as any
+        // 这里是如果没有当前音轨就重新写
+        for (let j = 0; j < state.partListNames.length; j++) {
+          if (!forms.background[j]) {
+            forms.background.push({ audioFileUrl: null, track: null })
+          }
+          forms.background[j].track = state.partListNames[j].value
+        }
+
+        // 循环添加所在音轨的原音
+        for (let index = forms.background.length; index < state.partListNames.length; index++) {
+          const part = state.partListNames[index].value
+          const sysData = {
+            ...forms.background[0],
+            track: part
+          }
+          if (!sysData.speed) {
+            sysData.speed = state.xmlFirstSpeed
+          }
+          createSys(sysData)
+        }
+
+        if (forms.background.length == 0) {
+          forms.background.push({ audioFileUrl: '', track: '' })
+        }
+      }
+      xmlRead.readAsText(file)
+    }
+
+    // 获取xml中所有轨道
+    const getPartListNames = (xml: any) => {
+      if (!xml) return []
+      const xmlParse = new DOMParser().parseFromString(xml, 'text/xml')
+      const partList =
+        xmlParse.getElementsByTagName('part-list')?.[0]?.getElementsByTagName('score-part') || []
+      const partListNames = Array.from(partList).map((item) => {
+        const part = item.getElementsByTagName('part-name')?.[0].textContent || ''
+        return {
+          value: part,
+          label: part
+        }
+      })
+
+      if (partListNames.length > 0) {
+        forms.background = forms.background.slice(0, partListNames.length)
+      }
+
+      state.xmlFirstSpeed = xmlParse.getElementsByTagName('per-minute')?.[0]?.textContent || ''
+      if (!forms.playSpeed) {
+        forms.playSpeed = state.xmlFirstSpeed
+      }
+      return partListNames
+    }
+
+    // 判断选择的音轨是否在选中
+    const initPartsListStatus = (track: string): any => {
+      const _names = state.partListNames.filter((n: any) => n.value?.toLocaleUpperCase?.() != 'COMMON')
+      const partListNames = deepClone(_names) || []
+      partListNames.forEach((item: any) => {
+        const index = forms.background.findIndex((ground: any) => item.value == ground.track)
+        if (index > -1 && track != item.value) {
+          item.disabled = true
+        } else {
+          item.disabled = false
+        }
+      })
+      return partListNames || []
+    }
+
+    // 添加原音
+    const createSys = (initData?: any) => {
+      forms.background.push({
+        audioFileUrl: null, // 原音
+        track: null, // 轨道
+        ...initData
+      })
+    }
+    // 删除原音
+    const removeSys = (index: number) => {
+      dialog.warning({
+        title: '警告',
+        content: `是否确认删除此原音?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          forms.background.splice(index, 1)
+        }
+      })
+    }
+
+    onMounted(async () => {
+      if (props.type === 'edit') {
+        const detail = props.data
+        try {
+          const { data } = await musicSheetDetail({ id: detail.id })
+          forms.audioType = data.audioType
+          forms.mp3Type = data.mp3Type
+          forms.xmlFileUrl = data.xmlFileUrl
+          forms.midiUrl = data.midiUrl
+          forms.metronomeUrl = data.metronomeUrl
+          forms.musicSheetName = data.musicSheetName
+          forms.musicTag = data.musicTag?.split(',')
+          forms.composer = data.composer
+          forms.playSpeed = data.playSpeed
+          forms.showFingering = Number(data.showFingering)
+          forms.canEvaluate = Number(data.canEvaluate)
+          forms.musicSubject = data.musicSubject ? Number(data.musicSubject) : null
+          forms.notation = Number(data.notation)
+          forms.auditVersion = Number(data.auditVersion)
+          forms.accompanimentType = data.accompanimentType
+          forms.sortNumber = data.sortNumber
+          forms.titleImg = data.titleImg
+          forms.remark = data.remark
+          forms.status = data.status
+          forms.musicSheetCategoriesId = data.musicSheetCategoriesId
+          forms.background = data.background || []
+          forms.musicSheetType = data.musicSheetType || "SINGLE"
+          // 获取渐变 和 是否多声部
+          try {
+            const extConfigJson = data.extConfigJson ? JSON.parse(data.extConfigJson) : {}
+            forms.graduals = extConfigJson.gradualTimes || {}
+          } catch (error) {}
+          axios.get(data.xmlFileUrl).then((res: any) => {
+            if (res?.data) {
+              gradualData.list = getGradualLengthByXml(res?.data as any).filter(
+                (item: any) => item.length === 2
+              )
+              state.partListNames = getPartListNames(res?.data as any) as any
+            }
+          })
+        } catch (error) {
+          console.log(error)
+        }
+      }
+    })
+
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm
+          class={styles.formContainer}
+          model={forms}
+          ref={formsRef}
+          label-placement="left"
+          label-width="130"
+        >
+          <NGrid cols={2}>
+            <NFormItemGi
+              label="播放类型"
+              path="audioType"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择播放类型'
+                }
+              ]}
+            >
+              <NRadioGroup
+                v-model:value={forms.audioType}
+                onUpdateValue={(value: string | number | boolean) => {
+                  console.log(value, 'value')
+                  if (value === 'MP3') {
+                    forms.mp3Type = 'MP3'
+                  } else {
+                    forms.mp3Type = ''
+                  }
+                }}
+              >
+                <NRadio value="MP3">MP3</NRadio>
+                <NRadio value="MIDI">MIDI</NRadio>
+              </NRadioGroup>
+            </NFormItemGi>
+            <NFormItemGi
+                label="曲谱类型"
+                path="musicSheetType"
+                rule={[
+                  {
+                    required: true,
+                    message: '请选择曲谱类型'
+                  }
+                ]}
+              >
+                <NRadioGroup v-model:value={forms.musicSheetType}>
+                  <NRadio value="SINGLE">单曲</NRadio>
+                  <NRadio value="CONCERT">合奏</NRadio>
+                </NRadioGroup>
+              </NFormItemGi>
+            {/* 只有mp3 才有节拍器 */}
+            {forms.audioType === 'MP3' && (
+              <NFormItemGi
+                label="是否含节拍器"
+                path="mp3Type"
+                rule={[
+                  {
+                    required: true,
+                    message: '请选择是否含节拍器'
+                  }
+                ]}
+              >
+                <NRadioGroup v-model:value={forms.mp3Type}>
+                  <NRadio value="MP3">不含节拍器</NRadio>
+                  <NRadio value="MP3_METRONOME">含节拍器</NRadio>
+                </NRadioGroup>
+              </NFormItemGi>
+            )}
+          </NGrid>
+          <NGrid cols={2}>
+            <NFormItemGi
+              label="上传XML"
+              path="xmlFileUrl"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择上传XML'
+                }
+              ]}
+            >
+              <UploadFile
+                size={10}
+                v-model:fileList={forms.xmlFileUrl}
+                tips="仅支持上传.xml格式文件"
+                listType="image"
+                accept=".xml"
+                bucketName="cloud-coach"
+                text="点击上传XML"
+                onReadFileInputEventAsArrayBuffer={readFileInputEventAsArrayBuffer}
+              />
+            </NFormItemGi>
+            {forms.audioType === 'MIDI' && (
+              <NFormItemGi
+                label="上传mid"
+                path="midiUrl"
+                rule={[
+                  {
+                    required: true,
+                    message: '请选择上传mid'
+                  }
+                ]}
+              >
+                <UploadFile
+                  size={10}
+                  v-model:fileList={forms.midiUrl}
+                  tips="仅支持上传.mid格式文件"
+                  listType="image"
+                  accept=".mid"
+                  bucketName="cloud-coach"
+                  text="点击上传mid"
+                />
+              </NFormItemGi>
+            )}
+            {/* 只有播放类型为mp3时才会有伴奏 */}
+            {forms.audioType === 'MP3' && (
+              <NFormItemGi
+                label={`伴奏(${forms.mp3Type === 'MP3' ? '不' : ''}含节拍器)`}
+                path="metronomeUrl"
+                rule={[
+                  {
+                    required: true,
+                    message: '请选择上传伴奏'
+                  }
+                ]}
+              >
+                <UploadFile
+                  size={10}
+                  v-model:fileList={forms.metronomeUrl}
+                  tips="仅支持上传.mp3/.aac格式文件"
+                  listType="image"
+                  accept=".mp3,.aac"
+                  bucketName="cloud-coach"
+                  text={`点击上传伴奏(${forms.mp3Type === 'MP3' ? '不' : ''}含节拍器)`}
+                />
+              </NFormItemGi>
+            )}
+            <NFormItemGi
+              label="曲目名称"
+              path="musicSheetName"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入曲目名称'
+                }
+              ]}
+            >
+              <NInput v-model:value={forms.musicSheetName} placeholder="请输入曲目名称" />
+            </NFormItemGi>
+            {/* <NFormItemGi
+              label="曲目标签"
+              path="musicTag"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择曲目标签'
+                }
+              ]}
+            >
+              <NSelect
+                placeholder="请选择曲目标签"
+                v-model:value={forms.musicTag}
+                options={state.tagList}
+                multiple
+                maxTagCount="responsive"
+                onUpdate:value={(value: any) => {
+                  // 判断条数
+                  if (value.length === 3) {
+                    state.tagList.forEach((item: any) => {
+                      if (
+                        forms.musicTag &&
+                        typeof value === 'object' &&
+                        value.includes(item.value)
+                      ) {
+                        item.disabled = false
+                      } else {
+                        item.disabled = true
+                      }
+                    })
+                  } else {
+                    state.tagList.forEach((item: any) => {
+                      item.disabled = false
+                    })
+                  }
+                }}
+              />
+            </NFormItemGi> */}
+            <NFormItemGi
+              label="曲目分类"
+              path="musicSheetCategoriesId"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择曲目分类'
+                }
+              ]}
+            >
+              <NCascader
+                valueField="id"
+                labelField="name"
+                children-field="musicSheetCategoriesList"
+                placeholder="请选择分类"
+                v-model:value={forms.musicSheetCategoriesId}
+                options={state.musicSheetCategories}
+                clearable
+              />
+            </NFormItemGi>
+            <NFormItemGi label="音乐人" path="composer">
+              <NInput v-model:value={forms.composer} placeholder="请输入音乐人姓名" />
+            </NFormItemGi>
+            {forms.musicSheetType != 'CONCERT' && <NFormItemGi
+              label="可用声部"
+              path="musicSubject"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择可用声部'
+                }
+              ]}
+            >
+              <NSelect
+                v-model:value={forms.musicSubject}
+                options={props.subjectList}
+                filterable
+                clearable
+                placeholder="请选择可用声部"
+              />
+            </NFormItemGi>}
+            <NFormItemGi
+              label="指法展示"
+              path="showFingering"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择指法展示'
+                }
+              ]}
+            >
+              <NRadioGroup v-model:value={forms.showFingering}>
+                <NRadio value={1}>展示</NRadio>
+                <NRadio value={0}>不展示</NRadio>
+              </NRadioGroup>
+            </NFormItemGi>
+            <NFormItemGi
+              label="是否评测"
+              path="canEvaluate"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择是否评测'
+                }
+              ]}
+            >
+              <NRadioGroup v-model:value={forms.canEvaluate}>
+                <NRadio value={1}>是</NRadio>
+                <NRadio value={0}>否</NRadio>
+              </NRadioGroup>
+            </NFormItemGi>
+
+            <NFormItemGi
+              label="能否转简谱"
+              path="notation"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择能否转简谱'
+                }
+              ]}
+            >
+              <NRadioGroup v-model:value={forms.notation}>
+                <NRadio value={1}>是</NRadio>
+                <NRadio value={0}>否</NRadio>
+              </NRadioGroup>
+            </NFormItemGi>
+
+            {/* <NFormItemGi
+              label="伴奏类型"
+              path="accompanimentType"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择伴奏类型'
+                }
+              ]}
+            >
+              <NRadioGroup v-model:value={forms.accompanimentType}>
+                <NRadio value="HOMEMODE">自制伴奏</NRadio>
+                <NRadio value="COMMON">普通伴奏</NRadio>
+              </NRadioGroup>
+            </NFormItemGi> */}
+            <NFormItemGi
+              label="排序值"
+              path="sortNumber"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入排序值'
+                }
+              ]}
+            >
+              <NInputNumber
+                v-model:value={forms.sortNumber}
+                placeholder="请输入排序值"
+                min={0}
+                style={{ width: '100%' }}
+              />
+            </NFormItemGi>
+          </NGrid>
+          <NGrid cols={2}>
+            <NFormItemGi label="曲谱封面" path="titleImg">
+              <UploadFile
+                accept=".jpg,.jpeg,.png"
+                tips="请上传大小2M以内的JPG、PNG图片"
+                v-model:fileList={forms.titleImg}
+                cropper
+                bucketName="cloud-coach"
+                options={{
+                  autoCrop: true, //是否默认生成截图框
+                  enlarge: 2, //  图片放大倍数
+                  autoCropWidth: 200, //默框高度
+                  fixedBox: true, //是否固定截图框大认生成截图框宽度
+                  autoCropHeight: 200, //默认生成截图小 不允许改变
+                  previewsCircle: false, //预览图是否是原圆形
+                  title: '曲谱封面'
+                }}
+              />
+            </NFormItemGi>
+            <NFormItemGi label="曲谱描述" path="remark">
+              <NInput
+                placeholder="请输入曲谱描述"
+                type="textarea"
+                rows={4}
+                showCount
+                maxlength={200}
+                v-model:value={forms.remark}
+              />
+            </NFormItemGi>
+          </NGrid>
+          {!!gradualData.list.length && (
+            <>
+              <NAlert showIcon={false} type="info">
+                识别到共1处渐变速度,请输入Dorico对应小节时间信息
+              </NAlert>
+              <NFormItem label="rit." required style={{ marginTop: '10px' }}>
+                <NSpace vertical>
+                  {gradualData.list.map((n: any, ni: number) => (
+                    <NInputGroup>
+                      <NFormItem
+                        path={`graduals.${n[0].measureIndex}`}
+                        rule={[
+                          { required: true, message: '请输入合奏曲目时间' },
+                          {
+                            pattern: /^((\d{2}):?){2,3}$/,
+                            message: '请输入正确的曲目时间',
+                            trigger: 'blur'
+                          }
+                        ]}
+                      >
+                        <NInputGroup>
+                          <NInputGroupLabel>{n[0].measureIndex}小节开始</NInputGroupLabel>
+                          <NInput
+                            placeholder="00:00:00"
+                            v-model:value={forms.graduals[n[0].measureIndex]}
+                          ></NInput>
+                        </NInputGroup>
+                      </NFormItem>
+                      <div style={{ lineHeight: '30px', padding: '0 4px' }}>~</div>
+                      <NFormItem
+                        path={`graduals.${n[1].measureIndex}`}
+                        rule={[
+                          { required: true, message: '请输入合奏曲目时间' },
+                          {
+                            pattern: /^((\d{2}):?){2,3}$/,
+                            message: '请输入正确的曲目时间',
+                            trigger: 'blur'
+                          }
+                        ]}
+                      >
+                        <NInputGroup>
+                          <NInput
+                            placeholder="00:00:00"
+                            v-model:value={forms.graduals[n[1].measureIndex]}
+                          ></NInput>
+                          <NInputGroupLabel>{n[1].measureIndex}小节结束</NInputGroupLabel>
+                        </NInputGroup>
+                      </NFormItem>
+                    </NInputGroup>
+                  ))}
+                </NSpace>
+              </NFormItem>
+            </>
+          )}
+          <NFormItem label="曲谱速度">
+            <NInputNumber v-model:value={forms.playSpeed} showButton={false} />
+          </NFormItem>
+          {/* 只有播放类型为mp3时才会有原音 */}
+          {forms.audioType === 'MP3' && (
+            <>
+              {forms.background.map((item: any, index: number) => (
+                <>
+                  {item.track?.toLocaleUpperCase?.() != 'COMMON' && <NGrid class={styles.audioSection}>
+                    <NFormItemGi
+                      span={12}
+                      label="原音"
+                      path={`background[${index}].audioFileUrl`}
+                      rule={[
+                        {
+                          required: true,
+                          message: `请上传${
+                            item.track ? item.track + '的' : '第' + (index + 1) + '个'
+                          }原音`
+                        }
+                      ]}
+                    >
+                      <UploadFile
+                        size={10}
+                        v-model:fileList={item.audioFileUrl}
+                        tips="仅支持上传.mp3/.aac格式文件"
+                        listType="image"
+                        accept=".mp3,.aac"
+                        bucketName="cloud-coach"
+                      />
+                    </NFormItemGi>
+                    {state.partListNames.length > 1 && (
+                      <NFormItemGi
+                        span={12}
+                        label="所属轨道"
+                        path={`background[${index}].track`}
+                        rule={[
+                          {
+                            required: true,
+                            message: '请选择所属轨道'
+                          }
+                        ]}
+                      >
+                        <NSelect
+                          placeholder="请选择所属轨道"
+                          v-model:value={item.track}
+                          options={initPartsListStatus(item.track)}
+                        />
+                      </NFormItemGi>
+                    )}
+                    <NGi class={styles.btnRemove}>
+                      <NButton
+                        type="primary"
+                        text
+                        disabled={forms.background.length === 1}
+                        onClick={() => removeSys(index)}
+                      >
+                        删除
+                      </NButton>
+                    </NGi>
+                  </NGrid>}
+                </>
+              ))}
+
+              <NButton
+                type="primary"
+                dashed
+                block
+                disabled={state.partListNames.length <= forms.background.length}
+                style={{
+                  marginBottom: '24px'
+                }}
+                onClick={createSys}
+              >
+                添加原音
+              </NButton>
+            </>
+          )}
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton
+            type="primary"
+            onClick={() => onSubmit()}
+            loading={btnLoading.value}
+            disabled={btnLoading.value}
+          >
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 23 - 0
src/views/content-manage/music-manage/modal/musicPreView.tsx

@@ -0,0 +1,23 @@
+import { defineComponent } from 'vue'
+import { useUserStore } from '@/store/modules/user'
+
+export default defineComponent({
+  name: 'musicPreView',
+  props: {
+    item: {
+      type: Object,
+      default: () => {}
+    }
+  },
+  setup(props, { emit }) {
+    const userStore = useUserStore()
+    const token = userStore.getToken
+    const prefix = /(localhost|192)/.test(location.host) ? 'https://ponline.colexiu.com' : location.origin
+    const src = prefix + `/orchestra-music-score/?_t=${Date.now()}&id=${props.item.id}&modelType=practice&modeType=json&Authorization=${token}`
+    return () => (
+      <div>
+        <iframe width={'667px'} height={'375px'} frameborder="0" src={src}></iframe>
+      </div>
+    )
+  }
+})

+ 125 - 0
src/views/content-manage/music-manage/modal/tag-operation.tsx

@@ -0,0 +1,125 @@
+import {
+  NForm,
+  NFormItem,
+  NInput,
+  NSelect,
+  NSpace,
+  NButton,
+  useMessage,
+  NCascader,
+  NInputNumber
+} from 'naive-ui'
+import type { TreeSelectOption } from 'naive-ui'
+import { defineComponent, PropType, reactive, ref } from 'vue'
+import { musicTagSave, musicTagUpdate } from '../../api'
+
+export default defineComponent({
+  name: 'city-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    },
+    catalogList: {
+      type: Array as PropType<Array<TreeSelectOption>>,
+      default: () => []
+    },
+    clientTypeList: {
+      type: Array as PropType<Array<string>>,
+      default: () => []
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      name: null,
+      sortNumber: null
+    })
+
+    const btnLoading = ref(false)
+    const formsRef = ref()
+    const message = useMessage()
+
+    // 提交记录
+    const onSubmit = async () => {
+      formsRef.value.validate(async (error: any) => {
+        if (error) return
+        try {
+          btnLoading.value = true
+          if (props.type === 'add') {
+            await musicTagSave({ ...forms })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await musicTagUpdate({ ...forms, id: props.data.id })
+            message.success('修改成功')
+          }
+          emit('getList')
+          emit('close')
+        } catch {
+          //
+        }
+        setTimeout(() => {
+          btnLoading.value = false
+        }, 100)
+      })
+    }
+
+    // 初始化数据
+    if (props.type === 'edit') {
+      const data = props.data
+      forms.name = data.name
+      forms.sortNumber = data.sortNumber
+    }
+
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="auto">
+          <NFormItem
+            label="名称"
+            path="name"
+            rule={[
+              {
+                required: true,
+                message: '请输入名称'
+              }
+            ]}
+          >
+            <NInput
+              v-model:value={forms.name}
+              placeholder="请输入名称"
+              clearable
+              showCount
+              maxlength={6}
+            />
+          </NFormItem>
+          <NFormItem label="排序" path="sortNumber">
+            <NInputNumber
+              v-model:value={forms.sortNumber}
+              placeholder="请输入排序"
+              min={0}
+              style={{ width: '100%' }}
+            />
+          </NFormItem>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton
+            type="primary"
+            onClick={() => onSubmit()}
+            loading={btnLoading.value}
+            disabled={btnLoading.value}
+          >
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 131 - 0
src/views/educational-manage/api.ts

@@ -0,0 +1,131 @@
+import { clearEmtryData } from '@/utils'
+import request from '@/utils/request/index'
+
+/**
+ * 获取素材列表
+ */
+export const fetchMaterialList = (data: object) => {
+  return request({
+    url: '/cbs-app/material/page',
+    method: 'post',
+    data
+  } as any)
+}
+
+/**
+ * 素材分类分页
+ */
+export const fetchCategoryList: any = (data: any) => {
+  return new Promise((resolve) => {
+    request({
+      url: '/cbs-app/materialCategory/page',
+      method: 'post',
+      data:{
+        ...data,
+        rows: 1000
+      }
+    }).then((res) => {
+      if (res?.data){
+        res.data.rows = clearEmtryData(res.data.rows, 'subMaterialCategoryList')
+        resolve(res)
+      }
+    })
+    .catch(() => {
+      resolve({})
+    })
+  })
+}
+/**
+ * 添加素材
+ */
+export const materialSave = (data: any) => {
+  return request({
+    url: '/cbs-app/material/save',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 删除素材
+ */
+export const deleteMaterial = (data: any) => {
+  return request({
+    url: '/cbs-app/material/remove?id=' + data,
+    method: 'post'
+  })
+}
+
+/**
+ * 素材详情
+ */
+export const fetchMaterailDetail = (data: any) => {
+  return request({
+    url: `/cbs-app/material/detail/${data}`,
+    method: 'get'
+  })
+}
+
+/**
+ * 修改素材
+ */
+export const updateMaterailData = (data: any) => {
+  return request({
+    url: `/cbs-app/material/update`,
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 教学素材分类分页
+ */
+export const materialCategoryPage = (data: any) => {
+  return request({
+    url: `/cbs-app/materialCategory/page`,
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 教学素材分类详情
+ */
+export const materialCategoryDetail = (data: any) => {
+  return request({
+    url: `/cbs-app/materialCategory/detail/${data}`,
+    method: 'get',
+  })
+}
+
+/**
+ * 教学素材分类新增
+ */
+ export const materialCategorySave = (data: any) => {
+  return request({
+    url: `/cbs-app/materialCategory/save`,
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 教学素材分类删除
+ */
+ export const materialCategoryRemove = (data: any) => {
+  return request({
+    url: `/cbs-app/materialCategory/remove?id=` + data,
+    method: 'post'
+  })
+}
+
+/**
+ * 教学素材分类修改
+ */
+ export const materialCategoryUpdate = (data: any) => {
+  return request({
+    url: `/cbs-app/materialCategory/update`,
+    method: 'post',
+    data
+  })
+}

+ 269 - 0
src/views/educational-manage/component/category-list.tsx

@@ -0,0 +1,269 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  DataTableColumn,
+  NButton,
+  NDataTable,
+  NDatePicker,
+  NFormItem,
+  NInput,
+  NModal,
+  NSpace,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { materialCategoryPage, materialCategoryRemove } from '../api'
+import styles from '../index.module.less'
+import dayjs from 'dayjs'
+import AddCategory from '../model/addCategory'
+import { getTimes } from '@/utils/dateUtil'
+
+export default defineComponent({
+  name: 'category-list',
+  setup(props) {
+    const dialog = useDialog()
+    const message = useMessage()
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10000
+      },
+      visiableCity: false,
+      dataList: [] as any,
+      cityType: 'add',
+      materail: null as any
+    })
+    const searchForm = reactive({
+      keyword: '',
+      times: null as any,
+      operatorId: '' //创建人
+    })
+
+    const columns = (): DataTableColumn[] => {
+      return [
+        {
+          title: '分类名称',
+          key: 'name',
+          fixed: 'left'
+        },
+        {
+          title: '分类描述',
+          key: 'desc'
+        },
+        {
+          title: '上传时间',
+          key: 'updateTime',
+          render(row: any) {
+            return row.updateTime || row.createTime
+          }
+        },
+
+        {
+          title: '操作',
+          key: 'operation',
+          fixed: 'right',
+          width: 180,
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  text
+                  //v-auth="materialCategory/save1599962104022290433"
+                  onClick={() => {
+                    state.materail = {
+                      parentId: row.id
+                    }
+                    state.visiableCity = true
+                  }}
+                >
+                  添加子分类
+                </NButton>
+                <NButton
+                  type="primary"
+                  text
+                  //v-auth="materialCategory/update1599962354053140482"
+                  onClick={() => {
+                    state.cityType = 'update'
+                    state.materail = row
+                    state.visiableCity = true
+                  }}
+                >
+                  修改
+                </NButton>
+                <NButton
+                  type="primary"
+                  text
+                  //v-auth="materialCategory/remove1599962245085122562"
+                  onClick={() => handleDelete(row)}
+                >
+                  删除
+                </NButton>
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { times, ...reset } = searchForm
+        const body = {
+          ...reset,
+          ...getTimes(times, ['startTime', 'endTime']),
+          page: state.pagination.page,
+          rows: state.pagination.rows
+        }
+        const { data } = await materialCategoryPage(body)
+        state.loading = false
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+    // 删除
+    const handleDelete = async (row: any) => {
+      dialog.warning({
+        title: '警告',
+        content: '是否确认删除此分类?',
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          const res = await materialCategoryRemove(row.id)
+          if (res.data) {
+            onSubmit()
+            message.success('删除成功')
+          }
+        }
+      })
+    }
+
+    onMounted(() => {
+      getList()
+    })
+
+    return () => (
+      <>
+        <SaveForm
+          ref={saveForm}
+          model={searchForm}
+          onSubmit={onSubmit}
+          onSetModel={(val: any) => Object.assign(searchForm, val)}
+          saveKey="category-key"
+        >
+          <NFormItem path="keyword" label="分类名称">
+            <NInput
+              placeholder="分类名称"
+              v-model:value={searchForm.keyword}
+              onKeydown={(e) => {
+                if (e.code === 'Enter') {
+                  onSubmit()
+                }
+              }}
+              clearable
+            />
+          </NFormItem>
+          {/* <NFormItem path="keyword">
+            <NInput
+              placeholder="更新人"
+              v-model:value={searchForm.operatorId}
+              onKeydown={(e) => {
+                if (e.code === 'Enter') {
+                  onSubmit()
+                }
+              }}
+              clearable
+            />
+          </NFormItem> */}
+          <NFormItem path="times" label="时间">
+            <NDatePicker
+              class={styles.datepicker}
+              value-format="yyyy.MM.dd"
+              v-model:value={searchForm.times}
+              type="daterange"
+              clearable
+            />
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+
+        <NSpace style={{ paddingBottom: '12px' }}>
+          <NButton
+            type="primary"
+            //v-auth="materialCategory/save1599962104022290433"
+            onClick={() => {
+              state.materail = null
+              state.cityType = 'add'
+              state.visiableCity = true
+            }}
+            disabled={state.loading}
+          >
+            新增分类
+          </NButton>
+        </NSpace>
+        <NDataTable
+          scroll-x="1300"
+          loading={state.loading}
+          columns={columns()}
+          data={state.dataList}
+          children-key="subMaterialCategoryList"
+          default-expand-all={false}
+          row-key={(row: any) => row.id}
+        ></NDataTable>
+
+        <NModal
+          v-model:show={state.visiableCity}
+          preset="dialog"
+          showIcon={false}
+          title={state.cityType === 'add' ? '新增分类' : '修改分类'}
+          style={{ width: '400px' }}
+          onClose={() => {
+            state.materail = null
+          }}
+        >
+          <AddCategory
+            item={state.materail}
+            list={state.dataList}
+            isAdd={state.cityType === 'add' ? true : false}
+            onClose={() => {
+              state.visiableCity = false
+            }}
+            onHandleSuccess={() => {
+              state.visiableCity = false
+              if (!state.materail) {
+                state.pagination.page = 1
+              }
+              getList()
+            }}
+          />
+        </NModal>
+      </>
+    )
+  }
+})

+ 450 - 0
src/views/educational-manage/component/material-list.tsx

@@ -0,0 +1,450 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  DataTableColumn,
+  NButton,
+  NCascader,
+  NDataTable,
+  NDatePicker,
+  NDescriptions,
+  NDescriptionsItem,
+  NFormItem,
+  NInput,
+  NModal,
+  NSelect,
+  NSpace,
+  NTag,
+  NTooltip,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, inject, onMounted, reactive, ref, provide } from 'vue'
+import { fetchCategoryList, fetchMaterialList, deleteMaterial, updateMaterailData } from '../api'
+import { subjectBasicConfigPage } from '@/views/system-manage/api'
+import styles from '../index.module.less'
+import { InternalRowData } from 'naive-ui/es/data-table/src/interface'
+import AddMaterial from '../model/addMaterial'
+import dayjs from 'dayjs'
+import { getLessonType, lessonType } from '@/views/knowledge-manage/knowledgeTypeData'
+import { filterTimes, getTimes } from '@/utils/dateUtil'
+import router from '@/router'
+import { useRoute } from 'vue-router'
+
+const classType: { [_: string]: any } = {
+  VIDEO: '视频',
+  IMG: '图片',
+  SONG: '曲目'
+}
+const classTypeList = Object.keys(classType).map((key: string) => {
+  return { label: classType[key], value: key }
+})
+export default defineComponent({
+  name: 'material-list',
+  setup(props) {
+    const dialog = useDialog()
+    const message = useMessage()
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 10
+      },
+      visiableCity: false,
+      dataList: [] as any,
+      cityType: 'add',
+      materail: null,
+      isLook: false,
+      employeeList: [] as any
+    })
+    const searchForm = reactive({
+      keyword: null as any,
+      time: null as any,
+      courseTypeCode: null,
+      materialCategoryId: null,
+      type: null
+    })
+    const route = useRoute()
+    // 获取分类列表
+    const category = reactive({
+      list: [] as any,
+      index: -1
+    })
+    const getCategoryList = async () => {
+      try {
+        const { data } = await fetchCategoryList({ page: 1 })
+        category.list = data?.rows || []
+      } catch {}
+    }
+    provide('categoryList', category)
+    // 声部
+    const subjects = reactive({
+      list: [] as any
+    })
+    const getSubjects = async () => {
+      try {
+        const { data } = await subjectBasicConfigPage({ page: 1, row: 1000 })
+        subjects.list = data?.rows || []
+      } catch {}
+    }
+    provide('subjects', subjects)
+
+    onMounted(() => {
+      console.log(route.query, 'route.query')
+      if (route.query.id) {
+        searchForm.keyword = route.query.id as string
+      }
+      getCategoryList()
+      getSubjects()
+    })
+
+    const columns = (): DataTableColumn[] => {
+      return [
+        {
+          title: '素材名称',
+          key: 'name',
+          render(row: any) {
+            return (
+              <NDescriptions labelPlacement="left" column={1}>
+                <NDescriptionsItem label="素材名称">{row.name}</NDescriptionsItem>
+                <NDescriptionsItem label="素材编号">{row.id}</NDescriptionsItem>
+                <NDescriptionsItem label="素材分类">{row.materialCategoryName}</NDescriptionsItem>
+              </NDescriptions>
+            )
+          }
+        },
+        // {
+        //   title: '编号',
+        //   key: 'id'
+        // },
+
+        // {
+        //   title: '素材分类',
+        //   key: 'materialCategoryName'
+        // }, 分段编号
+        {
+          title: '素材信息',
+          key: 'sn',
+          render(row: any) {
+            return (
+              <NDescriptions labelPlacement="left" column={1}>
+                <NDescriptionsItem label="分段编号">{row.sn}</NDescriptionsItem>
+                <NDescriptionsItem label="素材类型">
+                  {classType[row.type as string]}
+                </NDescriptionsItem>
+                {/* <NDescriptionsItem label="课程类型">{row.courseTypeName}</NDescriptionsItem> */}
+              </NDescriptions>
+            )
+          }
+        },
+        {
+          title: '课程信息',
+          key: 'type',
+          render(row: any) {
+            return (
+              <NDescriptions labelPlacement="left" column={1}>
+                <NDescriptionsItem label="课程类型">{row.courseTypeName}</NDescriptionsItem>
+                {/* 
+                <NDescriptionsItem label="建议时长">
+                  {row.adviseStudyTimeSecond ? row.adviseStudyTimeSecond + '秒' : ''}
+                </NDescriptionsItem> */}
+              </NDescriptions>
+            )
+          }
+        },
+        // {
+        //   title: '课程类型',
+        //   key: 'courseTypeName',
+        //   width: 200,
+        //   ellipsis: true,
+        //   render(row: InternalRowData) {
+        //     return (
+        //       <NTooltip placement="top-start">
+        //         {{
+        //           default: () => row.courseTypeName,
+        //           trigger: () => row.courseTypeName
+        //         }}
+        //       </NTooltip>
+        //     )
+        //   }
+        // },
+        // {
+        //   title: '建议学习时长',
+        //   key: 'adviseStudyTimeSecond',
+        //   render(row: InternalRowData) {
+        //     return row.adviseStudyTimeSecond ? row.adviseStudyTimeSecond + '秒' : ''
+        //   }
+        // },
+        {
+          title: '操作信息',
+          key: 'updateTime',
+          render(row: any) {
+            return (
+              <NDescriptions labelPlacement="left" column={1}>
+                <NDescriptionsItem label="上传人">{row.operatorName}</NDescriptionsItem>
+
+                <NDescriptionsItem label="上传时间">
+                  {row.updateTime ? row.updateTime : '--'}
+                </NDescriptionsItem>
+              </NDescriptions>
+            )
+          }
+        },
+        // {
+        //   title: '上传人',
+        //   key: 'operatorName'
+        // },
+        {
+          title: '状态',
+          key: 'delFlag',
+          render(row: InternalRowData) {
+            return (
+              <>
+                <NTag type={row.enableFlag ? 'success' : 'warning'}>
+                  {row.enableFlag ? '启用' : '停用'}
+                </NTag>
+              </>
+            )
+          }
+        },
+
+        {
+          title: '操作',
+          key: 'operation',
+          fixed: 'right',
+          width: 160,
+          render(row: any) {
+            return (
+              <NSpace>
+                {/* <NButton
+                  type={row.enableFlag ? 'error' : 'success'}
+                  text
+                  v-auth="material/update1599948375910109186"
+                  onClick={() => handleChange(row)}
+                >
+                  {row.enableFlag ? '停用' : '启用'}
+                </NButton> */}
+                <NButton
+                  type="primary"
+                  text
+                  onClick={() => {
+                    state.isLook = true
+                    state.materail = row
+                    state.visiableCity = true
+                  }}
+                >
+                  查看
+                </NButton>
+                <NButton
+                  type="primary"
+                  text
+                  //v-auth="material/update1599948375910109186"
+                  onClick={() => {
+                    state.cityType = 'update'
+                    state.materail = row
+                    state.visiableCity = true
+                  }}
+                >
+                  修改
+                </NButton>
+                <NButton
+                  type="primary"
+                  text
+                  disabled={row.enableFlag}
+                  //v-auth="material/remove1599948195949301762"
+                  onClick={() => handleDelete(row)}
+                >
+                  删除
+                </NButton>
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { time, ...reset } = searchForm
+        const body = {
+          ...reset,
+          ...filterTimes(time, ['startTime', 'endTime']),
+          page: state.pagination.page,
+          rows: state.pagination.rows
+        }
+        const { data } = await fetchMaterialList(body)
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+    // 删除
+    const handleDelete = async (row: any) => {
+      dialog.warning({
+        title: '警告',
+        content: '是否确认删除此素材?',
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          const res = await deleteMaterial(row.id)
+          if (res.data) {
+            onSubmit()
+            message.success('删除成功')
+          }
+        }
+      })
+    }
+
+    // 启用,停用
+    const handleChange = (row: any) => {
+      dialog.warning({
+        title: '提示',
+        content: row.enableFlag ? '是否停用此素材?' : '是否启用此素材?',
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          const res = await deleteMaterial(row.id)
+          if (res.data) {
+            await updateMaterailData({ id: row.id, enableFlag: row.enableFlag ? 0 : 1 })
+            message.success('修改成功')
+            getList()
+          }
+        }
+      })
+    }
+
+    onMounted(() => {
+      getList()
+    })
+
+    return () => (
+      <>
+        <SaveForm
+          ref={saveForm}
+          label-width=""
+          model={searchForm}
+          onSubmit={onSubmit}
+          onSetModel={(val: any) => Object.assign(searchForm, val)}
+          saveKey="material-key"
+        >
+          <NFormItem label="搜索条件" path="keyword">
+            <NInput
+              v-model:value={searchForm.keyword}
+              onKeydown={(e) => {
+                if (e.code === 'Enter') {
+                  onSubmit()
+                }
+              }}
+              placeholder={'素材名称/编号'}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="素材分类" path="materialCategoryId">
+            <NCascader
+              v-model:value={searchForm.materialCategoryId}
+              options={category.list}
+              checkStrategy="child"
+              childrenField="subMaterialCategoryList"
+              expandTrigger="hover"
+              valueField="id"
+              labelField="name"
+              clearable
+              filterable
+            />
+          </NFormItem>
+          <NFormItem label="素材类型" path="type">
+            <NSelect clearable v-model:value={searchForm.type} options={classTypeList} />
+          </NFormItem>
+          <NFormItem label="课程类型" path="courseTypeCode">
+            <NSelect clearable v-model:value={searchForm.courseTypeCode} options={lessonType} />
+          </NFormItem>
+          <NFormItem label="搜索时间" path="time">
+            <NDatePicker
+              class={styles.datepicker}
+              v-model:value={searchForm.time}
+              type="daterange"
+              clearable
+            />
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+
+        <NSpace style={{ paddingBottom: '12px' }}>
+          <NButton
+            type="primary"
+            //v-auth="material/save1599948016487616513"
+            onClick={() => {
+              state.cityType = 'add'
+              state.materail = null
+              state.visiableCity = true
+            }}
+          >
+            添加素材
+          </NButton>
+        </NSpace>
+        <NDataTable loading={state.loading} columns={columns()} data={state.dataList}></NDataTable>
+        <Pagination
+          v-model:page={state.pagination.page}
+          v-model:pageSize={state.pagination.rows}
+          v-model:pageTotal={state.pagination.pageTotal}
+          onList={getList}
+          sync
+          saveKey="material-key"
+        ></Pagination>
+
+        <NModal
+          v-model:show={state.visiableCity}
+          preset="dialog"
+          showIcon={false}
+          title={state.cityType === 'add' ? '添加素材' : '修改素材'}
+          style={{ width: '700px' }}
+          onClose={() => {
+            state.isLook = false
+          }}
+        >
+          {state.visiableCity && (
+            <AddMaterial
+              item={state.materail}
+              isLook={state.isLook}
+              onClose={() => {
+                state.visiableCity = false
+              }}
+              onHandleSuccess={() => {
+                state.visiableCity = false
+                if (!state.materail) {
+                  state.pagination.page = 1
+                }
+                getList()
+              }}
+            />
+          )}
+        </NModal>
+      </>
+    )
+  }
+})

+ 5 - 0
src/views/educational-manage/educationalData.ts

@@ -0,0 +1,5 @@
+export enum materialType {
+  视频 = 'VIDEO',
+  图片 = 'IMG',
+  曲目 = 'SONG'
+}

+ 8 - 0
src/views/educational-manage/index.module.less

@@ -0,0 +1,8 @@
+.datepicker {
+  width: 240px !important;
+  :global {
+    .n-input {
+      width: auto !important;
+    }
+  }
+}

+ 54 - 0
src/views/educational-manage/index.tsx

@@ -0,0 +1,54 @@
+import { NTabPane, NTabs } from 'naive-ui'
+import { defineComponent, reactive } from 'vue'
+import CategoryList from './component/category-list'
+import MaterialList from './component/material-list'
+import styles from './index.module.less'
+import { getTabsCache, setTabsCaches } from '@/hooks/use-async'
+import { useRoute } from 'vue-router'
+export default defineComponent({
+  name: 'educational-manage',
+  setup() {
+    const state = reactive({
+      tabName: 'MaterialList' as 'MaterialList' | 'CategoryList'
+    })
+    const route = useRoute()
+
+    getTabsCache((val: any) => {
+      if (val.form.tabName) {
+        state.tabName = val.form.tabName
+      }
+    })
+    const setTabs = (val: any) => {
+      setTabsCaches(val, 'tabName', route)
+    }
+
+    return () => {
+      return (
+        <div class="system-menu-container">
+          <div class={['section-container']} style="padding-top: 0">
+            <NTabs
+              size="large"
+              v-model:value={state.tabName}
+              onUpdate:value={(val: any) => setTabs(val)}
+            >
+              <NTabPane
+                name="MaterialList"
+                tab="素材列表"
+                //v-auth="cityFeeSetting/page1597885815002091522"
+              >
+                <MaterialList />
+              </NTabPane>
+              <NTabPane
+                name="CategoryList"
+                tab="素材分类"
+                //v-auth="orchestraSubsidyStandard/page1597886618878201858"
+              >
+                <CategoryList />
+              </NTabPane>
+            </NTabs>
+          </div>
+        </div>
+      )
+    }
+  }
+})

+ 107 - 0
src/views/educational-manage/model/addCategory.tsx

@@ -0,0 +1,107 @@
+import {
+  FormInst,
+  FormItemRule,
+  FormRules,
+  NButton,
+  NCascader,
+  NForm,
+  NFormItem,
+  NInput,
+  NSpace,
+  NSpin,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { materialCategorySave, materialCategoryUpdate } from '../api'
+import { filterPointCategory } from '@/views/teaching-manage/unit-test'
+
+export default defineComponent({
+  name: 'addCategory',
+  emits: ['handleSuccess', 'close'],
+  props: ['item', 'list', 'isAdd'],
+  setup(props, { emit }) {
+    const message = useMessage()
+    const loading = ref(false)
+    const formRef = ref<FormInst | null>(null)
+    const parentList = ref([
+      { name: '顶级分类', id: '0' },
+      ...filterPointCategory(props.list, 'subMaterialCategoryList')
+    ] as any)
+    const saveModel = reactive({
+      id: props?.item?.id ? props?.item?.id : null,
+      name: props?.item?.name || '',
+      desc: props?.item?.desc || '', //描述
+      parentId: props?.item?.parentId || '0'
+    })
+    const rules: FormRules = {
+      name: [{ required: true, message: '请填写素材名称', trigger: ['blur', 'change'] }],
+      desc: [{ required: true, message: '请填写素材描述', trigger: ['blur', 'change'] }]
+    }
+    const submit = () => {
+      formRef.value?.validate(async (err) => {
+        if (!err) {
+          const params: any = {
+            ...saveModel
+          }
+          let res: any = null
+          console.log(params)
+          if (saveModel.id) {
+            res = await materialCategoryUpdate(params)
+          } else {
+            res = await materialCategorySave(params)
+          }
+          if (res?.code == 200) {
+            message.success('保存成功')
+            emit('handleSuccess')
+          } else {
+            message.warning('保存失败')
+          }
+        }
+      })
+    }
+    return () => (
+      <div>
+        <NSpin show={loading.value}>
+          <NForm
+            ref={formRef}
+            onSubmit={submit}
+            labelPlacement="top"
+            model={saveModel}
+            rules={rules}
+            require-mark-placement="left"
+          >
+            {props.isAdd && (
+              <NFormItem label="父级分类" path="parentId">
+                <NCascader
+                  placeholder="请选择父级分类"
+                  v-model:value={saveModel.parentId}
+                  options={parentList.value}
+                  checkStrategy="all"
+                  expandTrigger="hover"
+                  childrenField="subMaterialCategoryList"
+                  valueField="id"
+                  labelField="name"
+                  clearable
+                  filterable
+                />
+              </NFormItem>
+            )}
+
+            <NFormItem label="素材名称" path="name">
+              <NInput v-model:value={saveModel.name} />
+            </NFormItem>
+            <NFormItem label="分类描述" path="desc">
+              <NInput v-model:value={saveModel.desc} />
+            </NFormItem>
+          </NForm>
+          <NSpace justify="end">
+            <NButton onClick={() => emit('close')}>取消</NButton>
+            <NButton type="primary" onClick={submit}>
+              保存
+            </NButton>
+          </NSpace>
+        </NSpin>
+      </div>
+    )
+  }
+})

+ 285 - 0
src/views/educational-manage/model/addMaterial.tsx

@@ -0,0 +1,285 @@
+import {
+  FormInst,
+  FormItemRule,
+  FormRules,
+  NButton,
+  NCascader,
+  NDrawer,
+  NDrawerContent,
+  NForm,
+  NFormItem,
+  NInput,
+  NInputNumber,
+  NModal,
+  NRadio,
+  NRadioGroup,
+  NSelect,
+  NSpace,
+  NSpin,
+  NSwitch,
+  NTag,
+  NUpload,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, inject, onMounted, reactive, ref, watch } from 'vue'
+import { fetchMaterailDetail, materialSave, updateMaterailData } from '../api'
+import { materialType } from '../educationalData'
+import UploadFile from '@/components/upload-file'
+import { lessonType } from '@/views/knowledge-manage/knowledgeTypeData'
+import SelectMusicSheet from './selectMusicSheet'
+
+export default defineComponent({
+  name: 'addMaterial',
+  emits: ['handleSuccess', 'close'],
+  props: ['item', 'isLook'],
+  setup(props, { emit }) {
+    const message = useMessage()
+    const loading = ref(false)
+    const formRef = ref<FormInst | null>(null)
+    const formContentRef = ref<any>()
+    const saveModel = reactive({
+      name: '',
+      sn: '', //序号
+      materialCategoryId: '', //素材分类
+      // adviseStudyTimeSecond: null,
+      type: materialType.视频,
+      content: '', // 视频、图片链接或者是曲目编号
+      courseTypeCode: [], // 课程类型
+      enableFlag: true //启用状态
+    })
+    onMounted(async () => {
+      if (props?.item?.id) {
+        loading.value = true
+        const res: any = await fetchMaterailDetail(props.item.id)
+        if (res?.code == 200) {
+          Object.keys(saveModel).forEach((key: any) => {
+            if (res?.data?.[key]) {
+              if (key === 'adviseStudyTimeSecond') (saveModel as any)[key] = res.data[key] + ''
+              else if (key === 'courseTypeCode')
+                (saveModel as any)[key] = res.data[key]?.split(',') || []
+              else (saveModel as any)[key] = res.data[key]
+            }
+            if (key === 'enableFlag') (saveModel as any)[key] = res.data[key]
+          })
+          if (saveModel.type === 'SONG') {
+            musicOpentions.music = {
+              musicSheetName: res.data.contentDesc
+            }
+          }
+          console.log('🚀 ~ saveModel', saveModel)
+        }
+        loading.value = false
+      }
+    })
+    function validateContent(rule: FormItemRule, value: string) {
+      if (!value) {
+        return saveModel.type === materialType.视频
+          ? new Error('请上传视频')
+          : saveModel.type === materialType.图片
+          ? new Error('请上传图片')
+          : new Error('请选择曲目')
+      }
+      return true
+    }
+    function validateAdviseStudyTimeSecond(rule: FormItemRule, value: string) {
+      if (!value) {
+        return new Error('请填写时间')
+      }
+      if (!/^\+?[1-9]\d*$/.test(value)) {
+        return new Error('请填写正确的时间')
+      }
+      return true
+    }
+    function validateCourseTypeCode(rule: FormItemRule, value: []) {
+      if (Array.isArray(value) && !value.length) {
+        return new Error('请选择课程类型')
+      }
+      return true
+    }
+    const rules: FormRules = {
+      name: [{ required: true, message: '请填写素材名称', trigger: ['blur', 'change'] }],
+      sn: [{ required: true, message: '分段编号', trigger: ['blur', 'change'] }],
+      materialCategoryId: [
+        { required: true, message: '请选择素材分类', trigger: ['blur', 'change'] }
+      ],
+      adviseStudyTimeSecond: [
+        {
+          validator: validateAdviseStudyTimeSecond,
+          trigger: ['change']
+        }
+      ],
+      content: [{ validator: validateContent, trigger: ['blur', 'change', 'input'] }],
+      courseTypeCode: [
+        {
+          validator: validateCourseTypeCode,
+          message: '请选择课程类型',
+          trigger: ['blur', 'change']
+        }
+      ]
+    }
+    const categoryList = inject('categoryList', { list: [] }).list || []
+    const submit = () => {
+      formRef.value?.validate(async (err) => {
+        if (!err) {
+          const params: any = {
+            ...saveModel,
+            courseTypeCode: saveModel.courseTypeCode?.join(',') || ''
+          }
+          let res: any = null
+          if (props?.item?.id) {
+            params.id = props.item.id
+            res = await updateMaterailData(params)
+          } else {
+            res = await materialSave(params)
+          }
+          if (res?.code == 200) {
+            message.success('保存成功')
+            emit('handleSuccess')
+          } else {
+            message.warning('保存失败')
+          }
+        }
+      })
+    }
+
+    // 改变素材类型
+    const changeType = () => {
+      saveModel.content = ''
+      musicOpentions.music = ''
+    }
+
+    // 添加曲谱
+    const musicOpentions = reactive({
+      show: false,
+      music: '' as any
+    })
+    const hanldeSelectMusic = (musicItem: any) => {
+      musicOpentions.music = musicItem
+      saveModel.content = musicItem.id
+      musicOpentions.show = false
+      formContentRef.value?.restoreValidation()
+      // console.log('🚀 ~ musicItem', musicItem)
+    }
+    return () => (
+      <div>
+        <NSpin show={loading.value}>
+          <NForm
+            disabled={props.isLook ? true : false}
+            ref={formRef}
+            onSubmit={submit}
+            labelPlacement="top"
+            model={saveModel}
+            rules={rules}
+            require-mark-placement="left"
+          >
+            <NSpace justify="space-between" item-style={{ flex: 1 }}>
+              <NFormItem label="素材名称" path="name">
+                <NInput v-model:value={saveModel.name} />
+              </NFormItem>
+              <NFormItem label="分段编号" path="sn">
+                <NInput v-model:value={saveModel.sn} />
+              </NFormItem>
+            </NSpace>
+            <NSpace justify="space-between" item-style={{ flex: 1 }}>
+              <NFormItem label="素材分类" path="materialCategoryId">
+                <NCascader
+                  v-model:value={saveModel.materialCategoryId}
+                  options={categoryList}
+                  checkStrategy="child"
+                  childrenField="subMaterialCategoryList"
+                  expandTrigger="hover"
+                  valueField="id"
+                  labelField="name"
+                />
+              </NFormItem>
+              {/* <NFormItem label="建议学习时长" path="adviseStudyTimeSecond" required>
+                <NInput
+                  v-model:value={saveModel.adviseStudyTimeSecond}
+                  v-slots={{
+                    suffix: () => '秒'
+                  }}
+                />
+              </NFormItem> */}
+            </NSpace>
+            <NSpace justify="space-between" item-style={{ flex: 1 }}>
+              <NFormItem label="课程类型" path="courseTypeCode">
+                <NSelect
+                  multiple
+                  clearable
+                  disabled={props?.item ? true : false}
+                  value={saveModel.courseTypeCode}
+                  onUpdateValue={(val: any) => {
+                    saveModel.courseTypeCode = val
+                  }}
+                  options={lessonType}
+                />
+              </NFormItem>
+              <NFormItem label="是否启用">
+                <NSwitch v-model:value={saveModel.enableFlag} />
+              </NFormItem>
+            </NSpace>
+            <NFormItem label="素材类型" required labelPlacement="left" path="type">
+              <NRadioGroup v-model:value={saveModel.type} onUpdateValue={() => changeType()}>
+                <NRadio value={materialType.视频}>视频</NRadio>
+                <NRadio value={materialType.图片}>图片</NRadio>
+                <NRadio value={materialType.曲目}>曲目</NRadio>
+              </NRadioGroup>
+            </NFormItem>
+
+            <NFormItem label="上传素材" path="content" ref={formContentRef} required>
+              {saveModel.type === materialType.曲目 ? (
+                <NSpace>
+                  <NButton
+                    disabled={props.isLook}
+                    type="primary"
+                    onClick={() => (musicOpentions.show = true)}
+                  >
+                    选择曲谱
+                  </NButton>
+                  {musicOpentions.music && (
+                    <NInput readonly value={musicOpentions.music?.musicSheetName}></NInput>
+                  )}
+                </NSpace>
+              ) : (
+                <UploadFile
+                  accept={
+                    saveModel.type === materialType.视频
+                      ? 'video/*'
+                      : saveModel.type === materialType.图片
+                      ? 'image/*'
+                      : ''
+                  }
+                  path="courseware/"
+                  listType={saveModel.type === materialType.图片 ? 'image-card' : 'image'}
+                  v-model:fileList={saveModel.content}
+                  size={1024}
+                  disabled={props.isLook}
+                  onReadFileInputEventAsArrayBuffer={() => {
+                    formContentRef.value?.restoreValidation()
+                  }}
+                ></UploadFile>
+              )}
+            </NFormItem>
+          </NForm>
+          {props.isLook ? (
+            ''
+          ) : (
+            <NSpace justify="end">
+              <NButton type="default" onClick={() => emit('close')}>
+                取消
+              </NButton>
+              <NButton type="primary" onClick={submit}>
+                保存
+              </NButton>
+            </NSpace>
+          )}
+        </NSpin>
+        <NDrawer width="80vw" height="100vh" v-model:show={musicOpentions.show}>
+          <NDrawerContent title="选择曲谱" closable>
+            <SelectMusicSheet onSelect={(row: any) => hanldeSelectMusic(row)} />
+          </NDrawerContent>
+        </NDrawer>
+      </div>
+    )
+  }
+})

+ 11 - 0
src/views/educational-manage/model/index.module.less

@@ -0,0 +1,11 @@
+.selectMusicSheet{
+    :global{
+        .section-container.section-save-form{
+            padding: 0;
+            margin: 0;
+        }
+        .n-form.n-form--inline .n-form-item{
+            width: 30%;
+        }
+    }
+}

+ 245 - 0
src/views/educational-manage/model/selectMusicSheet.tsx

@@ -0,0 +1,245 @@
+import Pagination from '@/components/pagination'
+import SaveForm from '@/components/save-form'
+import { musicSheetPage, musicTagPage } from '@/views/content-manage/api'
+import { subjectBasicConfigPage } from '@/views/system-manage/api'
+import { NButton, NImage, NSpace, NTag, NDataTable, NFormItem, NInput, NSelect } from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { accompanimentTypeArray, audioTypeArray } from '@/utils/searchArray'
+import styles from './index.module.less'
+
+export default defineComponent({
+  name: 'selectMusicSheet',
+  emits: ['select'],
+  setup(props, { emit }) {
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        keyword: null,
+        musicTag: null,
+        musicSubject: null,
+        audioType: null,
+        accompanimentType: null,
+        status: null,
+        topFlag: null
+      },
+      dataList: [] as any,
+      subjectList: [] as any,
+      tagList: [] as any,
+      visiableMusic: false,
+      musicOperation: 'add',
+      musicData: {} as any
+    })
+
+    const columns = (): any => {
+      return [
+        {
+          title: '曲目名称',
+          key: 'id',
+          render(row: any) {
+            return `${row.musicSheetName}(${row.id})`
+          }
+        },
+        {
+          title: '曲目封面',
+          key: 'titleImg',
+          render(row: any) {
+            return <NImage width={35} height={35} src={row.titleImg} />
+          }
+        },
+        {
+          title: '分类',
+          key: 'musicSheetCategoriesName',
+        },
+        {
+          title: '伴奏类型',
+          key: 'accompanimentType',
+          render(row: any) {
+            return row.accompanimentType === 'HOMEMODE' ? '自制伴奏' : '普通伴奏'
+          }
+        },
+        {
+          title: '可用声部',
+          key: 'musicSubject',
+          render(row: any) {
+            return <NTag type="primary">{row.musicSubject}</NTag>
+          }
+        },
+        {
+          title: '播放类型',
+          key: 'audioType',
+          render(row: any) {
+            return (
+              <>
+                {row.audioType === 'MP3' && <NTag type="primary">MP3</NTag>}
+                {row.audioType === 'MIDI' && <NTag type="default">MIDI</NTag>}
+              </>
+            )
+          }
+        },
+        {
+          title: '能否转简谱',
+          key: 'notation',
+          render(row: any) {
+            return (
+              <NTag type={row.notation ? 'primary' : 'default'}>{row.notation ? '是' : '否'}</NTag>
+            )
+          }
+        },
+        // {
+        //   title: '排序',
+        //   key: 'sortNumber'
+        // },
+        {
+          title: '操作',
+          key: 'operation',
+          fixed: 'right',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton type="success" size="small" text onClick={() => emit('select', row)}>
+                  添加
+                </NButton>
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await musicSheetPage({ page: state.pagination.page, rows: state.pagination.rows, ...state.searchForm,status: 1 })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    // 获取标签
+    const getTagList = async () => {
+      try {
+        const { data } = await musicTagPage({ page: 1, rows: 999 })
+        const tempList = data.rows || []
+        tempList.forEach((item: any) => {
+          item.label = item.name
+          item.value = item.id
+        })
+        state.tagList = tempList
+      } catch {}
+    }
+
+    // 获取声部
+    const getSubjectList = async () => {
+      try {
+        const { data } = await subjectBasicConfigPage({ page: 1, rows: 999 })
+        const tempList = data.rows || []
+        tempList.forEach((item: any) => {
+          item.label = item.subjectName
+          item.value = item.subjectId
+        })
+        state.subjectList = tempList
+      } catch {}
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    onMounted(() => {
+      getTagList()
+      getSubjectList()
+      getList()
+    })
+    return () => (
+      <div class={styles.selectMusicSheet}>
+        <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          onSubmit={onSubmit}
+          saveKey="select-music-list"
+          onSetModel={(val: any) => (state.searchForm = val)}
+        >
+          <NFormItem label="关键字" path="keyword">
+            <NInput
+              placeholder="曲目编号/名称"
+              v-model:value={state.searchForm.keyword}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="标签" path="musicTag">
+            <NSelect
+              placeholder="请选择标签"
+              v-model:value={state.searchForm.musicTag}
+              options={state.tagList}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="声部" path="musicSubject">
+            <NSelect
+              placeholder="请选择声部"
+              v-model:value={state.searchForm.musicSubject}
+              options={state.subjectList}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="播放类型" path="audioType">
+            <NSelect
+              placeholder="请选择播放类型"
+              v-model:value={state.searchForm.audioType}
+              options={audioTypeArray}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="伴奏类型" path="accompanimentType">
+            <NSelect
+              placeholder="请选择伴奏类型"
+              v-model:value={state.searchForm.accompanimentType}
+              options={accompanimentTypeArray}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+        <NDataTable
+          loading={state.loading}
+          columns={columns()}
+          scrollX={1200}
+          data={state.dataList}
+        ></NDataTable>
+        <Pagination
+          v-model:page={state.pagination.page}
+          v-model:pageSize={state.pagination.rows}
+          v-model:pageTotal={state.pagination.pageTotal}
+          onList={getList}
+          sync
+          saveKey="select-music-list"
+        ></Pagination>
+      </div>
+    )
+  }
+})

+ 114 - 0
src/views/knowledge-manage/api.ts

@@ -0,0 +1,114 @@
+import request from '@/utils/request/index'
+
+/**
+ * 获取知识点列表
+ */
+export const fetchKnowledgeList = (data: object) => {
+  return request({
+    url: '/cbs-app/knowledgePoint/page',
+    method: 'post',
+    data: data
+  } as any)
+}
+/**
+ * 新增知识点
+ */
+export const knowledgePointSave = (data: object) => {
+  return request({
+    url: '/cbs-app/knowledgePoint/save',
+    method: 'post',
+    data: data
+  } as any)
+}
+
+// 删除知识点
+export const fetchDelKnowledge = (data: object) => {
+  return request({
+    url: '/cbs-app/knowledgePoint/remove?id=' + data,
+    method: 'post'
+  })
+}
+
+/**
+ * 知识点详情
+ */
+export const knowledgePointDetail = (data: any) => {
+  return request({
+    url: `/cbs-app/knowledgePoint/detail/${data}`,
+    method: 'get'
+  } as any)
+}
+/**
+ * 知识点修改
+ */
+export const knowledgePointUpdate = (data: any) => {
+  return request({
+    url: `/cbs-app/knowledgePoint/update`,
+    method: 'post',
+    data
+  } as any)
+}
+/**
+ * 素材知识点关联列表
+ */
+export const knowledgePointMaterialRelationPage = (data: any) => {
+  return request({
+    url: `/cbs-app/knowledgePointMaterialRelation/page`,
+    method: 'post',
+    data
+  } as any)
+}
+
+/**
+ * 素材知识点移动数据存储
+ */
+export const knowledgePointMaterialRelationUpdate = (data: any) => {
+  return request({
+    url: `/cbs-app/knowledgePointMaterialRelation/update`,
+    method: 'post',
+    data
+  } as any)
+}
+
+/**
+ * 知识点删除素材关联
+ */
+export const knowledgePointMaterialRelationRemove = (data: any) => {
+  return request({
+    url: `/cbs-app/knowledgePointMaterialRelation/remove`,
+    method: 'post',
+    data
+  } as any)
+}
+
+/**
+ * 知识点素材分页查询
+ */
+export const knowledgePointMaterialRelationMaterialPage = (data: any) => {
+  return request({
+    url: `/cbs-app/knowledgePointMaterialRelation/materialPage`,
+    method: 'post',
+    data
+  } as any)
+}
+
+/**
+ * 素材关联知识点
+ */
+export const knowledgePointMaterialRelationSave = (data: any) => {
+  return request({
+    url: `/cbs-app/knowledgePointMaterialRelation/save`,
+    method: 'post',
+    data
+  } as any)
+}
+
+/**
+ * 知识点启用停用
+ */
+export const knowledgePointStatus = (data: any) => {
+  return request({
+    url: `/cbs-app/knowledgePoint/status?id=${data}`,
+    method: 'post',
+  } as any)
+}

+ 18 - 0
src/views/knowledge-manage/knowledge-detail/index.module.less

@@ -0,0 +1,18 @@
+:global {
+  .n-cascader-submenu {
+    width: 260px !important;
+  }
+}
+.loading {
+  animation: rotate 1s infinite linear;
+  color: #2d8cf0;
+}
+@keyframes rotate {
+  50% {
+    transform: rotate(180deg);
+  }
+
+  100% {
+    transform: rotate(360deg);
+  }
+}

+ 232 - 0
src/views/knowledge-manage/knowledge-detail/index.tsx

@@ -0,0 +1,232 @@
+import TheLink from '@/components/TheLink'
+import {
+  DataTableColumn,
+  NButton,
+  NCard,
+  NDataTable,
+  NDrawer,
+  NDrawerContent,
+  NModal,
+  NPageHeader,
+  NSpace,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { InternalRowData } from 'naive-ui/es/data-table/src/interface'
+import { defineComponent, onMounted, reactive } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import {
+  knowledgePointMaterialRelationPage,
+  knowledgePointMaterialRelationRemove,
+  knowledgePointMaterialRelationUpdate
+} from '../api'
+import { knowledgeTypeData } from '../knowledgeTypeData'
+import AddMaterialKnowledge from './model/addMaterialKnowledge'
+
+export default defineComponent({
+  name: 'knowledgeDetail',
+  setup() {
+    const message = useMessage()
+    const dialog = useDialog()
+    const route = useRoute()
+    const router = useRouter()
+    const parentData = reactive<{ name: string; id: any; courseTypeCode: any }>({
+      name: route.query.name as string,
+      id: route.query.id,
+      courseTypeCode: route.query.courseTypeCode as any
+    })
+    const state = reactive({
+      loading: false,
+      dataList: [] as any,
+      checkList: [] as string[],
+      isMover: false,
+      visiableKnowledge: false,
+      modalType: 'add',
+      materail: null
+    })
+
+    const columns = (): DataTableColumn[] => {
+      return [
+        {
+          type: 'selection'
+        },
+        // {
+        //   title: '排序',
+        //   key: 'relOrder'
+        // },
+        {
+          title: '素材名称',
+          key: 'materialName',
+          render(row: any) {
+            return (
+              <TheLink
+                to={{ path: '/educationalManage/educationalManage', query: { id: row.materialId } }}
+              >
+                {row.materialName}
+              </TheLink>
+            )
+          }
+        },
+        {
+          title: '素材分类',
+          key: 'materialCategoryName'
+        },
+        {
+          title: '分段编号',
+          key: 'sn',
+          render(row: any) {
+            return <TheLink to={{ path: '/educationalManage/educationalManage' }}>{row.sn}</TheLink>
+          }
+        },
+        {
+          title: '课程类型',
+          key: 'courseTypeName'
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          fixed: 'right',
+          render(row: InternalRowData, rowIndex: number) {
+            return (
+              <NSpace>
+                <NButton
+                  text
+                  type="primary"
+                  disabled={rowIndex === 0}
+                  onClick={() => handleRowMove('up', rowIndex)}
+                >
+                  上移
+                </NButton>
+                <NButton
+                  text
+                  type="primary"
+                  disabled={rowIndex === state.dataList.length - 1}
+                  onClick={() => handleRowMove('down', rowIndex)}
+                >
+                  下移
+                </NButton>
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+    const getList = async () => {
+      state.loading = true
+      try {
+        const { data } = await knowledgePointMaterialRelationPage({
+          knowledgePointId: parentData.id,
+          page: 1,
+          rows: 9999
+        })
+        if (Array.isArray(data?.rows)) {
+          state.dataList = data.rows.map((n: any, i: number) => ({ ...n, relOrder: i + 1 }))
+        }
+      } catch {}
+      state.loading = false
+    }
+
+    onMounted(() => {
+      getList()
+    })
+
+    // 批量删除
+    const hanldeDelete = () => {
+      const d = dialog.warning({
+        title: '警告',
+        content: '是否确认删除这些素材知识点关联?',
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          d.loading = true
+          const res: any = await knowledgePointMaterialRelationRemove(state.checkList)
+          d.loading = false
+          if (res?.code == 200) {
+            state.checkList = []
+            message.success('删除成功')
+            getList()
+          }
+        }
+      })
+    }
+
+    // 表格行移动
+    const handleRowMove = (type: 'up' | 'down', index: number) => {
+      if (type === 'up') {
+        if (index != 0) {
+          state.dataList[index] = state.dataList.splice(index - 1, 1, state.dataList[index])[0]
+        } else {
+          state.dataList.push(state.dataList.shift())
+        }
+      } else {
+        if (index != state.dataList.length - 1) {
+          state.dataList[index] = state.dataList.splice(index + 1, 1, state.dataList[index])[0]
+        } else {
+          state.dataList.unshift(state.dataList.splice(index, 1)[0])
+        }
+      }
+      state.isMover = true
+      handleSaveRow()
+    }
+    // 记录排序后的位置
+    const handleSaveRow = () => {
+      const body = state.dataList.map((n: any, i: number) => {
+        return {
+          knowledgePointId: parentData.id,
+          materialId: n.materialId,
+          relOrder: i + 1,
+          id: n.id
+        }
+      })
+      knowledgePointMaterialRelationUpdate(body)
+    }
+    return () => (
+      <div class="section-container">
+        <NPageHeader
+          on-back={() => router.push('/educationalManage/knowledgeManage')}
+          title={parentData.name}
+        ></NPageHeader>
+        <NSpace style={{ padding: '15px 0' }}>
+          <NButton
+            //v-auth="knowledgePointMaterialRelation/save1604770193787420673"
+            type="primary"
+            onClick={() => (state.visiableKnowledge = true)}
+          >
+            新增素材
+          </NButton>
+          <NButton
+            //v-auth="knowledgePointMaterialRelation/remove1604770313480273922"
+            type="error"
+            disabled={state.checkList.length ? false : true}
+            onClick={() => hanldeDelete()}
+          >
+            批量删除
+          </NButton>
+          {/* <NButton>保存移动的排序</NButton> */}
+        </NSpace>
+        <NDataTable
+          loading={state.loading}
+          maxHeight="calc(100vh - 290px)"
+          rowKey={(row: any) => row.id}
+          columns={columns()}
+          data={state.dataList}
+          onUpdateCheckedRowKeys={(value: any) => {
+            state.checkList = value
+          }}
+        ></NDataTable>
+
+        <NDrawer width="80vw" v-model:show={state.visiableKnowledge}>
+          <NDrawerContent title="新增素材关联" closable>
+            <AddMaterialKnowledge
+              parentData={parentData}
+              onClose={() => {
+                state.visiableKnowledge = false
+                getList()
+              }}
+            />
+          </NDrawerContent>
+        </NDrawer>
+      </div>
+    )
+  }
+})

+ 288 - 0
src/views/knowledge-manage/knowledge-detail/model/addMaterialKnowledge.tsx

@@ -0,0 +1,288 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  DataTableColumn,
+  NButton,
+  NCascader,
+  NCol,
+  NDataTable,
+  NDatePicker,
+  NFormItem,
+  NInput,
+  NModal,
+  NRow,
+  NSelect,
+  NSpace,
+  NTag,
+  useDialog,
+  useMessage,
+  NEmpty,
+  NIcon
+} from 'naive-ui'
+import { defineComponent, inject, onMounted, reactive, ref, provide } from 'vue'
+import styles from '../index.module.less'
+import { InternalRowData } from 'naive-ui/es/data-table/src/interface'
+import { lessonType } from '@/views/knowledge-manage/knowledgeTypeData'
+import { fetchCategoryList } from '@/views/educational-manage/api'
+import {
+  knowledgePointMaterialRelationMaterialPage,
+  knowledgePointMaterialRelationSave
+} from '../../api'
+
+const classType: { [_: string]: any } = {
+  VIDEO: '视频',
+  IMG: '图片',
+  SONG: '曲目'
+}
+export default defineComponent({
+  name: 'addMaterialKnowledge',
+  props: {
+    parentData: {
+      type: Object,
+      default: () => {}
+    }
+  },
+  emits: ['close'],
+  setup(props, { emit }) {
+    console.log('🚀 ~ props', props.parentData)
+    const dialog = useDialog()
+    const message = useMessage()
+    const state = reactive({
+      loading: false,
+      saveLoading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 10
+      },
+      dataList: [] as any,
+      checkList: [] as string[]
+    })
+    const searchForm = reactive({
+      keyword: '',
+      materialCategoryId: null
+    })
+
+    // 获取分类列表
+    const category = reactive({
+      list: [] as any,
+      index: -1,
+      loading: false
+    })
+    const getCategoryList = async () => {
+      category.loading = true
+      try {
+        const { data } = await fetchCategoryList({ page: 1 })
+        category.list = data?.rows || []
+      } catch {}
+      category.loading = false
+    }
+
+    const columns = (): DataTableColumn[] => {
+      return [
+        {
+          type: 'selection'
+        },
+        {
+          title: '素材名称',
+          key: 'name',
+          fixed: 'left'
+        },
+        // {
+        //   title: '编号',
+        //   key: 'id'
+        // },
+
+        {
+          title: '素材分类',
+          key: 'materialCategoryName'
+        },
+        {
+          title: '分段编号',
+          key: 'sn'
+        },
+        {
+          title: '素材类型',
+          key: 'type',
+          render(row: InternalRowData) {
+            return classType[row.type as string]
+          }
+        },
+        {
+          title: '课程类型',
+          key: 'courseTypeName'
+        }
+        // {
+        //   title: '建议学习时长',
+        //   key: 'adviseStudyTimeSecond',
+        //   render(row: InternalRowData) {
+        //     return row.adviseStudyTimeSecond ? row.adviseStudyTimeSecond + '秒' : ''
+        //   }
+        // }
+      ]
+    }
+
+    const getData = async () => {
+      const body = {
+        knowledgePointId: props.parentData.id,
+        ...searchForm,
+        courseTypeCode: props.parentData.courseTypeCode,
+        page: state.pagination.page,
+        rows: state.pagination.rows
+      }
+      return (await knowledgePointMaterialRelationMaterialPage(body)) as any
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await getData()
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    onMounted(() => {
+      getCategoryList()
+      getList()
+    })
+
+    // 素材关联知识点
+    const handleSave = async () => {
+      state.saveLoading = true
+      const body = {
+        knowledgePointId: props.parentData.id,
+        materialId: state.checkList.join(',')
+      }
+      await knowledgePointMaterialRelationSave(body)
+      state.saveLoading = false
+      message.success('保存成功')
+      emit('close')
+    }
+
+    return () => (
+      <>
+        <SaveForm
+          style={{ padding: 0, boxShadow: 'none' }}
+          ref={saveForm}
+          label-width=""
+          model={searchForm}
+          onSubmit={onSubmit}
+          onSetModel={(val: any) => Object.assign(searchForm, val)}
+          saveKey="addMaterialKnowledge-key"
+        >
+          <NRow gutter={14}>
+            <NCol span={6}>
+              <NFormItem label="素材名称/编号" path="keyword">
+                <NInput
+                  v-model:value={searchForm.keyword}
+                  onKeydown={(e) => {
+                    if (e.code === 'Enter') {
+                      onSubmit()
+                    }
+                  }}
+                  clearable
+                />
+              </NFormItem>
+            </NCol>
+            <NCol span={6}>
+              <NFormItem label="素材分类" path="materialCategoryId">
+                <NCascader
+                  v-model:value={searchForm.materialCategoryId}
+                  options={category.list}
+                  checkStrategy="child"
+                  childrenField="subMaterialCategoryList"
+                  expandTrigger="hover"
+                  valueField="id"
+                  labelField="name"
+                  clearable
+                  v-slots={{
+                    empty: () =>
+                      category.loading ? (
+                        <>
+                          <NEmpty
+                            description="加载中"
+                            v-slots={{
+                              icon: () => (
+                                <NIcon size="30" class={styles.loading}>
+                                  <svg
+                                    xmlns="http://www.w3.org/2000/svg"
+                                    xmlns:xlink="http://www.w3.org/1999/xlink"
+                                    viewBox="0 0 1024 1024"
+                                  >
+                                    <path
+                                      d="M988 548c-19.9 0-36-16.1-36-36c0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 0 0-94.3-139.9a437.71 437.71 0 0 0-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7c26.7 63.1 40.2 130.2 40.2 199.3c.1 19.9-16 36-35.9 36z"
+                                      fill="currentColor"
+                                    ></path>
+                                  </svg>
+                                </NIcon>
+                              )
+                            }}
+                          ></NEmpty>
+                        </>
+                      ) : null
+                  }}
+                />
+              </NFormItem>
+            </NCol>
+            {/* <NCol span={6}>
+              <NFormItem label="课程类型" path="courseTypeCode">
+                <NSelect clearable v-model:value={searchForm.courseTypeCode} options={lessonType} />
+              </NFormItem>
+            </NCol> */}
+            <NCol span={6}>
+              <NFormItem>
+                <NSpace>
+                  <NButton type="primary" onClick={onSearch}>
+                    搜索
+                  </NButton>
+                  <NButton type="default" onClick={onBtnReset}>
+                    重置
+                  </NButton>
+                </NSpace>
+              </NFormItem>
+            </NCol>
+          </NRow>
+        </SaveForm>
+        <NDataTable
+          loading={state.loading}
+          maxHeight="calc(100vh - 270px)"
+          rowKey={(row: any) => row.materialId}
+          columns={columns()}
+          data={state.dataList}
+          v-model:checkedRowKeys={state.checkList}
+        ></NDataTable>
+        <Pagination
+          v-model:page={state.pagination.page}
+          v-model:pageTotal={state.pagination.pageTotal}
+          v-model:pageSize={state.pagination.rows}
+          onList={getList}
+        ></Pagination>
+        <NButton
+          type="primary"
+          disabled={state.checkList.length ? false : true}
+          loading={state.saveLoading}
+          onClick={() => handleSave()}
+        >
+          保 存
+        </NButton>
+      </>
+    )
+  }
+})

+ 151 - 0
src/views/knowledge-manage/knowledge-list/component/knowledgeModel.tsx

@@ -0,0 +1,151 @@
+import {
+  FormInst,
+  FormItemRule,
+  FormRules,
+  NButton,
+  NForm,
+  NFormItem,
+  NInput,
+  NSpace,
+  NSelect,
+  NSpin,
+  useMessage,
+  NRadio,
+  NRadioGroup,
+  NInputNumber
+} from 'naive-ui'
+import { SelectMixedOption } from 'naive-ui/es/select/src/interface'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { knowledgePointSave, knowledgePointUpdate } from '../../api'
+import { knowledgeTypeData } from '../../knowledgeTypeData'
+
+export default defineComponent({
+  name: 'KnowledgeModel',
+  emits: ['handleSuccess', 'close'],
+  props: ['item'],
+  setup(props, { emit }) {
+    const message = useMessage()
+    const loading = ref(false)
+    const formRef = ref<FormInst | null>(null)
+    const saveModel = reactive({
+      id: props?.item?.id ? props?.item?.id : null,
+      name: props?.item?.name || '',
+      courseTypeCode: props?.item?.courseTypeCode?.split(',') || [], //课程类型
+      autoPlayFlag: props?.item?.autoPlayFlag == true ? 1 : 0,
+      totalMaterialTimeSecond: props?.item?.totalMaterialTimeSecond,
+      parentId: props?.item?.parentId || 0 // 默认第一层级
+    })
+    function validateCourseTypeCode(rule: FormItemRule, value: []) {
+      if (Array.isArray(value) && !value.length) {
+        return new Error('请选择课程类型')
+      }
+      return true
+    }
+    const rules: FormRules = {
+      name: [{ required: true, message: '请输入知识点名称', trigger: ['blur', 'change'] }],
+      courseTypeCode: [
+        {
+          validator: validateCourseTypeCode,
+          message: '请选择课程类型',
+          trigger: ['blur', 'change']
+        }
+      ]
+    }
+
+    const handleOptions = () => {
+      let _obj: SelectMixedOption[] = []
+      Object.keys(knowledgeTypeData).map((key) => {
+        _obj.push({
+          label: knowledgeTypeData[key],
+          value: key
+        })
+      })
+      return _obj
+    }
+    const hanldeAdd = () => {
+      formRef.value?.validate(async (err) => {
+        console.log('🚀 ~ err', err)
+        if (!err) {
+          const params: any = {
+            ...saveModel,
+            courseTypeCode: saveModel.courseTypeCode?.join(',') || ''
+          }
+          let res: any = null
+          let isCreate = ''
+          if (saveModel.id) {
+            res = await knowledgePointUpdate(params)
+          } else {
+            res = await knowledgePointSave(params)
+            isCreate = res.data
+          }
+          if (res?.code == 200) {
+            message.success('保存成功')
+            emit('handleSuccess', isCreate)
+          } else {
+            message.warning('保存失败')
+          }
+        }
+      })
+    }
+    return () => (
+      <div>
+        <NSpin show={loading.value}>
+          <NForm
+            ref={formRef}
+            labelPlacement="top"
+            model={saveModel}
+            rules={rules}
+            require-mark-placement="left"
+          >
+            <NFormItem label="知识点名称" path="name">
+              <NInput v-model:value={saveModel.name} />
+            </NFormItem>
+            <NFormItem label="课程类型" path="courseTypeCode">
+              <NSelect
+                multiple
+                disabled={props?.item ? true : false}
+                v-model:value={saveModel.courseTypeCode}
+                options={handleOptions()}
+                clearable
+              />
+            </NFormItem>
+            <NFormItem
+              label="建议学习时长"
+              path="totalMaterialTimeSecond"
+              rule={[
+                {
+                  required: true,
+                  type: 'number',
+                  min: 0,
+                  message: '请输入正确建议学习时长',
+                  trigger: 'blur'
+                }
+              ]}
+            >
+              <NInputNumber
+                placeholder="请输入建议学习时长"
+                showButton={false}
+                v-model:value={saveModel.totalMaterialTimeSecond}
+                v-slots={{
+                  suffix: () => 's(秒)'
+                }}
+              />
+            </NFormItem>
+            <NFormItem label="自动播放下一知识点" path="autoPlayFlag">
+              <NRadioGroup v-model:value={saveModel.autoPlayFlag}>
+                <NRadio value={1}>是</NRadio>
+                <NRadio value={0}>否</NRadio>
+              </NRadioGroup>
+            </NFormItem>
+          </NForm>
+          <NSpace justify="end">
+            <NButton onClick={() => emit('close')}>取消</NButton>
+            <NButton type="primary" onClick={hanldeAdd}>
+              保存
+            </NButton>
+          </NSpace>
+        </NSpin>
+      </div>
+    )
+  }
+})

+ 396 - 0
src/views/knowledge-manage/knowledge-list/index.tsx

@@ -0,0 +1,396 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  DataTableColumn,
+  NButton,
+  NDataTable,
+  NDatePicker,
+  NDescriptions,
+  NDescriptionsItem,
+  NFormItem,
+  NInput,
+  NModal,
+  NSelect,
+  NSpace,
+  NTag,
+  NTooltip,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref, provide } from 'vue'
+import { employeeArray } from '@/utils/searchArray'
+import dayjs from 'dayjs'
+import { fetchKnowledgeList, fetchDelKnowledge, knowledgePointStatus } from '../api'
+import { InternalRowData } from 'naive-ui/es/data-table/src/interface'
+import { knowledgeTypeData, lessonType } from '../knowledgeTypeData'
+import KnowledgeModel from './component/knowledgeModel'
+import { useRouter } from 'vue-router'
+import { filterTimes, getTimes } from '@/utils/dateUtil'
+export default defineComponent({
+  name: 'knowledge-manage',
+  setup() {
+    const router = useRouter()
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      dataList: [] as any,
+      visiableKnowledge: false,
+      modalType: 'add',
+      materail: null as any
+    })
+
+    const searchForm = reactive({
+      keyword: null,
+      time: null,
+      courseTypeCode: null,
+      operatorId: null,
+      enableFlag: null
+    })
+    const dialog = useDialog()
+    const message = useMessage()
+
+    // 获取知识点列表
+    const knowledge = reactive({
+      list: [] as any,
+      index: -1
+    })
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { time, ...resset } = searchForm
+        const { data } = await fetchKnowledgeList({
+          ...resset,
+          ...filterTimes(time, ['startUpdateTime', 'endUpdateTime']),
+          page: state.pagination.page,
+          rows: state.pagination.rows
+        })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    onMounted(() => {
+      getList()
+    })
+
+    // 打开详情
+    const openDetail = (row: any) => {
+      router.push({
+        path: '/knowledgeDetail',
+        query: {
+          id: row.id,
+          name: row.name,
+          courseTypeCode: row.courseTypeCode
+        }
+      })
+    }
+
+    const columns = (): DataTableColumn[] => {
+      return [
+        {
+          title: '知识点',
+          key: 'id',
+          render: (row: any) => (
+            <div style="display: inline-flex;vertical-align: middle;">
+              <NDescriptions labelPlacement="left" column={1}>
+                <NDescriptionsItem label="知识点名称">{row.name}</NDescriptionsItem>
+                <NDescriptionsItem label="知识点编号">{row.id}</NDescriptionsItem>
+              </NDescriptions>
+            </div>
+          )
+        },
+        {
+          title: '课程类型',
+          key: 'courseTypeName',
+          render: (row: any) => (
+            <>
+              <NDescriptions labelPlacement="left" column={1}>
+                <NDescriptionsItem label="课程类型">{row.courseTypeName}</NDescriptionsItem>
+                <NDescriptionsItem label="自动播放">
+                  {row.autoPlayFlag == 1 ? '是' : '否'}
+                </NDescriptionsItem>
+              </NDescriptions>
+            </>
+          )
+        },
+        {
+          title: '学习信息',
+          key: 'totalMaterialNum',
+          render: (row: any) => (
+            <>
+              <NDescriptions labelPlacement="left" column={1}>
+                <NDescriptionsItem label="素材数量">{row.totalMaterialNum}</NDescriptionsItem>
+                <NDescriptionsItem label="建议时长">
+                  {(row.totalMaterialTimeSecond || 0) + 's'}
+                </NDescriptionsItem>
+              </NDescriptions>
+            </>
+          )
+        },
+        {
+          title: '上传信息',
+          key: 'updateTime',
+          render: (row: any) => (
+            <>
+              <NDescriptions labelPlacement="left" column={1}>
+                <NDescriptionsItem label="修改人">{row.operatorName}</NDescriptionsItem>
+                <NDescriptionsItem label="修改时间">{row.updateTime}</NDescriptionsItem>
+              </NDescriptions>
+            </>
+          )
+        },
+        {
+          title: '状态',
+          key: 'enableFlag',
+          render(row: any) {
+            return (
+              <NTag type={row.enableFlag ? 'success' : 'warning'}>
+                {row.enableFlag ? '已启用' : '未启用'}
+              </NTag>
+            )
+          }
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          fixed: 'right',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  //v-auth="knowledgePoint/status1605123728664281090"
+                  type={row.enableFlag ? 'error' : 'success'}
+                  size="small"
+                  text
+                  onClick={() => changeState(row)}
+                >
+                  {row.enableFlag ? '停用' : '启用'}
+                </NButton>
+                {/* 一级分类下是否有子项 */}
+                {!(row.children && row.children.length > 0) && (
+                  <NButton type="primary" size="small" text onClick={() => openDetail(row)}>
+                    详情
+                  </NButton>
+                )}
+                {/* 是否是一级分类,一级分类下面是否有素材 */}
+                {row.parentId <= 0 && !row.materialFlag && (
+                  <NButton
+                    type="primary"
+                    size="small"
+                    text
+                    //v-auth="knowledgePoint/update1604688172914417665"
+                    onClick={() => {
+                      state.modalType = 'add-child'
+                      state.visiableKnowledge = true
+                      state.materail = {
+                        parentId: row.id,
+                        courseTypeCode: row.courseTypeCode
+                      }
+                    }}
+                  >
+                    添加子项
+                  </NButton>
+                )}
+
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="knowledgePoint/update1604688172914417665"
+                  onClick={() => {
+                    state.modalType = 'update'
+                    state.visiableKnowledge = true
+                    state.materail = row
+                  }}
+                >
+                  修改
+                </NButton>
+                <NButton
+                  type="primary"
+                  size="small"
+                  disabled={row.enableFlag ? true : false}
+                  text
+                  //v-auth="knowledgePoint/remove1604688068350418946"
+                  onClick={() => {
+                    handleDelete(row)
+                  }}
+                >
+                  删除
+                </NButton>
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    // 启用停用
+    const changeState = async (row: any, open = true) => {
+      dialog.warning({
+        title: '警告',
+        content: `是否${row.enableFlag ? '停用' : '启用'}此知识点?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          console.log('row', row)
+          const res = await knowledgePointStatus(row.id)
+          if (res.data) {
+            getList()
+            message.success('修改成功')
+          }
+        }
+      })
+    }
+
+    // 删除
+    const handleDelete = async (row: any) => {
+      dialog.warning({
+        title: '警告',
+        content: '是否确认删除此知识点?',
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          console.log('row', row)
+          const res = await fetchDelKnowledge(row.id)
+          if (res.data) {
+            onSubmit()
+            message.success('删除成功')
+          }
+        }
+      })
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    return () => (
+      <div class="system-menu-container">
+        <h2>知识点管理</h2>
+        <div class={['section-container']}>
+          <SaveForm
+            ref={saveForm}
+            model={searchForm}
+            onSubmit={onSubmit}
+            onSetModel={(val: any) => Object.assign(searchForm, val)}
+          >
+            <NFormItem path="search" label="关键字">
+              <NInput v-model:value={searchForm.keyword} placeholder="知识组名称/编号" clearable />
+            </NFormItem>
+            <NFormItem label="课程类型" path="courseTypeCode">
+              <NSelect clearable v-model:value={searchForm.courseTypeCode} options={lessonType} />
+            </NFormItem>
+            <NFormItem path="time" label="时间">
+              <NDatePicker v-model:value={searchForm.time} type="daterange" clearable></NDatePicker>
+            </NFormItem>
+            <NFormItem label="状态" path="enableFlag">
+              <NSelect
+                placeholder="请选择状态"
+                clearable
+                v-model:value={searchForm.enableFlag}
+                options={
+                  [
+                    { label: '已启用', value: 1 },
+                    { label: '未启用', value: 0 }
+                  ] as any
+                }
+              />
+            </NFormItem>
+            {/* <NFormItem path="status">
+            <NSelect
+              v-model:value={searchForm.operatorId}
+              placeholder="上传人"
+              options={employeeArray}
+              clearable
+            />
+          </NFormItem> */}
+            <NFormItem>
+              <NSpace>
+                <NButton type="primary" onClick={onSearch}>
+                  搜索
+                </NButton>
+                <NButton type="default" onClick={onBtnReset}>
+                  重置
+                </NButton>
+              </NSpace>
+            </NFormItem>
+          </SaveForm>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="knowledgePoint/save1604687872153460738"
+              onClick={() => {
+                state.visiableKnowledge = true
+                state.modalType = 'add'
+                state.materail = null
+              }}
+            >
+              新增知识点
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+            children-key="children"
+            default-expand-all={false}
+            row-key={(row: any) => row.id}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+          ></Pagination>
+
+          <NModal
+            v-model:show={state.visiableKnowledge}
+            preset="dialog"
+            showIcon={false}
+            title={state.modalType !== 'update' ? '新增知识点' : '修改知识点'}
+            style={{ width: '500px' }}
+            onClose={() => {
+              state.materail = null
+            }}
+          >
+            <KnowledgeModel
+              item={state.materail}
+              onHandleSuccess={(val: any) => {
+                if (val) {
+                  openDetail(val)
+                  return
+                }
+                console.log(val)
+                state.visiableKnowledge = false
+                getList()
+              }}
+              onClose={() => {
+                state.visiableKnowledge = false
+                state.materail = null
+              }}
+            ></KnowledgeModel>
+          </NModal>
+        </div>
+      </div>
+    )
+  }
+})

+ 23 - 0
src/views/knowledge-manage/knowledgeTypeData.ts

@@ -0,0 +1,23 @@
+interface knowledgeType {
+  [key: string]: string
+}
+export const knowledgeTypeData: knowledgeType = {
+  PERCUSSION_SINGLE: '打击乐',
+  FLUTE_SINGLE: '长笛',
+  SAX_SINGLE: '萨克斯',
+  CLARINET_SINGLE: '单簧管',
+  TRUMPET_SINGLE: '小号',
+  TROMBONE_SINGLE: '长号',
+  HORN_SINGLE: '圆号',
+  BARITONE_TUBA_SINGLE: '上低音号-大号',
+  MUSIC_THEORY: '乐理',
+  INSTRUMENTAL_ENSEMBLE: '合奏'
+}
+
+export const lessonType = Object.keys(knowledgeTypeData).map((key: string) => {
+  return { label: knowledgeTypeData[key], value: key }
+})
+
+export const getLessonType = (key: any) => {
+  return knowledgeTypeData[key] || ''
+}

+ 46 - 0
src/views/login-log/api.ts

@@ -0,0 +1,46 @@
+
+import request from '@/utils/request/index'
+
+/**
+ * @description: 获取登录日志
+ */
+export const sysUserLoginLogPage = (params: object) => {
+  return request({
+    url: '/cbs-app/sysUserLoginLog/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 获取登录统计
+ */
+export const sysUserLoginPage = (params: object) => {
+  return request({
+    url: '/cbs-app/sysUserLogin/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 获取登录设备
+ */
+export const sysUserDevicePage = (params: object) => {
+  return request({
+    url: '/cbs-app/sysUserDevice/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 获取操作日志
+ */
+export const sysUserAuditLogPage = (params: object) => {
+  return request({
+    url: '/cbs-app/sysUserAuditLog/page',
+    method: 'post',
+    data: params
+  } as any)
+}

+ 87 - 0
src/views/login-log/component/login-device.tsx

@@ -0,0 +1,87 @@
+import Pagination from '@/components/pagination'
+import { NDataTable, NIcon, NTooltip } from 'naive-ui'
+import { HelpCircleOutline } from '@vicons/ionicons5'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { sysUserDevicePage } from '../api'
+import { filterClientType } from '@/utils/filters'
+
+export default defineComponent({
+  name: 'login-device',
+  setup() {
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        name: ''
+      },
+      dataList: [] as any,
+      visiableCity: false
+    })
+
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '用户名称',
+          key: 'nickname'
+        },
+        {
+          title: '客户端',
+          key: 'clientType',
+          render(row: any) {
+            return filterClientType(row.clientType)
+          }
+        },
+        {
+          title: '设备型号',
+          key: 'userAgent',
+          width: '400'
+        },
+        {
+          title: '绑定时间',
+          key: 'bindTime'
+        },
+        {
+          title: '登录时间',
+          key: 'updateTime'
+        }
+      ]
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await sysUserDevicePage({
+          ...state.pagination
+        })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {}
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <>
+        <NDataTable loading={state.loading} columns={columns()} data={state.dataList}></NDataTable>
+        <Pagination
+          v-model:page={state.pagination.page}
+          v-model:pageSize={state.pagination.rows}
+          v-model:pageTotal={state.pagination.pageTotal}
+          onList={getList}
+          sync
+          saveKey="login-device"
+        ></Pagination>
+      </>
+    )
+  }
+})

+ 93 - 0
src/views/login-log/component/login-log.tsx

@@ -0,0 +1,93 @@
+import Pagination from '@/components/pagination'
+import { NDataTable, NIcon, NTooltip } from 'naive-ui'
+import { HelpCircleOutline } from '@vicons/ionicons5'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { sysUserLoginLogPage } from '../api'
+import { filterClientType } from '@/utils/filters'
+
+export default defineComponent({
+  name: 'login-log',
+  setup() {
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        name: ''
+      },
+      dataList: [] as any,
+      visiableCity: false
+    })
+
+    const columns = () => {
+      return [
+        // {
+        //   type: 'expand',
+        //   renderExpand: (rowData: any) => {
+        //     return `请求参数:${rowData.params} `
+        //   }
+        // },
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '登录用户',
+          key: 'nickname'
+        },
+        {
+          title: '客户端',
+          key: 'clientType',
+          render(row: any) {
+            return filterClientType(row.clientType)
+          }
+        },
+        {
+          title: '登录IP',
+          key: 'loginIp'
+        },
+        {
+          title: '登录时间',
+          key: 'loginTime'
+        },
+        {
+          title: '登录地点',
+          key: 'loginRegion'
+        }
+      ]
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await sysUserLoginLogPage({
+          ...state.pagination
+        })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+
+        state.dataList = data.rows || []
+      } catch {}
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <>
+        <NDataTable loading={state.loading} columns={columns()} data={state.dataList}></NDataTable>
+        <Pagination
+          v-model:page={state.pagination.page}
+          v-model:pageSize={state.pagination.rows}
+          v-model:pageTotal={state.pagination.pageTotal}
+          onList={getList}
+          sync
+          saveKey="login-log"
+        ></Pagination>
+      </>
+    )
+  }
+})

+ 110 - 0
src/views/login-log/component/login-statistics.tsx

@@ -0,0 +1,110 @@
+import Pagination from '@/components/pagination'
+import { NDataTable, NIcon, NTooltip } from 'naive-ui'
+import { HelpCircleOutline } from '@vicons/ionicons5'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { sysUserLoginPage } from '../api'
+import { filterClientType } from '@/utils/filters'
+
+export default defineComponent({
+  name: 'login-statistics',
+  setup() {
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        name: ''
+      },
+      dataList: [] as any,
+      visiableCity: false
+    })
+
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '登录用户',
+          key: 'nickname'
+        },
+        {
+          title: '客户端',
+          key: 'clientType',
+          render(row: any) {
+            return filterClientType(row.clientType)
+          }
+        },
+        {
+          title: '登录总次数',
+          key: 'loginCount'
+        },
+        {
+          title: '最后登录IP',
+          key: 'lastLoginIp'
+        },
+        {
+          title: '最后登录时间',
+          key: 'lastLoginTime'
+        },
+        {
+          title: '锁定日期',
+          key: 'lockDate'
+        },
+        {
+          title(column: any) {
+            return (
+              <NTooltip>
+                {{
+                  trigger: () => (
+                    <span style={{ display: 'flex', alignItems: 'center' }}>
+                      锁定时间
+                      <NIcon size={18}>
+                        <HelpCircleOutline />
+                      </NIcon>
+                    </span>
+                  ),
+                  default: () => '超过锁定时间自动解锁'
+                }}
+              </NTooltip>
+            )
+          },
+          key: 'lockTime'
+        }
+      ]
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await sysUserLoginPage({
+          ...state.pagination
+        })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {}
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <>
+        <NDataTable loading={state.loading} columns={columns()} data={state.dataList}></NDataTable>
+        <Pagination
+          v-model:page={state.pagination.page}
+          v-model:pageSize={state.pagination.rows}
+          v-model:pageTotal={state.pagination.pageTotal}
+          onList={getList}
+          sync
+          saveKey="login-statistics"
+        ></Pagination>
+      </>
+    )
+  }
+})

+ 63 - 0
src/views/login-log/index.tsx

@@ -0,0 +1,63 @@
+import { NTabPane, NTabs } from 'naive-ui'
+import { defineComponent, h, reactive, resolveDynamicComponent } from 'vue'
+import LoginLog from './component/login-log'
+import LoginStatistics from './component/login-statistics'
+import LoginDevice from './component/login-device'
+import { useRoute } from 'vue-router'
+import { getTabsCache, setTabsCaches } from '@/hooks/use-async'
+export default defineComponent({
+  name: 'city-manage',
+  setup() {
+    const state = reactive({
+      tabName: 'LoginLog' as 'LoginLog' | 'LoginStatistics' | 'LoginDevice'
+    })
+    const route = useRoute()
+    getTabsCache((val: any) => {
+      if (val.form.tabName) {
+        state.tabName = val.form.tabName
+      }
+    })
+    const setTabs = (val: any) => {
+      setTabsCaches(val, 'tabName', route)
+    }
+    return () => {
+      // const Component = resolveDynamicComponent(state.componentName)
+      return (
+        <div class="system-menu-container">
+          <h2>登录日志</h2>
+
+          <div class={['section-container']} style="padding-top: 0">
+            <NTabs
+              type="line"
+              size="large"
+              v-model:value={state.tabName}
+              onUpdate:value={(val: any) => setTabs(val)}
+            >
+              <NTabPane
+                name="LoginLog"
+                tab="登录日志"
+                //v-auth="sysUserLoginLog/page1597883965544714242"
+              >
+                <LoginLog />
+              </NTabPane>
+              <NTabPane
+                name="LoginStatistics"
+                tab="登录统计"
+                //v-auth="sysUserLogin/page1597884090153291778"
+              >
+                <LoginStatistics />
+              </NTabPane>
+              <NTabPane
+                name="LoginDevice"
+                tab="登录设备"
+                //v-auth="sysUserDevice/page1597884219157499905"
+              >
+                <LoginDevice />
+              </NTabPane>
+            </NTabs>
+          </div>
+        </div>
+      )
+    }
+  }
+})

+ 138 - 0
src/views/login-log/interface-log.tsx

@@ -0,0 +1,138 @@
+import Pagination from '@/components/pagination'
+import { filterClientType } from '@/utils/filters'
+import { NDataTable, NDescriptions, NDescriptionsItem, NIcon, NTooltip } from 'naive-ui'
+import type { EllipsisProps } from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { sysUserAuditLogPage } from './api'
+import TheTooltip from '@/components/TheTooltip'
+
+export default defineComponent({
+  name: 'interface-log',
+  setup() {
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        name: ''
+      },
+      dataList: [] as any,
+      visiableCity: false
+    })
+
+    const columns = (): any => {
+      return [
+        {
+          type: 'expand',
+          renderExpand: (rowData: any) => {
+            return `请求参数:${rowData.params} `
+          }
+        },
+        {
+          title: '编号',
+          key: 'id',
+          render(row: any) {
+            return (
+              <NDescriptions labelPlacement="left" column={1}>
+                <NDescriptionsItem label="日志编号">{row.id}</NDescriptionsItem>
+                <NDescriptionsItem label="操作用户">{row.nickname}</NDescriptionsItem>
+                <NDescriptionsItem label="操作时间">{row.startTime}</NDescriptionsItem>
+              </NDescriptions>
+            )
+          }
+        },
+        // {
+        //   title: '操作用户',
+        //   key: 'nickname'
+        // },
+        {
+          title: '客户端',
+          key: 'clientType',
+          render(row: any) {
+            return filterClientType(row.clientType)
+          }
+        },
+        {
+          title: '接口信息',
+          key: 'requestUri',
+          render(row: any) {
+            return (
+              <NDescriptions labelPlacement="left" column={1}>
+                <NDescriptionsItem label="接口地址">
+                  {' '}
+                  <TheTooltip content={row.requestUri} />{' '}
+                </NDescriptionsItem>
+                <NDescriptionsItem label="接口描述">
+                  {' '}
+                  <TheTooltip content={row.description} />{' '}
+                </NDescriptionsItem>
+              </NDescriptions>
+            )
+          }
+        },
+        // {
+        //   title: '接口描述',
+        //   key: 'description'
+        // },
+
+        {
+          title: '接口类路径',
+          key: 'classPath',
+          width: 230
+        },
+        // {
+        //   title: '操作时间',
+        //   key: 'startTime'
+        // },
+        {
+          title: '调用时长',
+          key: 'consumingTime',
+          render(row: any) {
+            return `${row.consumingTime}ms`
+          }
+        }
+      ]
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await sysUserAuditLogPage({
+          ...state.pagination
+        })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+
+        state.dataList = data.rows || []
+      } catch {}
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <h2>接口操作日志</h2>
+
+        <div class={['section-container']}>
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+            rowKey={(row: any) => row.id}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+          ></Pagination>
+        </div>
+      </div>
+    )
+  }
+})

+ 3 - 3
src/views/menu-manage/index.tsx

@@ -96,7 +96,7 @@ export default defineComponent({
                 type="primary"
                 text
                 size="small"
-                v-auth="sysMenu/update1597877909171064833"
+                //v-auth="sysMenu/update1597877909171064833"
                 onClick={() => {
                   state.visiableMenu = true
                   state.menuType = 'edit'
@@ -109,7 +109,7 @@ export default defineComponent({
                 type="primary"
                 text
                 size="small"
-                v-auth="sysMenu/remove1597878074728632322"
+                //v-auth="sysMenu/remove1597878074728632322"
                 onClick={() => onRmove(row)}
               >
                 删除
@@ -224,7 +224,7 @@ export default defineComponent({
           <NSpace style={{ paddingBottom: '12px' }} justify="space-between">
             <NButton
               type="primary"
-              v-auth="sysMenu/save1597877997041733633"
+              //v-auth="sysMenu/save1597877997041733633"
               onClick={() => {
                 state.menuType = 'add'
                 state.applyData = []

+ 46 - 0
src/views/music-categrory/api.ts

@@ -0,0 +1,46 @@
+import request from '@/utils/request/index'
+
+/**
+ * 曲谱分类列表
+ */
+export const getMusicSheetCategories = (params: object) => {
+  return request({
+    url: '/cbs-app/musicSheetCategories/queryTree',
+    method: 'get',
+    data: params,
+    params
+  } as any)
+}
+
+/**
+ * 新增曲谱分类  musicSheetCategories/save
+ */
+export const saveMusicSheetCategories = (params: object) => {
+  return request({
+    url: '/cbs-app/musicSheetCategories/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * 停用启用
+ */
+export const musicSheetCategoriesSwitching = (params: object) => {
+  return request({
+    url: `/cbs-app/musicSheetCategories/switching/${params}`,
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * 修改
+ */
+export const resetMusicSheetCategories = (params: object) => {
+  return request({
+    url: '/cbs-app/musicSheetCategories/update',
+    method: 'post',
+    data: params
+  } as any)
+}

+ 152 - 0
src/views/music-categrory/components/eidt-categroy.tsx

@@ -0,0 +1,152 @@
+import {
+  NForm,
+  NFormItem,
+  NInput,
+  NInputNumber,
+  NSpace,
+  NButton,
+  useMessage,
+  NCascader
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { useRoute } from 'vue-router'
+import { saveMusicSheetCategories, resetMusicSheetCategories } from '../api'
+import UploadFile from '@/components/upload-file'
+import { filterPointCategory } from '@/views/teaching-manage/unit-test'
+export default defineComponent({
+  emits: ['close', 'getList'],
+  props: ['actvieRow', 'isAdd', 'list'],
+  name: 'eidt-categroy',
+  setup(props, { emit }) {
+    const route = useRoute()
+    const orchestraId = route.query.orchestraId || ''
+    const message = useMessage()
+    const formRef = ref()
+    const parentList = ref([
+      { name: '顶级分类', id: 0 },
+      ...filterPointCategory(props.list, 'musicSheetCategoriesList')
+    ] as any)
+    const state = reactive({
+      loading: false,
+      dataList: [] as any,
+      btnLoading: false
+    })
+    const forms = reactive({
+      id: '',
+      name: '',
+      lessonTargetDesc: '',
+      sortNo: '',
+      semesterNo: '',
+      enableFlag: false,
+      parentId: 0
+    } as any)
+
+    const onSubmit = async () => {
+      formRef.value?.validate(async (errors: any) => {
+        if (!errors) {
+          try {
+            if (!props.isAdd) {
+              const { data } = await resetMusicSheetCategories({ ...forms })
+              emit('getList')
+              message.success('修改成功')
+            } else {
+              const { data } = await saveMusicSheetCategories({ ...forms })
+              emit('getList')
+              message.success('添加成功')
+            }
+          } catch (error) {}
+        } else {
+          // console.log(errors)
+          // message.error(errors)
+        }
+      })
+    }
+    onMounted(() => {
+      if (props?.actvieRow?.id && !props.isAdd) {
+        for (let key in props?.actvieRow) {
+          forms[key] = props?.actvieRow[key]
+        }
+      } else if (props?.actvieRow?.id && props.isAdd) {
+        forms.parentId = props?.actvieRow.id + ''
+      }
+    })
+    return () => (
+      <div>
+        <NForm ref={formRef} model={forms}>
+          {props.isAdd && (
+            <NFormItem label="父级分类" path="parentId">
+              <NCascader
+                placeholder="请选择父级分类"
+                v-model:value={forms.parentId}
+                options={parentList.value}
+                checkStrategy="all"
+                expandTrigger="hover"
+                childrenField="musicSheetCategoriesList"
+                valueField="id"
+                labelField="name"
+                clearable
+                filterable
+              />
+            </NFormItem>
+          )}
+
+          <NFormItem
+            label="教材名称"
+            rule={[
+              {
+                required: true,
+                message: '请输入教材名称',
+                trigger: ['blur', 'input']
+              }
+            ]}
+            path="name"
+          >
+            <NInput
+              v-model:value={forms.name}
+              placeholder="请输入教材名称"
+              maxlength={500}
+              clearable
+            ></NInput>
+          </NFormItem>
+          <NFormItem
+            label="教材封面"
+            path="coverImg"
+            rule={[
+              {
+                required: true,
+                message: '请上教材封面',
+                trigger: ['input', 'blur']
+              }
+            ]}
+          >
+            <UploadFile
+              v-model:fileList={forms.coverImg}
+              accept=".jpg,.jpeg,.png"
+              cropper
+              options={{
+                enlarge: 1, //  图片放大倍数
+                autoCropWidth: 349, //默认生成截图框宽度
+                autoCropHeight: 141 //默认生成截图框高度
+              }}
+              size={1}
+              tips="请上传请上传尺寸为349*141大小1M以内的JPG、PNG图片"
+            />
+          </NFormItem>
+        </NForm>
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton
+            type="primary"
+            onClick={() => onSubmit()}
+            loading={state.btnLoading}
+            disabled={state.btnLoading}
+          >
+            确认
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 340 - 0
src/views/music-categrory/index.tsx

@@ -0,0 +1,340 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  DataTableColumn,
+  NButton,
+  NDataTable,
+  NDatePicker,
+  NFormItem,
+  NImage,
+  NInput,
+  NSelect,
+  NSpace,
+  useDialog,
+  useMessage,
+  NModal
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { getMusicSheetCategories, musicSheetCategoriesSwitching } from './api'
+import { InternalRowData } from 'naive-ui/es/data-table/src/interface'
+import { useRouter } from 'vue-router'
+import { knowledgeTypeData, lessonType } from '@/views/knowledge-manage/knowledgeTypeData'
+import EidtCategroy from './components/eidt-categroy'
+
+type RowData = {
+  id: string
+  name: string
+  coverImg: string
+  musicSheetNum: string
+  updateTime: string
+  enable: boolean
+  musicSheetCategoriesList?: RowData[]
+}
+export default defineComponent({
+  emits: ['setTabName'],
+  name: 'music-categrory',
+  setup(props, { emit }) {
+    const router = useRouter()
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 999,
+        pageTotal: 0
+      },
+      dataList: [] as any,
+      visiablePlan: false,
+      modalType: 'add',
+      materail: null,
+      actvieRow: null as any,
+      isAdd: true
+    })
+
+    const searchForm = reactive<any>({
+      name: '', // 计划名称
+      keyword: '', // 关键字匹配
+      enable: null // 是否启用
+    })
+    const dialog = useDialog()
+    const message = useMessage()
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const body = {
+          page: state.pagination.page,
+          rows: state.pagination.rows,
+          ...searchForm
+        }
+        const { data } = await getMusicSheetCategories(body)
+        state.loading = false
+        // state.pagination.pageTotal = Number(data.total)
+        state.dataList = (data as RowData[]) || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    onMounted(() => {
+      getList()
+    })
+
+    //启用停用
+    const handleChangeState = (row: any, type: boolean) => {
+      dialog.warning({
+        title: '警告',
+        content: type ? `是否确认启用${row.name}?` : `是否确认停用${row.name}?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            const res = await musicSheetCategoriesSwitching(row.id)
+            message.success('操作成功')
+            onSubmit()
+          } catch (e) {}
+        }
+      })
+    }
+
+    const columns = (): DataTableColumn[] => {
+      return [
+        {
+          title: '教材编号',
+          key: 'id'
+        },
+        {
+          title: '教材名称',
+          key: 'name'
+        },
+        {
+          title: '教材封面',
+          key: 'coverImg',
+          render: (row: any) => (
+            <img src={row.coverImg} style={{ width: '175px', height: '70px' }} alt="" />
+          )
+        },
+        {
+          title: '曲目数量',
+          key: 'musicSheetNum'
+        },
+
+        // {
+        //   title: '使用音源',
+        //   key: 'soundResource'
+        //   //   render(row) {
+        //   //     return `第${row.semesterNo}学期`
+        //   //   }
+        // },
+        {
+          title: '最后更新时间',
+          key: 'updateTime'
+        },
+        {
+          title: '教材状态',
+          key: 'enable',
+          render(row) {
+            return row.enable ? '已启用' : '未启用'
+          }
+        },
+
+        {
+          title: '操作',
+          key: 'operation',
+          fixed: 'right',
+          render(row: any) {
+            return (
+              <NSpace>
+                {row.enable ? (
+                  <NButton
+                    //v-auth="musicSheetCategories/switching1608067671668359169"
+                    type="error"
+                    size="small"
+                    text
+                    onClick={() => handleChangeState(row, false)}
+                  >
+                    停用
+                  </NButton>
+                ) : (
+                  <NButton
+                    //v-auth="musicSheetCategories/switching1608067671668359169"
+                    type="primary"
+                    size="small"
+                    text
+                    onClick={() => handleChangeState(row, true)}
+                  >
+                    启用
+                  </NButton>
+                )}
+                {/* <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  onClick={() => {
+                    gotoDetail(row)
+                  }}
+                >
+                  查看
+                </NButton> */}
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  onClick={() => {
+                    resetPlan(row)
+                  }}
+                  //v-auth="musicSheetCategories/update1608067835686617090"
+                >
+                  修改
+                </NButton>
+                {row.currentLevel < 4 ? (
+                  <NButton
+                    type="primary"
+                    size="small"
+                    text
+                    onClick={() => handleAdd(row)}
+                    //v-auth="musicSheetCategories/save1608067543331045378"
+                  >
+                    添加
+                  </NButton>
+                ) : null}
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    // 删除
+    const handleDelete = async (row: any) => {
+      dialog.warning({
+        title: '警告',
+        content: '是否确认删除此计划?',
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          console.log('row', row)
+          //   const res = await lessonPlanRemove(row.id)
+          //   if (res.data) {
+          //     onSubmit()
+          //     message.success('删除成功')
+          //   }
+        }
+      })
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+    // 修改
+    const resetPlan = (row: any) => {
+      state.actvieRow = row
+      state.isAdd = false
+      state.visiablePlan = true
+    }
+    // 曲目新增
+    const handleAdd = (row: any) => {
+      state.actvieRow = row
+      state.isAdd = true
+      state.visiablePlan = true
+    }
+    // 详情
+    const gotoDetail = (row: any) => {
+      console.log('gotoDetail')
+      emit('setTabName', {
+        id: row.id,
+        tabName: 'MusicList'
+      })
+      // router.push({ path: '/planDetail', query: { name: row.name, id: row.id } })
+    }
+    return () => (
+      <div class="system-menu-container">
+        <div class={['section-container']}>
+          <SaveForm
+            ref={saveForm}
+            model={searchForm}
+            onSubmit={onSubmit}
+            onSetModel={(val: any) => Object.assign(searchForm, val)}
+            saveKey="music-categrory"
+          >
+            {/* <NFormItem path="keyword" label="计划名称/编号">
+              <NInput v-model:value={searchForm.keyword} placeholder="计划名称/编号" clearable />
+            </NFormItem> */}
+
+            <NFormItem path="enable" label="教材状态">
+              <NSelect
+                placeholder="教材状态"
+                clearable
+                v-model:value={searchForm.enable}
+                options={[
+                  { label: '已启用', value: 1 },
+                  { label: '未启用', value: 0 }
+                ]}
+              />
+            </NFormItem>
+            <NFormItem>
+              <NSpace>
+                <NButton type="primary" onClick={onSearch}>
+                  搜索
+                </NButton>
+                <NButton type="default" onClick={onBtnReset}>
+                  重置
+                </NButton>
+              </NSpace>
+            </NFormItem>
+          </SaveForm>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="musicSheetCategories/save1608067543331045378"
+              onClick={() => {
+                state.actvieRow = null
+                state.visiablePlan = true
+                state.isAdd = true
+              }}
+            >
+              新增分类
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            scrollX={1000}
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+            rowKey={(row: any) => row.id}
+            childrenKey="musicSheetCategoriesList"
+          ></NDataTable>
+        </div>
+        <NModal
+          v-model:show={state.visiablePlan}
+          preset="dialog"
+          showIcon={false}
+          title={state.actvieRow ? '修改曲谱分类' : '新增曲谱分类'}
+          style={{ width: '550px' }}
+        >
+          <EidtCategroy
+            onGetList={() => {
+              state.visiablePlan = false
+              getList()
+            }}
+            onClose={() => {
+              state.visiablePlan = false
+            }}
+            list={state.dataList}
+            isAdd={state.isAdd}
+            actvieRow={state.actvieRow}
+          ></EidtCategroy>
+        </NModal>
+      </div>
+    )
+  }
+})

+ 9 - 0
src/views/sms-code-message/api.ts

@@ -0,0 +1,9 @@
+import request from '@/utils/request/index'
+// 查询列表
+export const sysMessagePage = (params: object) => {
+  return request({
+    url: '/cbs-app/sysMessage/page',
+    method: 'post',
+    data: params
+  } as any)
+}

+ 210 - 0
src/views/sms-code-message/index.tsx

@@ -0,0 +1,210 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  NButton,
+  NFormItem,
+  NInput,
+  NSelect,
+  NSpace,
+  NDataTable,
+  DataTableColumn,
+  NDatePicker
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { sysMessagePage } from './api'
+import { getTimes } from '@/utils/dateUtil'
+import { sysParamConfigSingle } from '../system-manage/api'
+
+export const smsType = {
+  SMS_VERIFY_CODE_LOGIN: '验证码登录',
+  SMS_VERIFY_CODE_LOGOFF: '用户注销',
+  SMS_VERIFY_CODE_REGISTER: '验证码注册',
+  SMS_VERIFY_CODE_UPDATE_PSW: '密码修改',
+  SMS_VERIFY_CODE_UPDATE_PHONE: '修改手机号',
+  SMS_VERIFY_CODE: '验证码',
+  SMS_VERIFY_CODE_BANK_CARD: '银行卡验证'
+} as any
+
+export default defineComponent({
+  name: 'subsidy-manage',
+  setup() {
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      verify_code_expire_time: 0,
+      searchForm: {
+        messageType: '',
+        receiver: null,
+        times: null //  结算时间
+      },
+      dataList: [] as any
+    })
+    const columns = (): DataTableColumn[] => [
+      {
+        title: '手机号',
+        key: 'receiver',
+        ellipsis: {
+          tooltip: true
+        }
+      },
+      {
+        title: '验证码',
+        key: 'verityCode',
+        ellipsis: {
+          tooltip: true
+        }
+      },
+
+      {
+        title: '验证码类型',
+        key: 'messageType',
+        render(row: any) {
+          return smsType[row.messageType]
+        }
+      },
+      {
+        title: '发送时间',
+        key: 'createTime'
+      }
+    ]
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    const getList = async () => {
+      const { times, messageType, ...result } = state.searchForm
+      console.log(state, 'state.searchForm')
+      let type = messageType
+      // 判断是否选择的全部
+      if (!messageType) {
+        type =
+          'SMS_VERIFY_CODE_LOGIN,SMS_VERIFY_CODE_LOGOFF,SMS_VERIFY_CODE_REGISTER,SMS_VERIFY_CODE_UPDATE_PSW,SMS_VERIFY_CODE_UPDATE_PHONE,SMS_VERIFY_CODE,SMS_VERIFY_CODE_BANK_CARD'
+      }
+      let obj = {
+        type: 'SMS',
+        messageType: type,
+        ...result,
+        ...getTimes(times, ['startTime', 'endTime'], 'YYYY-MM-DD'),
+        ...state.pagination
+      }
+      try {
+        state.loading = true
+        const { data } = await sysMessagePage({ ...obj })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    const getSystemList = async () => {
+      try {
+        const { data } = await sysParamConfigSingle({ paramName: 'verify_code_expire_time' })
+        state.verify_code_expire_time = data.paramValue || 0
+      } catch {
+        //
+      }
+    }
+
+    onMounted(() => {
+      getSystemList()
+      getList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <h2>短信验证码</h2>
+        <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          saveKey="sms-code-message"
+          onSubmit={onSubmit}
+          onSetModel={(val: any) => {
+            console.log(val, 'val')
+            state.searchForm = val
+          }}
+        >
+          <NFormItem label="手机号" path="receiver">
+            <NInput
+              v-model:value={state.searchForm.receiver}
+              placeholder="请输入手机号"
+              clearable
+            />
+          </NFormItem>
+
+          <NFormItem label="验证码类型" path="messageType">
+            <NSelect
+              v-model:value={state.searchForm.messageType}
+              options={[
+                { value: 'SMS_VERIFY_CODE_LOGIN', label: '验证码登录' },
+                { value: 'SMS_VERIFY_CODE_LOGOFF', label: '用户注销' },
+                { value: 'SMS_VERIFY_CODE_REGISTER', label: '验证码注册' },
+                { value: 'SMS_VERIFY_CODE_UPDATE_PSW', label: '密码修改' },
+                { value: 'SMS_VERIFY_CODE_UPDATE_PHONE', label: '修改手机号' },
+                { value: 'SMS_VERIFY_CODE', label: '验证码' },
+                { value: 'SMS_VERIFY_CODE_BANK_CARD', label: '银行卡验证' }
+              ]}
+              placeholder="请选择验证码类型"
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="发送时间" path="times">
+            <NDatePicker
+              v-model:value={state.searchForm.times}
+              type="daterange"
+              clearable
+              value-format="yyyy.MM.dd"
+              startPlaceholder="开始时间"
+              endPlaceholder="结束时间"
+            />
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+
+        <div class={['section-container']}>
+          <p style={{ paddingBottom: '10px' }}>
+            验证码有效期为<span style={{ color: 'red' }}>{state.verify_code_expire_time}</span>
+            分钟
+          </p>
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+            rowKey={(row: any) => row.id}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            saveKey="sms-code-message"
+            sync
+          ></Pagination>
+        </div>
+      </div>
+    )
+  }
+})

+ 737 - 0
src/views/system-manage/api.ts

@@ -0,0 +1,737 @@
+import request from '@/utils/request/index'
+
+/**
+ * @description: 系统应用信息列表
+ */
+export const sysApplicationPage = (params: object) => {
+  return request({
+    url: '/cbs-app/sysApplication/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 系统应用信息添加
+ */
+export const sysApplicationSave = (params: object) => {
+  return request({
+    url: '/cbs-app/sysApplication/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 系统应用信息添加
+ */
+export const sysApplicationUpdate = (params: object) => {
+  return request({
+    url: '/cbs-app/sysApplication/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 系统应用信息删除
+ */
+export const sysApplicationRemove = (params: object) => {
+  return request({
+    url: '/cbs-app/sysApplication/remove',
+    method: 'post',
+    data: params,
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 系统应用信息详情
+ */
+export const sysApplicationDetail = (params: any) => {
+  return request({
+    url: '/cbs-app/sysApplication/detail/' + params.id,
+    method: 'get',
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 菜单权限列表
+ */
+export const sysMenuPage = (params: object) => {
+  return request({
+    url: '/cbs-app/sysMenu/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 菜单权限添加
+ */
+export const sysMenuSave = (params: object) => {
+  return request({
+    url: '/cbs-app/sysMenu/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 菜单权限修改
+ */
+export const sysMenuUpdate = (params: object) => {
+  return request({
+    url: '/cbs-app/sysMenu/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 菜单权限删除
+ */
+export const sysMenuRemove = (params: object) => {
+  return request({
+    url: '/cbs-app/sysMenu/remove',
+    method: 'post',
+    data: params,
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 系统角色列表
+ */
+export const sysRolePage = (params: object) => {
+  return request({
+    url: '/cbs-app/sysRole/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 系统角色添加
+ */
+export const sysRoleSave = (params: object) => {
+  return request({
+    url: '/cbs-app/sysRole/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 系统角色修改
+ */
+export const sysRoleUpdate = (params: object) => {
+  return request({
+    url: '/cbs-app/sysRole/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 系统角色删除
+ */
+export const sysRoleRemove = (params: object) => {
+  return request({
+    url: '/cbs-app/sysRole/remove',
+    method: 'post',
+    data: params,
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 系统角色详情
+ */
+export const sysRoleDetail = (params: any) => {
+  return request({
+    url: '/cbs-app/sysRole/detail/' + params.id,
+    method: 'get',
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 岗位列表
+ */
+export const sysPositionPage = (params: object) => {
+  return request({
+    url: '/cbs-app/sysPosition/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 岗位添加
+ */
+export const sysPositionSave = (params: object) => {
+  return request({
+    url: '/cbs-app/sysPosition/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 岗位修改
+ */
+export const sysPositionUpdate = (params: object) => {
+  return request({
+    url: '/cbs-app/sysPosition/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 岗位删除
+ */
+export const sysPositionRemove = (params: object) => {
+  return request({
+    url: '/cbs-app/sysPosition/remove',
+    method: 'post',
+    data: params,
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 员工列表
+ */
+export const sysEmployeePage = (params: object) => {
+  return request({
+    url: '/cbs-app/sysEmployee/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 员工添加
+ */
+export const sysEmployeeSave = (params: object) => {
+  return request({
+    url: '/cbs-app/sysEmployee/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 员工修改
+ */
+export const sysEmployeeUpdate = (params: object) => {
+  return request({
+    url: '/cbs-app/sysEmployee/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 员工删除
+ */
+export const sysEmployeeRemove = (params: object) => {
+  return request({
+    url: '/cbs-app/sysEmployee/remove',
+    method: 'post',
+    data: params,
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 员工 激活-冻结
+ */
+export const sysEmployeeStatus = (params: object) => {
+  return request({
+    url: '/cbs-app/sysEmployee/status',
+    method: 'post',
+    data: params,
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 员工 详情
+ */
+export const sysEmployeeDetail = (params: any) => {
+  return request({
+    url: '/cbs-app/sysEmployee/detail/' + params.id,
+    method: 'get',
+    params: params,
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 员工 重置密码
+ */
+export const resetPassword = (params: object) => {
+  return request({
+    url: '/cbs-app/user/resetPassword',
+    method: 'post',
+    data: params,
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 系统配置 列表
+ */
+export const sysParamConfigPage = (params?: object) => {
+  return request({
+    url: '/cbs-app/sysParamConfig/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+/**
+ * @description: 系统配置 列表
+ */
+export const sysParamConfigSingle = (params?: object) => {
+  return request({
+    url: '/cbs-app/sysParamConfig/queryByParamName',
+    method: 'get',
+    params
+  } as any)
+}
+
+/**
+ * @description: 系统配置 更新
+ */
+export const sysParamConfigUpdate = (params?: object) => {
+  return request({
+    url: '/cbs-app/sysParamConfig/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 支付服务配置参数
+ */
+export const sysPaymentConfigPage = (params?: object) => {
+  return request({
+    url: '/cbs-app/sysPaymentConfig/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 支付服务配置参数-新增
+ */
+export const sysPaymentConfigSave = (params?: object) => {
+  return request({
+    url: '/cbs-app/sysPaymentConfig/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 支付服务配置参数-修改
+ */
+export const sysPaymentConfigUpdate = (params?: object) => {
+  return request({
+    url: '/cbs-app/sysPaymentConfig/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 支付服务配置参数-删除
+ */
+export const sysPaymentConfigRemove = (params?: object) => {
+  return request({
+    url: '/cbs-app/sysPaymentConfig/remove',
+    method: 'post',
+    data: params,
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 消息配置
+ */
+export const sysMessageConfigPage = (params?: object) => {
+  return request({
+    url: '/cbs-app/sysMessageConfig/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 消息配置-新增
+ */
+export const sysMessageConfigSave = (params?: object) => {
+  return request({
+    url: '/cbs-app/sysMessageConfig/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 消息配置-修改
+ */
+export const sysMessageConfigUpdate = (params?: object) => {
+  return request({
+    url: '/cbs-app/sysMessageConfig/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 消息配置-删除
+ */
+export const sysMessageConfigRemove = (params?: object) => {
+  return request({
+    url: '/cbs-app/sysMessageConfig/remove',
+    method: 'post',
+    data: params,
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 协议
+ */
+export const schoolContractTemplatePage = (params?: object) => {
+  return request({
+    url: '/cbs-app/schoolContractTemplate/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 协议-新增
+ */
+export const schoolContractTemplateSave = (params?: object) => {
+  return request({
+    url: '/cbs-app/schoolContractTemplate/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 协议-修改
+ */
+export const schoolContractTemplateUpdate = (params?: object) => {
+  return request({
+    url: '/cbs-app/schoolContractTemplate/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+/**
+ * @description: 协议-启用协议
+ */
+export const schoolContractTemplateUpdateStatus = (params?: object) => {
+  return request({
+    url: '/cbs-app/schoolContractTemplate/updateStatus',
+    method: 'post',
+    data: params,
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 协议-详情
+ */
+export const schoolContractTemplateDetail = (params?: any) => {
+  return request({
+    url: '/cbs-app/schoolContractTemplate/detail/' + params.id,
+    method: 'get',
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 声部基础配置表-列表
+ */
+export const subjectBasicConfigPage = (params?: object) => {
+  return request({
+    url: '/cbs-app/subjectBasicConfig/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 声部基础配置表-新增
+ */
+export const subjectBasicConfigSave = (params?: object) => {
+  return request({
+    url: '/cbs-app/subjectBasicConfig/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 声部基础配置表-修改
+ */
+export const subjectBasicConfigUpdate = (params?: object) => {
+  return request({
+    url: '/cbs-app/subjectBasicConfig/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 声部-修改
+ */
+export const subjectBasicConfigUpdateConfigStatus = (params?: object) => {
+  return request({
+    url: '/cbs-app/subjectBasicConfig/updateConfigStatus',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 声部-列表
+ */
+export const subjectPage = (params?: object) => {
+  return request({
+    url: '/cbs-app/subject/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 声部-添加
+ */
+export const subjectSave = (params?: object) => {
+  return request({
+    url: '/cbs-app/subject/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 声部-修改
+ */
+export const subjectUpdate = (params?: object) => {
+  return request({
+    url: '/cbs-app/subject/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 商品列表
+ */
+export const productList = (params?: object) => {
+  return request({
+    url: '/cbs-app/open/mall/productSearch',
+    method: 'post',
+    data: params,
+    noToken: true
+  } as any)
+}
+
+/**
+ * @description: 商品-详情
+ */
+export const productDetail = (params?: any) => {
+  return request({
+    url: '/cbs-app/open/mall/product/detail/' + params.id,
+    method: 'get',
+    noToken: true
+  } as any)
+}
+
+/**
+ * @description: APP版本信息管理
+ */
+export const appVersionInfoPage = (params?: any) => {
+  return request({
+    url: '/cbs-app/appVersionInfo/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: APP版本信息管理-添加
+ */
+export const appVersionInfoSave = (params?: object) => {
+  return request({
+    url: '/cbs-app/appVersionInfo/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: APP版本信息管理-修改
+ */
+export const appVersionInfoUpdate = (params?: object) => {
+  return request({
+    url: '/cbs-app/appVersionInfo/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 平台建议
+ */
+export const sysSuggestionPage = (params?: any) => {
+  return request({
+    url: '/cbs-app/sysSuggestion/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 节假日表-按年查询节假日
+ */
+export const holidaysFestivalsQuery = (params?: any) => {
+  return request({
+    url: '/cbs-app/holidaysFestivals/query/' + params.year,
+    method: 'get',
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 节假日表-添加
+ */
+export const holidaysFestivalsSave = (params?: object) => {
+  return request({
+    url: '/cbs-app/holidaysFestivals/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 节假日表-修改
+ */
+export const holidaysFestivalsUpdate = (params?: object) => {
+  return request({
+    url: '/cbs-app/holidaysFestivals/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 请假类别-列表
+ */
+export const leaveCategoryPage = (params?: object) => {
+  return request({
+    url: '/cbs-app/leaveCategory/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 请假类别-添加
+ */
+export const leaveCategorySave = (params?: object) => {
+  return request({
+    url: '/cbs-app/leaveCategory/save',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 请假类别-修改
+ */
+export const leaveCategoryUpdate = (params?: object) => {
+  return request({
+    url: '/cbs-app/leaveCategory/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 请假类别-删除
+ */
+export const leaveCategoryRemove = (params?: object) => {
+  return request({
+    url: '/cbs-app/leaveCategory/remove',
+    method: 'post',
+    data: params,
+    requestType: 'form'
+  } as any)
+}
+
+/**
+ * @description: 获取会员-原价
+ */
+export const getVipPriceSettings = (params?: object) => {
+  return request({
+    url: '/cbs-app/vipPriceSettings/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 修改会员-售价
+ */
+export const setVipPriceSettings = (params?: object) => {
+  return request({
+    url: '/cbs-app/vipPriceSettings/update',
+    method: 'post',
+    data: params
+  } as any)
+}
+/**
+ * @description: 批量修改
+ */
+export const vipPriceSettingsBatchUpdate = (params?: object) => {
+  return request({
+    url: '/cbs-app/vipPriceSettings/batchUpdate',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 设备号管理列表
+ */
+export const sysUserDevicePage = (params?: object) => {
+  return request({
+    url: '/cbs-app/sysUserDevice/page',
+    method: 'post',
+    data: params
+  } as any)
+}
+
+/**
+ * @description: 设备号管理-删除
+ */
+export const sysUserDeviceRemove = (params?: object) => {
+  return request({
+    url: '/cbs-app/sysUserDevice/removeIds',
+    method: 'post',
+    data: params
+  } as any)
+}

+ 221 - 0
src/views/system-manage/app-version/index.tsx

@@ -0,0 +1,221 @@
+import Pagination from '@/components/pagination'
+import {
+  NButton,
+  NDataTable,
+  NFormItem,
+  NModal,
+  NSelect,
+  NSpace,
+  NTag,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { appVersionInfoPage } from '../api'
+import VersionOperation from './version-operation'
+import { filterPlatform, filterStatus, platformArr, statusArr } from './operation'
+import SaveForm from '@/components/save-form'
+
+export default defineComponent({
+  name: 'app-version',
+  setup() {
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        keyword: null,
+        status: null,
+        platform: null
+      },
+      dataList: [] as any,
+      visiableApp: false,
+      appOperation: 'add',
+      appData: {} as any
+    })
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '客户端',
+          key: 'platform',
+          render(row: any) {
+            return filterPlatform(row.platform)
+          }
+        },
+        {
+          title: '版本号',
+          key: 'version'
+        },
+        {
+          title: '强制更新',
+          key: 'isForceUpdate',
+          render(row: any) {
+            return (
+              <NTag type={row.isForceUpdate ? 'primary' : 'default'}>
+                {row.isForceUpdate ? '是' : '否'}
+              </NTag>
+            )
+          }
+        },
+        {
+          title: '状态',
+          key: 'status',
+          render(row: any) {
+            return filterStatus(row.status)
+          }
+        },
+        {
+          title: '描述',
+          key: 'description'
+        },
+        {
+          title: '下载连接',
+          key: 'downloadUrl',
+          width: 250
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="appVersionInfo/update1598525730761052161"
+                  onClick={() => {
+                    state.visiableApp = true
+                    state.appOperation = 'edit'
+                    state.appData = row
+                  }}
+                >
+                  修改
+                </NButton>
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await appVersionInfoPage({ ...state.pagination, ...state.searchForm })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      // console.log('搜索', { ...state.form })
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <h2>APP版本控制</h2>
+        <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          onSubmit={onSubmit}
+          onSetModel={(val: any) => (state.searchForm = val)}
+        >
+          <NFormItem label="客户端" path="platform">
+            <NSelect
+              v-model:value={state.searchForm.platform}
+              placeholder="请选择客户端"
+              options={platformArr}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="状态" path="status">
+            <NSelect
+              v-model:value={state.searchForm.status}
+              placeholder="请选择状态"
+              options={statusArr}
+              clearable
+            />
+          </NFormItem>
+          {/* platform version */}
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="appVersionInfo/save1598525667141849090"
+              onClick={() => {
+                state.visiableApp = true
+                state.appOperation = 'add'
+                state.appData = {}
+              }}
+            >
+              添加APP版本
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+          ></Pagination>
+        </div>
+        <NModal
+          v-model:show={state.visiableApp}
+          preset="dialog"
+          showIcon={false}
+          title={state.appOperation === 'add' ? '新增APP版本' : '修改APP版本'}
+          style={{ width: '500px' }}
+        >
+          <VersionOperation
+            v-if={state.visiableApp}
+            type={state.appOperation}
+            data={state.appData}
+            onClose={() => (state.visiableApp = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </div>
+    )
+  }
+})

+ 54 - 0
src/views/system-manage/app-version/operation.ts

@@ -0,0 +1,54 @@
+const platform = {
+  'ios-school': '苹果-管理端',
+  'ios-teacher': '苹果-老师端',
+  'ios-student': '苹果-学生端',
+  'android-school': '安卓-管理端',
+  'android-teacher': '安卓-老师端',
+  'android-student': '安卓-学生端'
+}
+
+const status = {
+  newest: '最新',
+  history: '历史',
+  auditing: '审核中'
+}
+
+export const platformArr = [
+  { value: 'ios-school', label: '苹果-管理端' },
+  { value: 'ios-teacher', label: '苹果-老师端' },
+  { value: 'ios-student', label: '苹果-学生端' },
+  { value: 'android-school', label: '安卓-管理端' },
+  { value: 'android-teacher', label: '安卓-老师端' },
+  { value: 'android-student', label: '安卓-学生端' }
+]
+
+export const statusArr = [
+  { value: 'newest', label: '最新' },
+  { value: 'history', label: '历史' },
+  { value: 'auditing', label: '审核中' }
+]
+
+export const filterStatus = (key: 'newest' | 'history' | 'auditing' | '') => {
+  if (key && status && status[key]) {
+    return status[key]
+  } else {
+    return key
+  }
+}
+
+export const filterPlatform = (
+  key:
+    | 'ios-school'
+    | 'ios-teacher'
+    | 'ios-student'
+    | 'android-school'
+    | 'android-teacher'
+    | 'android-student'
+    | ''
+) => {
+  if (key && platform && platform[key]) {
+    return platform[key]
+  } else {
+    return key
+  }
+}

+ 221 - 0
src/views/system-manage/app-version/version-operation.tsx

@@ -0,0 +1,221 @@
+import UploadFile from '@/components/upload-file'
+import {
+  NForm,
+  NInput,
+  NSpace,
+  NButton,
+  useMessage,
+  NFormItem,
+  NSelect,
+  NRadio,
+  NRadioGroup
+} from 'naive-ui'
+import { defineComponent, onMounted, PropType, reactive, ref } from 'vue'
+import { appVersionInfoSave, appVersionInfoUpdate } from '../api'
+import { platformArr, statusArr } from './operation'
+export default defineComponent({
+  name: 'role-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    applyList: {
+      type: Array as PropType<any>,
+      default: () => []
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      platform: null,
+      version: null,
+      isForceUpdate: null,
+      status: null,
+      downloadUrl: null,
+      description: null
+    })
+    const btnLoading = ref(false)
+    const formsRef = ref()
+    const message = useMessage()
+
+    const onSubmit = async () => {
+      formsRef.value.validate(async (error: any) => {
+        if (error) return false
+        try {
+          btnLoading.value = true
+
+          if (props.type === 'add') {
+            await appVersionInfoSave({ ...forms })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await appVersionInfoUpdate({
+              ...forms,
+              id: props.data.id
+            })
+            message.success('修改成功')
+          }
+
+          emit('close')
+          emit('getList')
+        } catch {}
+        btnLoading.value = false
+      })
+    }
+    const changeApp = () => {
+      forms.version = null
+      forms.isForceUpdate = null
+      forms.status = null
+      forms.downloadUrl = null
+      forms.description = null
+    }
+    onMounted(async () => {
+      if (props.type === 'edit') {
+        const data = props.data
+        forms.platform = data.platform
+        forms.version = data.version
+        forms.isForceUpdate = data.isForceUpdate
+        forms.status = data.status
+        forms.downloadUrl = data.downloadUrl
+        forms.description = data.description
+      }
+    })
+
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="100">
+          <NFormItem
+            label="APP客户端"
+            path="platform"
+            rule={[
+              {
+                required: true,
+                message: '请选择APP客户端'
+              }
+            ]}
+          >
+            <NSelect
+              options={platformArr}
+              v-model:value={forms.platform}
+              placeholder="请选择APP客户端"
+              disabled={props.type == 'add' ? false : true}
+              onChange={() => changeApp()}
+            />
+          </NFormItem>
+
+          <NFormItem
+            label="版本号"
+            path="version"
+            rule={[
+              {
+                required: true,
+                message: '请输入版本号'
+              }
+            ]}
+          >
+            <NInput
+              v-model:value={forms.version}
+              placeholder="请输入版本号"
+              clearable
+              maxlength={100}
+            ></NInput>
+          </NFormItem>
+          <NFormItem
+            label="强制更新"
+            path="isForceUpdate"
+            rule={[
+              {
+                required: true,
+                message: '请选择强制更新'
+              }
+            ]}
+          >
+            <NRadioGroup v-model:value={forms.isForceUpdate}>
+              <NRadio value={true}>是</NRadio>
+              <NRadio value={false}>否</NRadio>
+            </NRadioGroup>
+          </NFormItem>
+          <NFormItem
+            label="状态"
+            path="status"
+            rule={[
+              {
+                required: true,
+                message: '请选择状态'
+              }
+            ]}
+          >
+            <NSelect options={statusArr} v-model:value={forms.status} placeholder="请选择状态" />
+          </NFormItem>
+          {(forms.platform === 'android-student' ||
+            forms.platform === 'android-teacher' ||
+            forms.platform === 'android-school') && (
+            <NFormItem
+              label="上传文件"
+              path="downloadUrl"
+              rule={[
+                {
+                  required: true,
+                  message: '请上传文件'
+                }
+              ]}
+            >
+              <UploadFile
+                size={200}
+                listType="image"
+                accept=".apk,.war"
+                v-model:fileList={forms.downloadUrl}
+                bucketName="appstore"
+                // accept=".apk"
+              />
+            </NFormItem>
+          )}
+          {(forms.platform === 'android-student' ||
+            forms.platform === 'android-teacher' ||
+            forms.platform === 'android-school') && (
+            <NFormItem
+              label="上传链接"
+              path="downloadUrl"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入链接地址'
+                }
+              ]}
+            >
+              <NInput
+                v-model:value={forms.downloadUrl}
+                placeholder="请输入链接地址"
+                clearable
+                maxlength={100}
+              ></NInput>
+            </NFormItem>
+          )}
+          <NFormItem label="描述" path="description">
+            <NInput
+              v-model:value={forms.description}
+              placeholder="请输入描述"
+              clearable
+              type="textarea"
+              rows={2}
+              maxlength={200}
+            ></NInput>
+          </NFormItem>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton type="primary" onClick={() => onSubmit()} loading={btnLoading.value}>
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 290 - 0
src/views/system-manage/device-num-manage/index.tsx

@@ -0,0 +1,290 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  DataTableRowKey,
+  NButton,
+  NCascader,
+  NDataTable,
+  NDatePicker,
+  NFormItem,
+  NInput,
+  NModal,
+  NSelect,
+  NSpace,
+  NTag,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import {
+  sysApplicationPage,
+  sysRolePage,
+  sysRoleRemove,
+  sysUserDevicePage,
+  sysUserDeviceRemove
+} from '../api'
+import { formatDataList } from '@/utils/urlUtils'
+import { getTimes } from '@/utils/dateUtil'
+import { filterClientType } from '@/utils/filters'
+import { clientTypeArray } from '@/utils/searchArray'
+
+export default defineComponent({
+  name: 'subsidy-list',
+  setup() {
+    const state = reactive({
+      loading: false,
+      clientTypeList: [] as any,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        keyword: null,
+        createTime: null as any,
+        clientType: null,
+        deviceNum: null
+      },
+      dataList: [] as any,
+      applyList: [] as any,
+      visiableRole: false,
+      roleOperation: 'add',
+      roleData: {} as any
+    })
+    const dialog = useDialog()
+    const message = useMessage()
+
+    const checkedRowKeysRef = ref<DataTableRowKey[]>([])
+    const handleCheck = (rowKeys: DataTableRowKey[]) => {
+      console.log('🚀 ~ rowKeys', rowKeys)
+      checkedRowKeysRef.value = rowKeys
+    }
+
+    const columns: any = () => {
+      return [
+        {
+          type: 'selection'
+        },
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '用户姓名',
+          key: 'nickname'
+        },
+        {
+          title: '手机号',
+          key: 'phone'
+        },
+        {
+          title: '设备号',
+          key: 'deviceNum'
+        },
+        {
+          title: '客户端',
+          key: 'clientType',
+          render(row: any) {
+            return filterClientType(row.clientType)
+          }
+        },
+        {
+          title: '绑定时间',
+          key: 'createTime'
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="sysUserDevice/removeIds1633324668827324417"
+                  onClick={() => {
+                    onRemove(row)
+                  }}
+                >
+                  解除绑定
+                </NButton>
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const onRemove = (row: any): void => {
+      console.log(row, 'row')
+      dialog.warning({
+        title: '警告',
+        content: `是否确定解除绑定?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysUserDeviceRemove([row.id])
+            getList()
+            message.success('解除绑定成功')
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+
+        const { createTime, ...res } = state.searchForm
+        const { data } = await sysUserDevicePage({
+          ...state.pagination,
+          ...res,
+          ...getTimes(createTime, ['bindStartTime', 'bindEndTime'], 'YYYY-MM-DD')
+        })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    const getApplyList = async (parentId = 0) => {
+      try {
+        const { data } = await sysApplicationPage({
+          page: 1,
+          rows: 999,
+          leafQuery: true,
+          parentId
+        })
+        state.applyList = formatDataList(data.rows || [], 'bizApps')
+      } catch {}
+    }
+
+    const handleDeletes = () => {
+      console.log(checkedRowKeysRef.value)
+      dialog.warning({
+        title: '警告',
+        content: `是否确认批量解除绑定?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            const res = await sysUserDeviceRemove(checkedRowKeysRef.value)
+            message.success('解除绑定成功')
+            checkedRowKeysRef.value = []
+            getList()
+          } catch (e: any) {}
+        }
+      })
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    onMounted(() => {
+      clientTypeArray.forEach((item: any) => {
+        if (item.value != 'BACKEND') {
+          state.clientTypeList.push(item)
+        }
+      })
+      getList()
+      getApplyList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <h2>设备号管理</h2>
+
+        <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          onSubmit={onSubmit}
+          onSetModel={(val: any) => (state.searchForm = val)}
+        >
+          <NFormItem label="搜索" path="appId">
+            <NInput
+              v-model:value={state.searchForm.keyword}
+              placeholder="请输入姓名、手机号"
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="设备号" path="roleName">
+            <NInput
+              v-model:value={state.searchForm.deviceNum}
+              placeholder="请输入设备号"
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="客户端" path="cityCode">
+            <NSelect
+              v-model={[state.searchForm.clientType, 'value']}
+              placeholder="请选择客户端"
+              options={state.clientTypeList}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem path="createTime" label="绑定时间">
+            <NDatePicker
+              type="daterange"
+              v-model:formattedValue={state.searchForm.createTime}
+              valueFormat="yyyy-MM-dd"
+              startPlaceholder="绑定开始日期"
+              endPlaceholder="绑定结束日期"
+              clearable
+            />
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="sysUserDevice/removeIds1631499811567173634"
+              onClick={handleDeletes}
+            >
+              批量解除绑定
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            rowKey={(row: any) => row.id}
+            loading={state.loading}
+            v-model:checkedRowKeys={checkedRowKeysRef.value}
+            onUpdate:checked-row-keys={handleCheck}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+          ></Pagination>
+        </div>
+      </div>
+    )
+  }
+})

+ 485 - 0
src/views/system-manage/employee-manage/employee-operation.tsx

@@ -0,0 +1,485 @@
+import deepClone from '@/utils/deep.clone'
+import { queryAllProvince, sysAreaPage } from '@/views/city-manage/api'
+import dayjs from 'dayjs'
+import {
+  NForm,
+  NFormItem,
+  NInput,
+  NSpace,
+  NButton,
+  useMessage,
+  NRadioGroup,
+  NRadio,
+  NCascader,
+  NGrid,
+  NFormItemGi,
+  NSelect,
+  NDatePicker,
+  NAlert
+} from 'naive-ui'
+import {
+  defineComponent,
+  nextTick,
+  onMounted,
+  PropType,
+  reactive,
+  ref,
+  shallowReactive,
+  shallowRef
+} from 'vue'
+import {
+  sysEmployeeDetail,
+  sysEmployeeSave,
+  sysEmployeeUpdate,
+  sysPositionPage,
+  sysRolePage
+} from '../api'
+export default defineComponent({
+  name: 'role-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    applyList: {
+      type: Array as PropType<any>,
+      default: () => []
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      nickname: null,
+      phone: null,
+      gender: null,
+      entryDate: null,
+      roleList: [],
+      positionList: [],
+      employeeCityList: [],
+      userReceiveAddress: {
+        name: null,
+        phoneNumber: null,
+        address: null, //临时
+        province: null,
+        city: null,
+        region: null,
+        detailAddress: null
+      } as any
+    })
+    const btnLoading = ref(false)
+    const formsRef = ref()
+    const message = useMessage()
+    const state = shallowReactive({
+      selectRole: [],
+      selectPosition: [],
+      selectCity: [] as any,
+      selectCityEmployee: [] as any
+    })
+
+    const onSubmit = async () => {
+      console.log(forms.userReceiveAddress.address, '1212', state.selectCity)
+      const selectArea =
+        formatParentAreaId(forms.userReceiveAddress.address, state.selectCity) || []
+      // if (selectArea.length > 0) {
+      console.log(selectArea, 'selectArea')
+      forms.userReceiveAddress.province = selectArea[0] || null
+      forms.userReceiveAddress.city = selectArea[1] || null
+      forms.userReceiveAddress.region = selectArea[2] || selectArea[1] || null
+      // }
+      formsRef.value.validate(async (error: any) => {
+        if (error) return false
+        try {
+          btnLoading.value = true
+          const { roleList, positionList, entryDate, employeeCityList, ...res } = forms
+          console.log(roleList, positionList, employeeCityList)
+
+          const tempRoleList: any = []
+          if (roleList && roleList.length > 0) {
+            roleList.forEach((item: any) => {
+              tempRoleList.push({
+                roleName: ' ',
+                id: item
+              })
+            })
+          }
+          const tempPositionList: any = []
+          if (positionList) {
+            tempPositionList.push({
+              roleName: ' ',
+              id: positionList
+            })
+          }
+
+          const tempEmployeeList: any = []
+          if (employeeCityList && employeeCityList.length > 0) {
+            employeeCityList.forEach((item: any) => {
+              const itemCode = formatParentAreaId(item, state.selectCity)
+              tempEmployeeList.push({
+                cityCode: itemCode[1],
+                provinceCode: itemCode[0]
+              })
+            })
+          }
+
+          if (props.type === 'add') {
+            await sysEmployeeSave({
+              ...res,
+              roleList: tempRoleList,
+              positionList: tempPositionList,
+              employeeCityList: tempEmployeeList,
+              entryDate: dayjs(entryDate).format('YYYY-MM-DD HH:mm:ss')
+            })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await sysEmployeeUpdate({
+              ...res,
+              roleList: tempRoleList,
+              positionList: tempPositionList,
+              employeeCityList: tempEmployeeList,
+              entryDate: dayjs(entryDate).format('YYYY-MM-DD HH:mm:ss'),
+              id: props.data.id
+            })
+            message.success('修改成功')
+          }
+
+          emit('close')
+          emit('getList')
+        } catch {}
+        btnLoading.value = false
+      })
+    }
+
+    const formatParentId = (id: any, list: any, ids = [] as any) => {
+      for (const item of list) {
+        if (item.children && item.children.length > 0) {
+          const cIds: any = formatParentId(id, item.children, [...ids, item.code])
+          if (cIds.includes(id)) {
+            return cIds
+          }
+        }
+        if (item.code === id) {
+          return [...ids, id]
+        }
+      }
+      return ids
+    }
+
+    const formatParentAreaId = (id: any, list: any, ids = [] as any) => {
+      for (const item of list) {
+        if (item.areas && item.areas.length > 0) {
+          const cIds: any = formatParentAreaId(id, item.areas, [...ids, item.code])
+          if (cIds.includes(id)) {
+            return cIds
+          }
+        }
+        if (item.code === id) {
+          return [...ids, id]
+        }
+      }
+      return ids
+    }
+
+    const getPositionList = async () => {
+      try {
+        const { data } = await sysPositionPage({ page: 1, rows: 999 })
+        state.selectPosition = data.rows || []
+      } catch {}
+    }
+
+    const getRoleList = async () => {
+      try {
+        const { data } = await sysRolePage({ page: 1, rows: 999, enable: true })
+        state.selectRole = data.rows || []
+      } catch {}
+    }
+
+    const phoneValidater = (rule: any, value: string) => {
+      if (!value) {
+        return true
+      } else if (
+        !/^((13[0-9])|(14(0|[5-7]|9))|(15([0-3]|[5-9]))|(16(2|[5-7]))|(17[0-8])|(18[0-9])|(19([0-3]|[5-9])))\d{8}$/.test(
+          value
+        )
+      ) {
+        return new Error('请输入正确的手机号')
+      } else {
+        return true
+      }
+    }
+
+    const getAreaList = async (parentId = 0) => {
+      try {
+        const { data } = await queryAllProvince()
+        const tempList = data || []
+        state.selectCity = deepClone(tempList)
+        state.selectCityEmployee = tempList.map((item: any) => {
+          item.isLeaf = false
+          item.areas.map((area: any) => {
+            area.isLeaf = true
+            area.areas = null
+          })
+          return item
+        })
+      } catch {
+        //
+      }
+    }
+
+    onMounted(async () => {
+      getPositionList()
+      getRoleList()
+      getAreaList()
+      if (props.type === 'edit') {
+        const data = props.data
+        try {
+          const res = await sysEmployeeDetail({ id: data.id })
+          const detail = res.data || {}
+          forms.nickname = detail.nickname
+          forms.phone = detail.phone
+          forms.gender = detail.gender
+          forms.entryDate = detail.entryDate && new Date(detail.entryDate).getTime()
+          forms.roleList =
+            detail.roleList &&
+            detail.roleList.map((position: any) => {
+              return position.id
+            })
+          forms.positionList =
+            (detail.positionList && detail.positionList.length > 0 && detail.positionList[0].id) ||
+            null
+          forms.employeeCityList =
+            detail.employeeCityList &&
+            detail.employeeCityList.map((city: any) => {
+              return Number(city.cityCode)
+            })
+          const userReceiveAddress = detail.userReceiveAddress || {}
+          forms.userReceiveAddress = {
+            id: userReceiveAddress.id,
+            name: userReceiveAddress.name,
+            phoneNumber: userReceiveAddress.phoneNumber,
+            address: Number(userReceiveAddress.region || userReceiveAddress.city) || null, //临时
+            province: userReceiveAddress.province,
+            city: userReceiveAddress.city,
+            region: userReceiveAddress.region,
+            detailAddress: userReceiveAddress.detailAddress
+          }
+        } catch {}
+      }
+    })
+
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="auto">
+          <NAlert title="基本信息" showIcon={false} bordered={false} style="margin-bottom: 12px;" />
+          <NGrid cols={2}>
+            <NFormItemGi
+              label="员工姓名"
+              path="nickname"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入员工姓名'
+                }
+              ]}
+            >
+              <NInput
+                v-model:value={forms.nickname}
+                placeholder="请输入员工姓名"
+                clearable
+                maxlength={100}
+              ></NInput>
+            </NFormItemGi>
+            <NFormItemGi
+              label="手机号"
+              path="phone"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入手机号'
+                },
+                {
+                  pattern:
+                    /^((13[0-9])|(14(0|[5-7]|9))|(15([0-3]|[5-9]))|(16(2|[5-7]))|(17[0-8])|(18[0-9])|(19([0-3]|[5-9])))\d{8}$/,
+                  message: '请输入正确的手机号',
+                  trigger: 'blur'
+                }
+              ]}
+            >
+              <NInput
+                v-model:value={forms.phone}
+                placeholder="请输入手机号"
+                clearable
+                maxlength={11}
+              ></NInput>
+            </NFormItemGi>
+            <NFormItemGi
+              label="性别"
+              path="gender"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择性别'
+                }
+              ]}
+            >
+              <NRadioGroup v-model:value={forms.gender}>
+                <NSpace>
+                  <NRadio value={1}>男</NRadio>
+                  <NRadio value={0}>女</NRadio>
+                </NSpace>
+              </NRadioGroup>
+            </NFormItemGi>
+            <NFormItemGi
+              label="入职日期"
+              path="entryDate"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择入职日期'
+                }
+              ]}
+            >
+              <NDatePicker
+                v-model:value={forms.entryDate}
+                type="date"
+                // valueFormat="yyyy-MM-dd mm:hh:ss"
+                style="width: 100%"
+              />
+            </NFormItemGi>
+            <NFormItemGi
+              label="角色"
+              path="roleList"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择角色'
+                }
+              ]}
+            >
+              <NSelect
+                v-model:value={forms.roleList}
+                options={state.selectRole}
+                labelField="roleName"
+                valueField="id"
+                placeholder="请选择角色"
+                multiple
+                maxTagCount={'responsive'}
+                clearable
+              />
+            </NFormItemGi>
+            <NFormItemGi
+              label="岗位"
+              path="positionList"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择角色'
+                }
+              ]}
+            >
+              <NSelect
+                v-model:value={forms.positionList}
+                options={state.selectPosition}
+                labelField="roleName"
+                valueField="id"
+                clearable
+                placeholder="请选择岗位"
+                maxTagCount={'responsive'}
+              />
+            </NFormItemGi>
+            <NFormItemGi
+              label="负责城市"
+              path="employeeCityList"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择负责城市'
+                }
+              ]}
+            >
+              <NCascader
+                v-model:value={forms.employeeCityList}
+                options={state.selectCityEmployee}
+                labelField="name"
+                valueField="code"
+                childrenField="areas"
+                checkStrategy="child"
+                showPath={false}
+                clearable
+                multiple
+                maxTagCount={'responsive'}
+                placeholder="请选择负责城市"
+              />
+            </NFormItemGi>
+          </NGrid>
+
+          <NAlert title="维修地址" showIcon={false} bordered={false} style="margin-bottom: 12px;" />
+          <NGrid cols={2}>
+            <NFormItemGi label="员工姓名" path="userReceiveAddress.name">
+              <NInput
+                v-model:value={forms.userReceiveAddress.name}
+                placeholder="请输入员工姓名"
+                clearable
+                maxlength={100}
+              ></NInput>
+            </NFormItemGi>
+            <NFormItemGi
+              label="手机号"
+              path="userReceiveAddress.phoneNumber"
+              rule={[
+                {
+                  validator: phoneValidater,
+                  trigger: 'blur'
+                }
+              ]}
+            >
+              <NInput
+                v-model:value={forms.userReceiveAddress.phoneNumber}
+                placeholder="请输入手机号"
+                clearable
+                maxlength={11}
+                style="width: 100%;"
+              ></NInput>
+            </NFormItemGi>
+          </NGrid>
+          <NFormItem label="省市区" path="userReceiveAddress.address">
+            <NCascader
+              v-model:value={forms.userReceiveAddress.address}
+              options={state.selectCity}
+              labelField="name"
+              valueField="code"
+              childrenField="areas"
+              checkStrategy="child"
+              showPath={true}
+              clearable
+              maxTagCount={'responsive'}
+              placeholder="请选择省市区"
+            />
+          </NFormItem>
+          <NFormItem label="详情地址" path="userReceiveAddress.detailAddress">
+            <NInput
+              v-model:value={forms.userReceiveAddress.detailAddress}
+              maxlength={180}
+              type="textarea"
+              rows={2}
+              placeholder="请输入详情地址"
+              clearable
+            />
+          </NFormItem>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton type="primary" onClick={() => onSubmit()} loading={btnLoading.value}>
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 53 - 0
src/views/system-manage/employee-manage/employee-tab.tsx

@@ -0,0 +1,53 @@
+import { NTabPane, NTabs } from 'naive-ui'
+import { defineComponent, h, reactive, resolveDynamicComponent } from 'vue'
+import EmployeeManage from './index'
+import RoleManage from '@/views/system-manage/role-mange/index'
+import StationManage from '@/views/system-manage/station-manage/index'
+import { useRoute } from 'vue-router'
+import { getTabsCache, setTabsCaches } from '@/hooks/use-async'
+export default defineComponent({
+  name: 'employee-tab',
+  setup() {
+    const state = reactive({
+      tabName: 'CityList' as 'CityList' | 'SubsidyList'
+    })
+    const route = useRoute()
+    getTabsCache((val: any) => {
+      if (val.form.tabName) {
+        state.tabName = val.form.tabName
+      }
+    })
+    const setTabs = (val: any) => {
+      setTabsCaches(val, 'tabName', route)
+    }
+    return () => {
+      // const Component = resolveDynamicComponent(state.componentName)
+      return (
+        <div class="system-menu-container">
+          <h2>员工管理</h2>
+
+          <div class={['section-container']} style="padding-top: 0">
+            <NTabs
+              type="line"
+              size="large"
+              v-model:value={state.tabName}
+              onUpdate:value={(val: any) => setTabs(val)}
+            >
+              <NTabPane name="CityList" tab="员工管理" //v-auth="sysEmployee/page1596444091629301762"
+              >
+                <EmployeeManage></EmployeeManage>
+              </NTabPane>
+              <NTabPane
+                name="SubsidyList"
+                tab="岗位管理"
+                //v-auth="sysPosition/page1596107770146689025"
+              >
+                <StationManage></StationManage>
+              </NTabPane>
+            </NTabs>
+          </div>
+        </div>
+      )
+    }
+  }
+})

+ 285 - 0
src/views/system-manage/employee-manage/index.tsx

@@ -0,0 +1,285 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  NButton,
+  NDataTable,
+  NFormItem,
+  NInput,
+  NModal,
+  NSelect,
+  NSpace,
+  NTag,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { resetPassword, sysEmployeePage, sysEmployeeRemove, sysEmployeeStatus } from '../api'
+import EmployeeOperation from './employee-operation'
+import { filterEmployee } from '@/utils/filters'
+import { employeeArray } from '@/utils/searchArray'
+
+export default defineComponent({
+  name: 'subsidy-list',
+  setup() {
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        keyword: null,
+        status: null
+      },
+      dataList: [] as any,
+      visiableEmployee: false,
+      employeeOperation: 'add',
+      employeeData: {} as any
+    })
+    const dialog = useDialog()
+    const message = useMessage()
+
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '姓名',
+          key: 'nickname'
+        },
+        {
+          title: '手机号',
+          key: 'phone'
+        },
+        {
+          title: '角色',
+          key: 'roleList',
+          // ellipsis: {
+          //   tooltip: true
+          // },
+          render(row: any) {
+            return (
+              <NSpace>
+                {row.roleList &&
+                  row.roleList.map((role: any) => <NTag type="info">{role.roleName}</NTag>)}
+              </NSpace>
+            )
+          }
+        },
+        {
+          title: '岗位',
+          key: 'positionList',
+          render(row: any) {
+            return (
+              <NSpace>
+                {row.positionList &&
+                  row.positionList.map((role: any) => <NTag type="info">{role.roleName}</NTag>)}
+              </NSpace>
+            )
+          }
+        },
+        {
+          title: '状态',
+          key: 'status',
+          render(row: any) {
+            return filterEmployee(row.status)
+          }
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="sysEmployee/update1597891779411431425"
+                  onClick={() => {
+                    state.visiableEmployee = true
+                    state.employeeOperation = 'edit'
+                    state.employeeData = row
+                  }}
+                >
+                  修改
+                </NButton>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="user/resetPassword1597892728867639297"
+                  onClick={() => onResetPassword(row)}
+                >
+                  重置密码
+                </NButton>
+                {row.status !== 'CANCEL' && (
+                  <NButton
+                    type="primary"
+                    size="small"
+                    text
+                    onClick={() => onChangeStatus(row)}
+                    //v-auth="sysEmployee/status1597892902956421121"
+                  >
+                    {row.status === 'LOCKED' ? '解冻' : '冻结'}
+                  </NButton>
+                )}
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    // 重置密码
+    const onResetPassword = (row: any): void => {
+      dialog.warning({
+        title: '警告',
+        content: `重置"${row.nickname}"的密码,是否继续?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await resetPassword({
+              userId: row.id,
+              password: 'gyt' + row.phone.substr(7),
+              clientType: row.clientType
+            })
+            message.success('重置成功')
+          } catch {}
+        }
+      })
+    }
+
+    const onChangeStatus = (row: any) => {
+      const statuStr = row.status === 'LOCKED' ? '解冻' : '冻结'
+      dialog.warning({
+        title: '警告',
+        content: `是否${statuStr}"${row.nickname}"?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysEmployeeStatus({ id: row.id })
+            getList()
+            message.success(statuStr + '成功')
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await sysEmployeePage({ ...state.pagination, ...state.searchForm })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        {/* <h2>员工管理</h2> */}
+        <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          onSubmit={onSubmit}
+          saveKey="EmployeeManage"
+          onSetModel={(val: any) => (state.searchForm = val)}
+        >
+          <NFormItem label="关键字" path="search">
+            <NInput
+              v-model:value={state.searchForm.keyword}
+              placeholder="员工编号/姓名/手机号"
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="状态" path="status">
+            <NSelect
+              v-model:value={state.searchForm.status}
+              placeholder="请选择状态"
+              options={employeeArray}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="sysEmployee/save1597891616592744449"
+              onClick={() => {
+                state.visiableEmployee = true
+                state.employeeOperation = 'add'
+                state.employeeData = {}
+              }}
+            >
+              添加员工
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            saveKey="EmployeeManage"
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+          ></Pagination>
+        </div>
+        <NModal
+          v-model:show={state.visiableEmployee}
+          preset="dialog"
+          showIcon={false}
+          title={state.employeeOperation === 'add' ? '新增员工' : '修改员工'}
+          style={{ width: '700px' }}
+        >
+          <EmployeeOperation
+            type={state.employeeOperation}
+            data={state.employeeData}
+            onClose={() => (state.visiableEmployee = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </div>
+    )
+  }
+})

+ 273 - 0
src/views/system-manage/menu-manage/index.tsx

@@ -0,0 +1,273 @@
+import {
+  NButton,
+  NModal,
+  NSpace,
+  NDataTable,
+  useDialog,
+  useMessage,
+  NTag,
+  NFormItem,
+  NCascader
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { sysApplicationPage, sysMenuPage, sysMenuRemove } from '../api'
+import MenuOperation from './menu-operation'
+import SaveForm from '@/components/save-form'
+import { formatDataList } from '@/utils/urlUtils'
+
+/* 
+parentId 要结点
+path 路径
+name 名称
+component 组件名
+icon 图标
+type 菜单 | 按钮
+permission 权限
+parentPermission 高亮路径
+sort 菜单排序
+hidden 是否隐藏
+ */
+
+export default defineComponent({
+  name: 'menu-manage',
+  setup() {
+    const dialog = useDialog()
+    const message = useMessage()
+    const state = reactive({
+      loading: false,
+      applyList: [] as any,
+      dataList: [] as any,
+      visiableMenu: false,
+      menuType: 'add',
+      applyData: {} as any,
+      expandedRowKeys: [],
+      searchForm: {
+        appId: null
+      }
+    })
+
+    const columns = (): any => [
+      {
+        title: '菜单名称',
+        key: 'name',
+        width: 330,
+        render(row: any) {
+          return `${row.name} - ${row.id}`
+        }
+      },
+      {
+        title: '应用名称',
+        key: 'appName'
+      },
+      {
+        title: '组件名',
+        key: 'component'
+      },
+      {
+        title: '路由路径',
+        key: 'path'
+      },
+      {
+        title: '权限标识',
+        key: 'permission'
+      },
+      {
+        title: '排序',
+        key: 'sort'
+      },
+      {
+        title: '类型',
+        key: 'type',
+        render(row: any) {
+          return row.type === '0' ? (
+            <NTag type="primary">菜单</NTag>
+          ) : (
+            <NTag type="default">按钮</NTag>
+          )
+        }
+      },
+      {
+        title: '操作',
+        key: 'operation',
+        render(row: any) {
+          return (
+            <NSpace>
+              <NButton
+                type="primary"
+                text
+                size="small"
+                //v-auth="sysMenu/update1597877909171064833"
+                onClick={() => {
+                  state.visiableMenu = true
+                  state.menuType = 'edit'
+                  state.applyData = row
+                }}
+              >
+                编辑
+              </NButton>
+              <NButton
+                type="primary"
+                text
+                size="small"
+                //v-auth="sysMenu/remove1597878074728632322"
+                onClick={() => onRmove(row)}
+              >
+                删除
+              </NButton>
+            </NSpace>
+          )
+        }
+      }
+    ]
+
+    const onRmove = (row: any): void => {
+      dialog.warning({
+        title: '警告',
+        content: `删除"${row.name}",是否继续?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysMenuRemove({ id: row.id })
+            getList()
+            message.success('删除成功')
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        state.expandedRowKeys = []
+        const { data } = await sysMenuPage({
+          ...state.searchForm,
+          parentId: 0,
+          delFlag: false
+        })
+        state.loading = false
+        const tempList = [] as any
+        ;(data || []).forEach((item: any) => {
+          item.isLeaf = item.children ? false : true
+          tempList.push(item)
+        })
+
+        state.dataList = formatDataList(tempList)
+      } catch {}
+    }
+
+    const getApplyList = async (parentId = 0) => {
+      try {
+        const { data } = await sysApplicationPage({
+          page: 1,
+          rows: 999,
+          leafQuery: true,
+          parentId
+        })
+        state.applyList = formatDataList(data.rows || [], 'bizApps')
+      } catch {}
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    onMounted(() => {
+      getList()
+      getApplyList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <h2>菜单管理</h2>
+
+        <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          onSubmit={onSubmit}
+          onSetModel={(val: any) => (state.searchForm = val)}
+        >
+          <NFormItem label="应用" path="name">
+            <NCascader
+              v-model:value={state.searchForm.appId}
+              options={state.applyList}
+              showPath={true}
+              allowCheckingNotLoaded={false}
+              checkStrategy="child"
+              valueField="id"
+              labelField="appName"
+              childrenField="bizApps"
+              placeholder="请选择应用"
+              expandTrigger="hover"
+              clearable
+            ></NCascader>
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }} justify="space-between">
+            <NButton
+              type="primary"
+              //v-auth="sysMenu/save1597877997041733633"
+              onClick={() => {
+                state.menuType = 'add'
+                state.applyData = []
+                state.visiableMenu = true
+              }}
+            >
+              添加菜单
+            </NButton>
+          </NSpace>
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+            allowCheckingNotLoaded
+            cascade={false}
+            virtualScroll
+            maxHeight={400}
+            expandedRowKeys={state.expandedRowKeys}
+            onUpdateExpandedRowKeys={(keys: any) => {
+              state.expandedRowKeys = keys
+            }}
+            rowKey={(row: any) => row.id}
+          ></NDataTable>
+        </div>
+
+        <NModal
+          v-model:show={state.visiableMenu}
+          preset="dialog"
+          showIcon={false}
+          title={state.menuType === 'add' ? '添加菜单' : '修改菜单'}
+          style={{ width: '700px' }}
+        >
+          <MenuOperation
+            type={state.menuType}
+            data={state.applyData}
+            menuList={state.dataList}
+            applyList={state.applyList}
+            onClose={() => (state.visiableMenu = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </div>
+    )
+  }
+})

+ 315 - 0
src/views/system-manage/menu-manage/menu-operation.tsx

@@ -0,0 +1,315 @@
+import {
+  NForm,
+  NFormItem,
+  NInput,
+  NSelect,
+  NSpace,
+  NButton,
+  NTreeSelect,
+  useMessage,
+  NRadioGroup,
+  NRadio,
+  NCascader,
+  NGrid,
+  NFormItemGi,
+  NInputNumber
+} from 'naive-ui'
+import type { TreeSelectOption } from 'naive-ui'
+import { defineComponent, onMounted, PropType, reactive, ref } from 'vue'
+import { sysApplicationDetail, sysApplicationPage, sysMenuSave, sysMenuUpdate } from '../api'
+export default defineComponent({
+  name: 'city-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    menuList: {
+      type: Array,
+      default: () => []
+    },
+    applyList: {
+      type: Array as PropType<any>,
+      default: () => []
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    // parentId 上级菜单
+    // path 路径
+    // name 名称
+    // component 组件名
+    // icon 图标
+    // type 菜单 | 按钮
+    // permission 权限
+    // parentPermission 高亮路径
+    // sort 菜单排序
+    // hidden 是否隐藏
+    // linkPath 外链地址
+    const forms = reactive({
+      appId: null,
+      parentId: null,
+      type: 0,
+      name: null,
+      path: null,
+      icon: null,
+      component: null,
+      permission: null,
+      parentPermission: null,
+      linkPath: null,
+      sort: 0,
+      hidden: 0
+    })
+
+    const menuList = ref<any[]>([{ id: '0', name: '根结点', isLeaf: true }, ...props.menuList])
+    const btnLoading = ref(false)
+
+    const formsRef = ref()
+    const message = useMessage()
+
+    const onSubmit = async () => {
+      formsRef.value.validate(async (error: any) => {
+        if (error) return false
+        try {
+          btnLoading.value = true
+
+          if (props.type === 'add') {
+            await sysMenuSave({
+              ...forms
+            })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await sysMenuUpdate({ ...forms, id: props.data.id })
+            message.success('修改成功')
+          }
+          emit('close')
+          emit('getList')
+        } catch {}
+        btnLoading.value = false
+      })
+    }
+
+    onMounted(async () => {
+      if (props.type === 'edit') {
+        const data = props.data
+        forms.appId = data.appId
+        forms.parentId = data.parentId
+        forms.type = Number(data.type)
+        forms.name = data.name
+        forms.path = data.path
+        forms.icon = data.icon
+        forms.component = data.component
+        forms.permission = data.permission
+        forms.parentPermission = data.parentPermission
+        forms.linkPath = data.linkPath
+        forms.sort = data.sort
+        forms.hidden = data.hidden
+      }
+    })
+
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="100">
+          <NGrid cols={2}>
+            <NFormItemGi
+              label="应用分类"
+              path="parentId"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择应用分类'
+                }
+              ]}
+            >
+              <NCascader
+                v-model:value={forms.appId}
+                options={props.applyList}
+                showPath={true}
+                allowCheckingNotLoaded={false}
+                checkStrategy="child"
+                valueField="id"
+                labelField="appName"
+                childrenField="bizApps"
+                placeholder="请选择应用分类"
+                expandTrigger="hover"
+                clearable
+              ></NCascader>
+            </NFormItemGi>
+            <NFormItemGi
+              label="上级菜单"
+              path="parentId"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择上级菜单'
+                }
+              ]}
+            >
+              <NTreeSelect
+                v-model:value={forms.parentId}
+                placeholder="请选择上级菜单"
+                checkStrategy="parent"
+                options={menuList.value}
+                showPath={false}
+                keyField="id"
+                labelField="name"
+                virtualScroll
+              />
+              {/* <NCascader
+                v-model:value={forms.parentId}
+                options={menuList.value}
+                showPath={false}
+                allowCheckingNotLoaded={false}
+                checkStrategy="child"
+                valueField="id"
+                labelField="name"
+                placeholder="请选择上级菜单"
+                expandTrigger="hover"
+                clearable
+              ></NCascader> */}
+            </NFormItemGi>
+            <NFormItemGi label="菜单类型" path="type">
+              <NRadioGroup v-model:value={forms.type}>
+                <NSpace>
+                  <NRadio value={0}>菜单</NRadio>
+                  <NRadio value={1}>按钮</NRadio>
+                </NSpace>
+              </NRadioGroup>
+            </NFormItemGi>
+
+            <NFormItemGi
+              label="菜单名称"
+              path="name"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入菜单名称',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput
+                v-model:value={forms.name}
+                placeholder="请输入菜单名称"
+                clearable
+                maxlength={100}
+              ></NInput>
+            </NFormItemGi>
+            {forms.type === 0 && (
+              <>
+                <NFormItemGi
+                  label="路由路径"
+                  path="path"
+                  rule={[
+                    {
+                      required: true,
+                      message: '请输入路由路径',
+                      trigger: ['blur', 'input']
+                    }
+                  ]}
+                >
+                  <NInput
+                    v-model:value={forms.path}
+                    placeholder="请输入路由路径"
+                    clearable
+                    maxlength={100}
+                  ></NInput>
+                </NFormItemGi>
+                <NFormItemGi label="菜单图标" path="icon">
+                  <NInput
+                    v-model:value={forms.icon}
+                    placeholder="请输入菜单图标"
+                    clearable
+                    maxlength={100}
+                  ></NInput>
+                </NFormItemGi>
+                <NFormItemGi
+                  label="组件名称"
+                  path="component"
+                  rule={[
+                    {
+                      required: true,
+                      message: '请输入组件名称',
+                      trigger: ['blur', 'input']
+                    }
+                  ]}
+                >
+                  <NInput
+                    v-model:value={forms.component}
+                    placeholder="请输入组件名称"
+                    clearable
+                    maxlength={100}
+                  ></NInput>
+                </NFormItemGi>
+                <NFormItemGi label="链接地址" path="linkPath">
+                  <NInput
+                    v-model:value={forms.linkPath}
+                    placeholder="外链/内嵌时链接地址(http://www.colexiu.com)"
+                    clearable
+                    maxlength={100}
+                  ></NInput>
+                </NFormItemGi>
+                <NFormItemGi label="高亮路径" path="parentPermission">
+                  <NInput
+                    v-model:value={forms.parentPermission}
+                    placeholder="子页面高亮路径"
+                    clearable
+                    maxlength={100}
+                  ></NInput>
+                </NFormItemGi>
+              </>
+            )}
+            <NFormItemGi
+              label="权限标识"
+              path="permission"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入权限标识',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput
+                v-model:value={forms.permission}
+                placeholder="请输入权限标识"
+                clearable
+                maxlength={100}
+              ></NInput>
+            </NFormItemGi>
+            <NFormItemGi label="菜单排序" path="sort">
+              <NInputNumber
+                v-model:value={forms.sort}
+                placeholder="请输入菜单排序"
+                clearable
+              ></NInputNumber>
+            </NFormItemGi>
+            {forms.type === 0 && (
+              <NFormItemGi label="是否隐藏" path="hidden">
+                <NRadioGroup v-model:value={forms.hidden}>
+                  <NSpace>
+                    <NRadio value={1}>是</NRadio>
+                    <NRadio value={0}>否</NRadio>
+                  </NSpace>
+                </NRadioGroup>
+              </NFormItemGi>
+            )}
+          </NGrid>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton type="primary" onClick={() => onSubmit()} loading={btnLoading.value}>
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 752 - 0
src/views/system-manage/param-settings/component/attendance-rule.tsx

@@ -0,0 +1,752 @@
+import {
+  addFormMinute,
+  addFormMinuteAddS,
+  reduceFormMinute,
+  reduceFormMinuteAddS,
+  reduceFormMinuteMS
+} from '@/utils/dateUtil'
+import {
+  NAlert,
+  NButton,
+  NForm,
+  NFormItem,
+  NFormItemGi,
+  NGrid,
+  NGridItem,
+  NInput,
+  NInputGroup,
+  NSelect,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, reactive, ref, shallowRef } from 'vue'
+import { sysParamConfigPage, sysParamConfigUpdate } from '../../api'
+import styles from './index.module.less'
+
+export default defineComponent({
+  name: 'attendance-rule',
+  setup() {
+    const selectUnit = [
+      {
+        label: '元',
+        value: 'MONEY'
+      },
+      {
+        label: '%',
+        value: 'PERCENTAGE'
+      }
+    ]
+    const forms = reactive({}) as any
+    const beforeData: any = shallowRef({}) // 储存原始数据
+    const formsRef = ref()
+    const btnLoading = ref(false)
+    const message = useMessage()
+
+    const getList = async () => {
+      try {
+        const { data } = await sysParamConfigPage({ page: 1, rows: 999, group: 'ATTENDANCE' })
+
+        const rows = data.rows || []
+        rows.forEach((row: any) => {
+          forms[row.paramName] = row.paramValue
+        })
+        beforeData.value = { ...forms }
+      } catch {}
+    }
+
+    // 数组进行对比
+    const dataCompare = (beforeData: any, afterData: any) => {
+      const changeDate: any = []
+      for (let key in beforeData) {
+        if (beforeData[key] != afterData[key]) {
+          changeDate.push({
+            paramName: key,
+            paramValue: afterData[key]
+          })
+        }
+      }
+      return changeDate || []
+    }
+
+    const onSubmit = async () => {
+      formsRef.value.validate(async (errors: any) => {
+        if (errors) return
+
+        const submitData = dataCompare(beforeData.value, forms)
+        if (submitData && submitData.length > 0) {
+          btnLoading.value = true
+          try {
+            await sysParamConfigUpdate({ configs: [...submitData], group: 'ATTENDANCE' })
+            message.success('保存成功')
+            beforeData.value = { ...forms }
+          } catch {}
+          btnLoading.value = false
+        } else {
+          message.success('保存成功')
+        }
+      })
+    }
+
+    getList()
+
+    return () => (
+      <>
+        <NForm labelPlacement="left" model={forms} requireMarkPlacement="left" ref={formsRef}>
+          <NAlert
+            title="考勤定位范围"
+            showIcon={false}
+            bordered={false}
+            style="margin-bottom: 12px;"
+          />
+          <h3>考勤定位范围</h3>
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="签到、签退GPS定位须在学校定位点"
+              path="scope_of_attendance"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入定位距离',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput
+                class={[styles.w140, styles.mr14]}
+                v-model:value={forms['scope_of_attendance']}
+              >
+                {{ suffix: () => '米' }}
+              </NInput>
+              以内
+            </NFormItemGi>
+          </NGrid>
+          <h3>签到签退定位异常</h3>
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              v-slots={{
+                label: () => (
+                  <>
+                    <span class={styles.red}>签到</span>地点未在教学点范围内,扣减课酬
+                  </>
+                )
+              }}
+              path="sign_in_attendance"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入定位距离',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInputGroup class={[styles.mr14, styles.w140]}>
+                <NInput class={styles.w80} v-model:value={forms['sign_in_attendance']}></NInput>
+                <NSelect
+                  class={styles.w60}
+                  options={selectUnit}
+                  v-model:value={forms['sign_in_attendance_type']}
+                  defaultValue={'MONEY'}
+                />
+              </NInputGroup>
+            </NFormItemGi>
+          </NGrid>
+
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              v-slots={{
+                label: () => (
+                  <>
+                    <span class={styles.red}>签退</span>地点未在教学点范围内,扣减课酬
+                  </>
+                )
+              }}
+              path="sign_out_attendance"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入金额',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInputGroup class={[styles.mr14, styles.w140]}>
+                <NInput class={styles.w80} v-model:value={forms['sign_out_attendance']}></NInput>
+                <NSelect
+                  class={styles.w60}
+                  options={selectUnit}
+                  v-model:value={forms['sign_out_attendance_type']}
+                  defaultValue={'MONEY'}
+                />
+              </NInputGroup>
+            </NFormItemGi>
+          </NGrid>
+
+          <NAlert title="签到规则" showIcon={false} bordered={false} style="margin-bottom: 12px;" />
+          <h3>正常签到</h3>
+          <NGrid class={styles.pl13} cols={1}>
+            <NGridItem class={styles.inlineFlex}>
+              <NFormItem
+                label="课程开始前"
+                path="normal_sign_in_start"
+                rule={[
+                  {
+                    required: true,
+                    pattern: /^\d+$/,
+                    message: '请输入分钟数',
+                    trigger: ['blur', 'input']
+                  }
+                ]}
+              >
+                <NInput
+                  class={[styles.w140, styles.mr14]}
+                  v-model:value={forms['normal_sign_in_start']}
+                >
+                  {{ suffix: () => '分钟' }}
+                </NInput>
+              </NFormItem>
+              <NFormItem
+                label="至课程开始前"
+                showRequireMark={false}
+                path="normal_sign_in_end"
+                rule={[
+                  {
+                    required: true,
+                    pattern: /^\d+$/,
+                    message: '请输入分钟数',
+                    trigger: ['blur', 'input']
+                  }
+                ]}
+              >
+                <NInput
+                  class={[styles.w140, styles.mr14]}
+                  v-model:value={forms['normal_sign_in_end']}
+                  onUpdateValue={(val: any) => {
+                    forms['abnormal_sign_in'] = val
+                  }}
+                >
+                  {{ suffix: () => '分钟' }}
+                </NInput>
+              </NFormItem>
+            </NGridItem>
+            <NGridItem>
+              <div class={styles.tips}>
+                例: <span class={styles.red}>12:00:00</span> 开始的课程,正常签到时间范围:
+                <span class={styles.red}>
+                  {reduceFormMinute('12:00:00', forms['normal_sign_in_start'])} ~
+                  {reduceFormMinute('12:00:00', forms['normal_sign_in_end'])}
+                </span>{' '}
+                且最早
+                <span class={styles.red}>
+                  {reduceFormMinute('12:00:00', forms['normal_sign_in_start'])}
+                </span>
+                可以进行签到。
+              </div>
+            </NGridItem>
+          </NGrid>
+
+          {forms['normal_sign_in_end'] && (
+            <>
+              <h3>异常签到</h3>
+              <NGrid class={styles.pl13} cols={1}>
+                <NGridItem class={styles.inlineFlex}>
+                  <NFormItem
+                    label="未在课程开始前"
+                    path="abnormal_sign_in"
+                    rule={[
+                      {
+                        required: true,
+                        pattern: /^\d+$/,
+                        message: '请输入分钟数',
+                        trigger: ['blur', 'input']
+                      }
+                    ]}
+                  >
+                    <NInput
+                      class={[styles.w140, styles.mr14]}
+                      disabled
+                      v-model:value={forms['abnormal_sign_in']}
+                    >
+                      {{ suffix: () => '分钟' }}
+                    </NInput>
+                  </NFormItem>
+                  <NFormItem
+                    label="前签到,扣减当日训练补助"
+                    showRequireMark={false}
+                    path="abnormal_sign_in_fee"
+                    rule={[
+                      {
+                        required: true,
+                        pattern: /^\d+$/,
+                        message: '请输入金额',
+                        trigger: ['blur', 'input']
+                      }
+                    ]}
+                  >
+                    <NInputGroup class={[styles.mr14, styles.w140]}>
+                      <NInput
+                        class={styles.w80}
+                        v-model:value={forms['abnormal_sign_in_fee']}
+                      ></NInput>
+                      <NSelect
+                        class={styles.w60}
+                        options={selectUnit}
+                        v-model:value={forms['abnormal_sign_in_fee_type']}
+                        defaultValue={'MONEY'}
+                      />
+                    </NInputGroup>
+                  </NFormItem>
+                </NGridItem>
+                <NGridItem>
+                  <div class={styles.tips}>
+                    例: <span class={styles.red}>12:00:00</span> 开始的课程,在
+                    <span class={styles.red}>
+                      {reduceFormMinuteAddS('12:00:00', forms['abnormal_sign_in'])} ~ 12:00:00
+                    </span>{' '}
+                    时间段内签到为
+                    <span class={styles.red}>异常签到</span>。
+                  </div>
+                </NGridItem>
+              </NGrid>
+            </>
+          )}
+
+          <h3>迟到</h3>
+          <NGrid class={styles.pl13} cols={1}>
+            <NGridItem class={styles.inlineFlex}>
+              <NFormItem
+                label="课程开始后"
+                path="late_sign_in"
+                rule={[
+                  {
+                    required: true,
+                    pattern: /^\d+$/,
+                    message: '请输入分钟数',
+                    trigger: ['blur', 'input']
+                  }
+                ]}
+              >
+                <NInput
+                  class={[styles.w140, styles.mr14]}
+                  v-model:value={forms['late_sign_in']}
+                  onUpdateValue={(val: any) => {
+                    forms['absence_sign_in'] = val
+                  }}
+                >
+                  {{ suffix: () => '分钟' }}
+                </NInput>
+              </NFormItem>
+              <NFormItem
+                label="以内签到,扣减当日训练补助"
+                showRequireMark={false}
+                path="late_sign_in_fee"
+                rule={[
+                  {
+                    required: true,
+                    pattern: /^\d+$/,
+                    message: '请输入金额',
+                    trigger: ['blur', 'input']
+                  }
+                ]}
+              >
+                <NInputGroup class={[styles.mr14, styles.w140]}>
+                  <NInput class={styles.w80} v-model:value={forms['late_sign_in_fee']}></NInput>
+                  <NSelect
+                    class={styles.w60}
+                    options={selectUnit}
+                    v-model:value={forms['late_sign_in_fee_type']}
+                    defaultValue={'MONEY'}
+                  />
+                </NInputGroup>
+              </NFormItem>
+            </NGridItem>
+            <NGridItem>
+              <div class={styles.tips}>
+                例: <span class={styles.red}>12:00:00</span> 开始的课程,在
+                <span class={styles.red}>
+                  {addFormMinuteAddS('12:00:00', 0)} ~{' '}
+                  {addFormMinute('12:00:00', forms['late_sign_in'])}
+                </span>
+                时间段内签到为
+                <span class={styles.red}>迟到</span>。
+              </div>
+            </NGridItem>
+          </NGrid>
+
+          {forms['late_sign_in'] && (
+            <>
+              <h3>旷课</h3>
+              <NGrid class={styles.pl13} cols={1}>
+                <NGridItem class={styles.inlineFlex}>
+                  <NFormItem
+                    label="课程开始"
+                    path="absence_sign_in"
+                    rule={[
+                      {
+                        required: true,
+                        pattern: /^\d+$/,
+                        message: '请输入分钟数',
+                        trigger: ['blur', 'input']
+                      }
+                    ]}
+                  >
+                    <NInput
+                      class={[styles.w140, styles.mr14]}
+                      v-model:value={forms['absence_sign_in']}
+                      disabled
+                    >
+                      {{ suffix: () => '分钟' }}
+                    </NInput>
+                  </NFormItem>
+                  <NFormItem
+                    label="后签到,扣减当日训练补助"
+                    showRequireMark={false}
+                    path="absence_sign_in_fee"
+                    rule={[
+                      {
+                        required: true,
+                        pattern: /^\d+$/,
+                        message: '请输入金额',
+                        trigger: ['blur', 'input']
+                      }
+                    ]}
+                  >
+                    <NInputGroup class={[styles.mr14, styles.w140]}>
+                      <NInput
+                        class={styles.w80}
+                        v-model:value={forms['absence_sign_in_fee']}
+                      ></NInput>
+                      <NSelect
+                        class={styles.w60}
+                        options={selectUnit}
+                        v-model:value={forms['absence_sign_in_fee_type']}
+                        defaultValue={'MONEY'}
+                      />
+                    </NInputGroup>
+                  </NFormItem>
+                </NGridItem>
+                <NGridItem>
+                  <div class={styles.tips}>
+                    例: <span class={styles.red}>12:00:00</span> 开始的课程,在
+                    <span class={styles.red}>
+                      {addFormMinute('12:00:00', forms['absence_sign_in'])}
+                    </span>{' '}
+                    后签到(含
+                    <span class={styles.red}>
+                      {addFormMinute('12:00:00', forms['absence_sign_in'])}
+                    </span>
+                    )为
+                    <span class={styles.red}>旷课</span>。
+                  </div>
+                </NGridItem>
+              </NGrid>
+            </>
+          )}
+
+          <h3>未签到</h3>
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="扣减当日训练补助"
+              path="not_sign_in_fee"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入金额',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInputGroup class={[styles.mr14, styles.w140]}>
+                <NInput class={styles.w80} v-model:value={forms['not_sign_in_fee']}></NInput>
+                <NSelect
+                  class={styles.w60}
+                  options={selectUnit}
+                  v-model:value={forms['not_sign_in_fee_type']}
+                  defaultValue={'MONEY'}
+                />
+              </NInputGroup>
+            </NFormItemGi>
+          </NGrid>
+
+          <NAlert title="签退规则" showIcon={false} bordered={false} style="margin-bottom: 12px;" />
+          <h3>正常签退</h3>
+          <NGrid class={styles.pl13} cols={1}>
+            <NGridItem class={styles.inlineFlex}>
+              <NFormItem
+                label="当日课程结束"
+                path="normal_sign_out"
+                rule={[
+                  {
+                    required: true,
+                    pattern: /^\d+$/,
+                    message: '请输入分钟数',
+                    trigger: ['blur', 'input']
+                  }
+                ]}
+              >
+                <NInput class={[styles.w140, styles.mr14]} v-model:value={forms['normal_sign_out']}>
+                  {{ suffix: () => '分钟' }}
+                </NInput>
+                内签退
+              </NFormItem>
+            </NGridItem>
+            <NGridItem>
+              <div class={styles.tips}>
+                例: <span class={styles.red}>13:00:00</span> 当日课程结束,可在
+                <span class={styles.red}>
+                  13:00:00 ~ {addFormMinute('13:00:00', forms['normal_sign_out'])}{' '}
+                </span>
+                时间段内完成签退。
+              </div>
+            </NGridItem>
+          </NGrid>
+
+          <h3>异常签退</h3>
+          <NGrid class={styles.pl13} cols={1}>
+            <NGridItem class={styles.inlineFlex}>
+              <NFormItem
+                label="课程结束前"
+                path="abnormal_sign_out"
+                rule={[
+                  {
+                    required: true,
+                    pattern: /^\d+$/,
+                    message: '请输入分钟数',
+                    trigger: ['blur', 'input']
+                  }
+                ]}
+              >
+                <NInput
+                  class={[styles.w140, styles.mr14]}
+                  v-model:value={forms['abnormal_sign_out']}
+                  onUpdateValue={(val: any) => {
+                    forms['premise_sign_out'] = val
+                  }}
+                >
+                  {{ suffix: () => '分钟' }}
+                </NInput>
+              </NFormItem>
+              <NFormItem
+                label="至结束时间段内签退,扣减当日训练补助"
+                showRequireMark={false}
+                path="abnormal_sign_out_fee"
+                rule={[
+                  {
+                    required: true,
+                    pattern: /^\d+$/,
+                    message: '请输入金额',
+                    trigger: ['blur', 'input']
+                  }
+                ]}
+              >
+                <NInputGroup class={[styles.mr14, styles.w140]}>
+                  <NInput
+                    class={styles.w80}
+                    v-model:value={forms['abnormal_sign_out_fee']}
+                  ></NInput>
+                  <NSelect
+                    class={styles.w60}
+                    options={selectUnit}
+                    v-model:value={forms['abnormal_sign_out_fee_type']}
+                    defaultValue={'MONEY'}
+                  />
+                </NInputGroup>
+              </NFormItem>
+            </NGridItem>
+            <NGridItem>
+              <div class={styles.tips}>
+                例: <span class={styles.red}>17:00:00</span> 当日课程结束,在
+                <span class={styles.red}>
+                  {reduceFormMinute('17:00:00', forms['abnormal_sign_out'])} ~{' '}
+                  {reduceFormMinuteMS('17:00:00', 0)}
+                </span>
+                时间段内签退为
+                <span class={styles.red}>异常签到</span>。
+              </div>
+            </NGridItem>
+          </NGrid>
+
+          {forms['abnormal_sign_out'] && (
+            <>
+              <h3>早退</h3>
+              <NGrid class={styles.pl13} cols={1}>
+                <NGridItem class={styles.inlineFlex}>
+                  <NFormItem
+                    label="课程结束前"
+                    path="premise_sign_out"
+                    rule={[
+                      {
+                        required: true,
+                        pattern: /^\d+$/,
+                        message: '请输入分钟数',
+                        trigger: ['blur', 'input']
+                      }
+                    ]}
+                  >
+                    <NInput
+                      class={[styles.w140, styles.mr14]}
+                      v-model:value={forms['premise_sign_out']}
+                      disabled
+                    >
+                      {{ suffix: () => '分钟' }}
+                    </NInput>
+                  </NFormItem>
+                  <NFormItem
+                    label="以上签退,扣减当日训练补助"
+                    showRequireMark={false}
+                    path="premise_sign_out_fee"
+                    rule={[
+                      {
+                        required: true,
+                        pattern: /^\d+$/,
+                        message: '请输入金额',
+                        trigger: ['blur', 'input']
+                      }
+                    ]}
+                  >
+                    <NInputGroup class={[styles.mr14, styles.w140]}>
+                      <NInput
+                        class={styles.w80}
+                        v-model:value={forms['premise_sign_out_fee']}
+                      ></NInput>
+                      <NSelect
+                        class={styles.w60}
+                        options={selectUnit}
+                        v-model:value={forms['premise_sign_out_fee_type']}
+                        defaultValue={'MONEY'}
+                      />
+                    </NInputGroup>
+                  </NFormItem>
+                </NGridItem>
+                <NGridItem>
+                  <div class={styles.tips}>
+                    例: <span class={styles.red}>17:00:00</span> 当日课程结束,在
+                    <span class={styles.red}>
+                      {reduceFormMinute('17:00:00', forms['premise_sign_out'])}
+                    </span>
+                    前签退(含
+                    <span class={styles.red}>
+                      {reduceFormMinute('17:00:00', forms['premise_sign_out'])}
+                    </span>
+                    )为
+                    <span class={styles.red}>早退</span>。
+                  </div>
+                </NGridItem>
+              </NGrid>
+            </>
+          )}
+
+          {/* <h3>旷课</h3>
+          <NGrid class={styles.pl13} cols={1}>
+            <NGridItem class={styles.inlineFlex}>
+              <NFormItem
+                label="课程开始"
+                path="absence_sign_out"
+                rule={[
+                  {
+                    required: true,
+                    pattern: /^\d+$/,
+                    message: '请输入分钟数',
+                    trigger: ['blur', 'input']
+                  }
+                ]}
+              >
+                <NInput class={[styles.w140, styles.mr14]} v-model:value={forms['absence_sign_out']}>
+                  {{ suffix: () => '分钟' }}
+                </NInput>
+              </NFormItem>
+              <NFormItem
+                label="后签到,扣减当日训练补助"
+                showRequireMark={false}
+                path="absence_sign_out_fee"
+                rule={[
+                  {
+                    required: true,
+                    pattern: /^\d+$/,
+                    message: '请输入金额',
+                    trigger: ['blur', 'input']
+                  }
+                ]}
+              >
+                <NInputGroup class={[styles.mr14, styles.w140]}>
+                  <NInput class={styles.w80} v-model:value={forms['absence_sign_out_fee']}></NInput>
+                  <NSelect
+                    class={styles.w60}
+                    options={selectUnit}
+                    v-model:value={forms['absence_sign_out_fee_type']}
+                    defaultValue={'MONEY'}
+                  />
+                </NInputGroup>
+              </NFormItem>
+            </NGridItem>
+            <NGridItem>
+              <div class={styles.tips}>
+                例: <span class={styles.red}>12:00:00</span> 开始的课程,在
+                <span class={styles.red}>12:30:01 ~ 12:00:00</span> 以后签到为
+                <span class={styles.red}>旷课</span>
+              </div>
+            </NGridItem>
+          </NGrid> */}
+
+          <h3>未签退</h3>
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="课程未签退,扣减当日训练补助"
+              path="not_sign_out_fee"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入金额',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInputGroup class={[styles.mr14, styles.w140]}>
+                <NInput class={styles.w80} v-model:value={forms['not_sign_out_fee']}></NInput>
+                <NSelect
+                  class={styles.w60}
+                  options={selectUnit}
+                  v-model:value={forms['not_sign_out_fee_type']}
+                  defaultValue={'MONEY'}
+                />
+              </NInputGroup>
+            </NFormItemGi>
+          </NGrid>
+
+          <h3>签退提醒</h3>
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="当日训练结束后"
+              path="sign_out_remind"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入分钟数',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput class={[styles.w140, styles.mr14]} v-model:value={forms['sign_out_remind']}>
+                {{ suffix: () => '分钟' }}
+              </NInput>
+              对当日存在未签退课程的老师发送短信、推送、公众号提醒
+            </NFormItemGi>
+          </NGrid>
+        </NForm>
+
+        <NButton
+          type="primary"
+          onClick={onSubmit}
+          loading={btnLoading.value}
+          //v-auth="sysParamConfig/update1597903049401421825"
+        >
+          保存设置
+        </NButton>
+      </>
+    )
+  }
+})

+ 209 - 0
src/views/system-manage/param-settings/component/finance-rule.tsx

@@ -0,0 +1,209 @@
+import { NAlert, NButton, NForm, NFormItemGi, NGrid, NInput, useMessage } from 'naive-ui'
+import { defineComponent, reactive, ref, shallowRef } from 'vue'
+import { sysParamConfigPage, sysParamConfigUpdate } from '../../api'
+import styles from './index.module.less'
+
+export default defineComponent({
+  name: 'finance-rule',
+  setup() {
+    const forms = reactive({}) as any
+    const beforeData: any = shallowRef({}) // 储存原始数据
+    const formsRef = ref()
+    const btnLoading = ref(false)
+    const message = useMessage()
+
+    const getList = async () => {
+      try {
+        const { data } = await sysParamConfigPage({ page: 1, rows: 999, group: 'FINANCE' })
+
+        const rows = data.rows || []
+        rows.forEach((row: any) => {
+          if (row.paramName === 'refund_reason') {
+            forms[row.paramName] = row.paramValue.trim()
+          } else {
+            forms[row.paramName] = row.paramValue
+          }
+        })
+        beforeData.value = { ...forms }
+      } catch {}
+    }
+
+    // 数组进行对比
+    const dataCompare = (beforeData: any, afterData: any) => {
+      const changeDate: any = []
+      for (let key in beforeData) {
+        if (beforeData[key] != afterData[key]) {
+          changeDate.push({
+            paramName: key,
+            paramValue: afterData[key]
+          })
+        }
+      }
+      return changeDate || []
+    }
+
+    const onSubmit = async () => {
+      formsRef.value.validate(async (errors: any) => {
+        if (errors) return
+
+        const submitData = dataCompare(beforeData.value, forms)
+        if (submitData && submitData.length > 0) {
+          btnLoading.value = true
+          try {
+            await sysParamConfigUpdate({ configs: [...submitData], group: 'FINANCE' })
+            message.success('保存成功')
+            beforeData.value = { ...forms }
+          } catch {}
+          btnLoading.value = false
+        } else {
+          message.success('保存成功')
+        }
+      })
+    }
+
+    getList()
+    return () => (
+      <>
+        <NForm ref={formsRef} labelPlacement="left" model={forms} requireMarkPlacement="left">
+          <NAlert title="训练补助" showIcon={false} bordered={false} style="margin-bottom: 12px;" />
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="训练结束"
+              path="training_end"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入训练结束天数',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput class={[styles.w140, styles.mr14]} v-model:value={forms['training_end']}>
+                {{ suffix: () => '天' }}
+              </NInput>
+              后,发放训练补助
+            </NFormItemGi>
+          </NGrid>
+
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="训练补助结算扣除"
+              path="subsidy_tax_rate"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入训练补助结算百分比',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput class={[styles.w140, styles.mr14]} v-model:value={forms['subsidy_tax_rate']}>
+                {{ suffix: () => '‰' }}
+              </NInput>
+              手续费
+            </NFormItemGi>
+          </NGrid>
+
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="申请提现后"
+              path="subsidy_withdraw"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入申请提现的工作日',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput class={[styles.w140, styles.mr14]} v-model:value={forms['subsidy_withdraw']}>
+                {{ suffix: () => '个工作日' }}
+              </NInput>
+              内打款
+            </NFormItemGi>
+          </NGrid>
+
+          <NAlert title="交易退费" showIcon={false} bordered={false} style="margin-bottom: 12px;" />
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="交易完成"
+              path="transaction_complete"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入交易完成天数',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput
+                class={[styles.w140, styles.mr14]}
+                v-model:value={forms['transaction_complete']}
+              >
+                {{ suffix: () => '天' }}
+              </NInput>
+              内可申请退费
+            </NFormItemGi>
+          </NGrid>
+
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="退费扣除"
+              path="transaction_complete_tax_rate"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入退费扣除百分比',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput
+                class={[styles.w140, styles.mr14]}
+                v-model:value={forms['transaction_complete_tax_rate']}
+              >
+                {{ suffix: () => '‰' }}
+              </NInput>
+              手续费
+            </NFormItemGi>
+          </NGrid>
+
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="退费原因"
+              path="refund_reason"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入退费原因',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput
+                type="textarea"
+                class={[styles.mr14]}
+                style={'width: 400px !important;'}
+                v-model:value={forms['refund_reason']}
+              ></NInput>
+            </NFormItemGi>
+          </NGrid>
+        </NForm>
+
+        <NButton
+          type="primary"
+          onClick={onSubmit}
+          loading={btnLoading.value}
+          //v-auth="sysParamConfig/update1597903120771698689"
+        >
+          保存设置
+        </NButton>
+      </>
+    )
+  }
+})

+ 42 - 0
src/views/system-manage/param-settings/component/index.module.less

@@ -0,0 +1,42 @@
+.mr14 {
+  margin-right: 14px;
+}
+
+.inlineFlex {
+  display: inline-flex !important;
+}
+
+.w140 {
+  width: 140px !important;
+}
+
+.w80 {
+  width: 80px !important;
+}
+
+.w60 {
+  width: 60px !important;
+}
+
+.red {
+  color: red;
+}
+
+h3 {
+  margin-bottom: 12px;
+  padding-left: 13px;
+}
+
+.pl13 {
+  padding-left: 13px;
+}
+
+.tips {
+  font-size: 13px;
+  color: #333;
+  margin-bottom: 20px;
+  background: #f7f7f7;
+  padding: 6px 12px;
+  border-radius: 4px;
+  max-width: 650px;
+}

+ 130 - 0
src/views/system-manage/param-settings/component/leave-category-operation.tsx

@@ -0,0 +1,130 @@
+import { clientTypeArray, openTypeArray } from '@/utils/searchArray'
+import {
+  NForm,
+  NFormItem,
+  NInput,
+  NSpace,
+  NButton,
+  useMessage,
+  NRadioGroup,
+  NRadio,
+  NSelect
+} from 'naive-ui'
+import { defineComponent, onMounted, PropType, reactive, ref } from 'vue'
+import { leaveCategorySave, leaveCategoryUpdate } from '../../api'
+export default defineComponent({
+  name: 'role-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      clientType: null,
+      name: null,
+      remark: null
+    })
+    const btnLoading = ref(false)
+
+    const formsRef = ref()
+    const message = useMessage()
+
+    const onSubmit = async () => {
+      formsRef.value.validate(async (error: any) => {
+        if (error) return false
+        try {
+          btnLoading.value = true
+          if (props.type === 'add') {
+            await leaveCategorySave({ ...forms })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await leaveCategoryUpdate({ ...forms, id: props.data.id })
+            message.success('修改成功')
+          }
+
+          emit('close')
+          emit('getList')
+        } catch (e: any) {
+          console.log(e, 'e')
+        }
+        btnLoading.value = false
+      })
+    }
+
+    onMounted(async () => {
+      if (props.type === 'edit') {
+        const data = props.data
+        forms.clientType = data.clientType
+        forms.name = data.name
+        forms.remark = data.remark
+      }
+    })
+
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="auto">
+          <NFormItem
+            label="客户端类型"
+            path="clientType"
+            rule={[
+              {
+                required: true,
+                message: '请输入客户端类型'
+              }
+            ]}
+          >
+            <NSelect
+              placeholder="请输入客户端类型"
+              v-model:value={forms.clientType}
+              options={clientTypeArray}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem
+            label="类型名称"
+            path="name"
+            rule={[
+              {
+                required: true,
+                message: '请输入类型名称'
+              }
+            ]}
+          >
+            <NInput
+              v-model:value={forms.name}
+              placeholder="请输入类型名称"
+              clearable
+              maxlength={100}
+            ></NInput>
+          </NFormItem>
+          <NFormItem label="备注" path="remark">
+            <NInput
+              v-model:value={forms.remark}
+              maxlength={180}
+              type="textarea"
+              rows={3}
+              placeholder="请输入备注"
+              clearable
+            />
+          </NFormItem>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton type="primary" onClick={onSubmit} loading={btnLoading.value}>
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 161 - 0
src/views/system-manage/param-settings/component/leave-category.tsx

@@ -0,0 +1,161 @@
+import Pagination from '@/components/pagination'
+import { NButton, NDataTable, NModal, NSpace, NTag, useDialog, useMessage } from 'naive-ui'
+import { defineComponent, onMounted, reactive } from 'vue'
+import { leaveCategoryPage, leaveCategoryRemove } from '../../api'
+import { filterClientType, filterMessageGroup } from '@/utils/filters'
+import LeaveCategoryOperation from './leave-category-operation'
+import TheTooltip from '@/components/TheTooltip'
+
+export default defineComponent({
+  name: 'subsidy-list',
+  setup() {
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      dataList: [] as any,
+      visiableLeave: false,
+      leaveOperation: 'add',
+      leaveData: {} as any
+    })
+    const dialog = useDialog()
+    const message = useMessage()
+
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '客户端类型',
+          key: 'clientType',
+          render(row: any) {
+            return filterClientType(row.clientType)
+          }
+        },
+        {
+          title: '类型名称',
+          key: 'name',
+          render(row: any) {
+            return <TheTooltip content={row.name} />
+          }
+        },
+        {
+          title: '备注',
+          key: 'remark',
+          render(row: any) {
+            return <TheTooltip content={row.remark} />
+          }
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="leaveCategory/update1610189620435570689"
+                  onClick={() => {
+                    state.visiableLeave = true
+                    state.leaveOperation = 'edit'
+                    state.leaveData = row
+                  }}
+                >
+                  修改
+                </NButton>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  onClick={() => onRemove(row)}
+                  //v-auth="leaveCategory/remove1610189694385344513"
+                >
+                  删除
+                </NButton>
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const onRemove = (row: any): void => {
+      dialog.warning({
+        title: '警告',
+        content: `删除"${row.name}",是否继续?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await leaveCategoryRemove({ id: row.id })
+            getList()
+            message.success('删除成功')
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await leaveCategoryPage({ ...state.pagination })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <>
+        <NSpace style="padding-bottom: 12px">
+          <NButton
+            type="primary"
+            //v-auth="leaveCategory/save1610189531537297410"
+            onClick={() => {
+              state.visiableLeave = true
+              state.leaveOperation = 'add'
+              state.leaveData = []
+            }}
+          >
+            新增
+          </NButton>
+        </NSpace>
+        <NDataTable loading={state.loading} columns={columns()} data={state.dataList}></NDataTable>
+        <Pagination
+          v-model:page={state.pagination.page}
+          v-model:pageSize={state.pagination.rows}
+          v-model:pageTotal={state.pagination.pageTotal}
+          onList={getList}
+          sync
+        ></Pagination>
+
+        <NModal
+          v-model:show={state.visiableLeave}
+          preset="dialog"
+          showIcon={false}
+          title={state.leaveOperation === 'add' ? '新增请假类型' : '修改请假类型'}
+          style={{ width: '600px' }}
+        >
+          <LeaveCategoryOperation
+            type={state.leaveOperation}
+            data={state.leaveData}
+            onClose={() => (state.visiableLeave = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </>
+    )
+  }
+})

+ 110 - 0
src/views/system-manage/param-settings/component/member-fee.tsx

@@ -0,0 +1,110 @@
+import { NAlert, NButton, NForm, NFormItemGi, NGrid, NInput, useMessage } from 'naive-ui'
+import { defineComponent, reactive, ref, shallowRef } from 'vue'
+import { getVipPriceSettings, setVipPriceSettings } from '../../api'
+import styles from './index.module.less'
+
+export default defineComponent({
+  name: 'member-fee',
+  setup() {
+    const forms = reactive({ id: 0, originalPrice: 0, salePrice: 0 }) as any
+    const beforeData: any = shallowRef({}) // 储存原始数据
+    const formsRef = ref()
+    const btnLoading = ref(false)
+    const message = useMessage()
+
+    const getList = async () => {
+      try {
+        const { data } = await getVipPriceSettings({ page: 1, rows: 999, cardTypes: ['VIP'] })
+
+        const rows = data.rows[0]
+
+        forms.id = rows.id
+        forms.originalPrice = rows.originalPrice
+        forms.salePrice = rows.salePrice
+        console.log(forms)
+        // beforeData.value = { ...forms }
+      } catch {}
+    }
+
+    // // 数组进行对比
+    // const dataCompare = (beforeData: any, afterData: any) => {
+    //   const changeDate: any = []
+    //   for (let key in beforeData) {
+    //     if (beforeData[key] != afterData[key]) {
+    //       changeDate.push({
+    //         id: key,
+    //         paramValue: afterData[key]
+    //       })
+    //     }
+    //   }
+    //   return changeDate || []
+    // }
+
+    const onSubmit = async () => {
+      formsRef.value.validate(async (errors: any) => {
+        if (errors) return
+        try {
+          const res = await setVipPriceSettings({ ...forms })
+        } catch (e) {
+          console.log(e)
+        }
+        message.success('保存成功')
+      })
+    }
+
+    getList()
+    return () => (
+      <>
+        <NForm ref={formsRef} labelPlacement="left" model={forms} requireMarkPlacement="left">
+          <NAlert title="会员价格" showIcon={false} bordered={false} style="margin-bottom: 12px;" />
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="会员原价"
+              path="originalPrice"
+              rule={[
+                {
+                  required: true,
+                  pattern: /(^[1-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9]\.[0-9]([0-9])?$)/,
+                  message: '请输正确的会员原价',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput class={[styles.w140, styles.mr14]} v-model:value={forms.originalPrice}>
+                {{ suffix: () => '元' }}
+              </NInput>
+            </NFormItemGi>
+          </NGrid>
+
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="会员售价"
+              path="salePrice"
+              rule={[
+                {
+                  required: true,
+                  pattern: /(^[1-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9]\.[0-9]([0-9])?$)/,
+                  message: '请输正确的入会员售价',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput class={[styles.w140, styles.mr14]} v-model:value={forms.salePrice}>
+                {{ suffix: () => '元' }}
+              </NInput>
+            </NFormItemGi>
+          </NGrid>
+        </NForm>
+
+        <NButton
+          type="primary"
+          onClick={onSubmit}
+          loading={btnLoading.value}
+          //v-auth="vipPriceSettings/update1623234797500751874"
+        >
+          保存设置
+        </NButton>
+      </>
+    )
+  }
+})

+ 255 - 0
src/views/system-manage/param-settings/component/message-operation.tsx

@@ -0,0 +1,255 @@
+import { getMenus } from '@/api/system/menu'
+import UploadFile from '@/components/upload-file'
+import { clientTypeArray, messageGroupArray, openTypeArray } from '@/utils/searchArray'
+import { SearchOutline } from '@vicons/ionicons5'
+import {
+  NForm,
+  NFormItemGi,
+  NInput,
+  NSpace,
+  NButton,
+  useMessage,
+  NRadioGroup,
+  NRadio,
+  NSelect,
+  NGrid
+} from 'naive-ui'
+import { defineComponent, onMounted, PropType, reactive, ref } from 'vue'
+import { sysMessageConfigSave, sysMessageConfigUpdate } from '../../api'
+const messageTypeArray = [
+  { label: 'CUSTOME_MESSAGE_PUSH', value: 'CUSTOME_MESSAGE_PUSH' },
+  { label: 'SMS_VERIFY_CODE_LOGIN', value: 'SMS_VERIFY_CODE_LOGIN' },
+  { label: 'SMS_VERIFY_CODE_LOGOFF', value: 'SMS_VERIFY_CODE_LOGOFF' },
+  { label: 'SMS_VERIFY_CODE_REGISTER', value: 'SMS_VERIFY_CODE_REGISTER' },
+  { label: 'SMS_VERIFY_CODE_UPDATE_PSW', value: 'SMS_VERIFY_CODE_UPDATE_PSW' },
+  { label: 'SMS_VERIFY_CODE_UPDATE_PHONE', value: 'SMS_VERIFY_CODE_UPDATE_PHONE' },
+  { label: 'SMS_VERIFY_CODE', value: 'SMS_VERIFY_CODE' },
+  { label: 'CAPTCHA_SESSION_KEY', value: 'CAPTCHA_SESSION_KEY' },
+  { label: 'SMS_VERIFY_CODE_BANK_CARD', value: 'SMS_VERIFY_CODE_BANK_CARD' },
+  { label: 'STUDENT_JOIN_FANSGROUP', value: 'STUDENT_JOIN_FANSGROUP' },
+  { label: 'FANSGROUP_APPLY_SUCCESS', value: 'FANSGROUP_APPLY_SUCCESS' }
+]
+
+const subTypeArray = [
+  { label: 'TEACHER_AUTH', value: 'TEACHER_AUTH' },
+  { label: 'PRACTICE', value: 'PRACTICE' },
+  { label: 'MUSIC_SHEET', value: 'MUSIC_SHEET' },
+  { label: 'COURSE', value: 'COURSE' },
+  { label: 'INCOME', value: 'INCOME' },
+  { label: 'HOMEWORK', value: 'HOMEWORK' },
+  { label: 'EVALUATE', value: 'EVALUATE' },
+  { label: 'GROUP_CHAT', value: 'GROUP_CHAT' },
+  { label: 'VIP', value: 'VIP' }
+]
+
+const actionArray = [
+  { label: 'h5', value: 'h5' },
+  { label: 'app', value: 'app' }
+]
+
+export default defineComponent({
+  name: 'role-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      sendFlag: null,
+      clientId: null,
+      actionUrl: null,
+      icon: null,
+      description: null,
+      params: null,
+      content: null,
+      messageType: null,
+      action: null,
+      subType: null,
+      group: null
+    }) as any
+    const btnLoading = ref(false)
+
+    const formsRef = ref()
+    const message = useMessage()
+
+    const onSubmit = async () => {
+      console.log('date', '1212')
+      formsRef.value.validate(async (error: any) => {
+        if (error) return false
+
+        try {
+          btnLoading.value = true
+          if (props.type === 'add') {
+            await sysMessageConfigSave({ ...forms })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await sysMessageConfigUpdate({ ...forms, id: props.data.id })
+            message.success('修改成功')
+          }
+
+          emit('close')
+          emit('getList')
+        } catch (e: any) {
+          console.log(e, 'e')
+        }
+        btnLoading.value = false
+      })
+    }
+
+    onMounted(async () => {
+      if (props.type === 'edit') {
+        const data = props.data
+        forms.sendFlag = data.sendFlag
+        forms.clientId = data.clientId
+        forms.actionUrl = data.actionUrl
+        forms.icon = data.icon
+        forms.description = data.description
+        forms.params = data.params
+        forms.content = data.content
+        forms.messageType = data.messageType
+        forms.action = data.action
+        forms.subType = data.subType
+        forms.group = data.group
+      }
+    })
+
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="100">
+          <NGrid cols={2}>
+            <NFormItemGi label="图标地址" path="icon">
+              <UploadFile size={5} listType="image-card" v-model:fileList={forms.icon} />
+            </NFormItemGi>
+            <NFormItemGi
+              label="是否发送"
+              path="sendFlag"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择是否发送'
+                }
+              ]}
+            >
+              <NRadioGroup v-model:value={forms.sendFlag}>
+                <NRadio value={1}>发送</NRadio>
+                <NRadio value={0}>不发送</NRadio>
+              </NRadioGroup>
+            </NFormItemGi>
+            <NFormItemGi
+              label="客户端类型"
+              path="clientId"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入客户端类型'
+                }
+              ]}
+            >
+              <NSelect
+                placeholder="请输入客户端类型"
+                v-model:value={forms.clientId}
+                options={clientTypeArray}
+              />
+            </NFormItemGi>
+            <NFormItemGi
+              label="消息类型"
+              path="messageType"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入消息类型'
+                }
+              ]}
+            >
+              <NSelect
+                placeholder="请输入消息类型"
+                v-model:value={forms.messageType}
+                options={messageTypeArray}
+              />
+            </NFormItemGi>
+            <NFormItemGi
+              label="消息组"
+              path="group"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入消息组'
+                }
+              ]}
+            >
+              <NSelect
+                placeholder="请输入消息组"
+                v-model:value={forms.group}
+                options={messageGroupArray}
+              />
+            </NFormItemGi>
+            <NFormItemGi label="二级分类" path="subType">
+              <NSelect
+                placeholder="请输入二级分类"
+                v-model:value={forms.subType}
+                options={subTypeArray}
+              />
+            </NFormItemGi>
+            <NFormItemGi label="跳转类型" path="action">
+              <NSelect
+                placeholder="请输入跳转类型"
+                v-model:value={forms.action}
+                options={actionArray}
+              />
+            </NFormItemGi>
+            <NFormItemGi label="跳转连接" path="actionUrl">
+              <NInput
+                v-model:value={forms.actionUrl}
+                placeholder="请输入跳转连接"
+                clearable
+                maxlength={100}
+              ></NInput>
+            </NFormItemGi>
+            <NFormItemGi label="其它参数" path="params">
+              <NInput
+                v-model:value={forms.params}
+                placeholder="请输入其它参数"
+                clearable
+                maxlength={100}
+              ></NInput>
+            </NFormItemGi>
+            <NFormItemGi label="消息内容" path="content">
+              <NInput
+                v-model:value={forms.content}
+                placeholder="请输入消息内容"
+                clearable
+                maxlength={100}
+              ></NInput>
+            </NFormItemGi>
+            <NFormItemGi label="描述" path="description">
+              <NInput
+                v-model:value={forms.description}
+                maxlength={180}
+                type="textarea"
+                rows={2}
+                placeholder="请输入描述"
+                clearable
+              />
+            </NFormItemGi>
+          </NGrid>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton type="primary" onClick={onSubmit} loading={btnLoading.value}>
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 180 - 0
src/views/system-manage/param-settings/component/message-setting.tsx

@@ -0,0 +1,180 @@
+import Pagination from '@/components/pagination'
+import { NButton, NDataTable, NModal, NSpace, NTag, useDialog, useMessage } from 'naive-ui'
+import { defineComponent, onMounted, reactive } from 'vue'
+import { sysMessageConfigPage, sysMessageConfigRemove } from '../../api'
+import { filterClientType, filterMessageGroup } from '@/utils/filters'
+import MessageOperation from './message-operation'
+
+export default defineComponent({
+  name: 'subsidy-list',
+  setup() {
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      dataList: [] as any,
+      visiablePayment: false,
+      paymentOperation: 'add',
+      paymentData: {} as any
+    })
+    const dialog = useDialog()
+    const message = useMessage()
+
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '客户端类型',
+          key: 'clientId',
+          render(row: any) {
+            return filterClientType(row.clientId)
+          }
+        },
+        {
+          title: '消息类型',
+          key: 'messageType'
+        },
+        {
+          title: '消息跳转类型',
+          key: 'action'
+        },
+        {
+          title: '二级分类',
+          key: 'subType'
+        },
+        {
+          title: '消息组',
+          key: 'group',
+          render(row: any) {
+            return filterMessageGroup(row.group)
+          }
+        },
+        {
+          title: '消息是否发送',
+          key: 'sendFlag',
+          render(row: any) {
+            return row.sendFlag === 1 ? (
+              <NTag type="primary">发送</NTag>
+            ) : (
+              <NTag type="default">不发送</NTag>
+            )
+          }
+        },
+        {
+          title: '描述',
+          key: 'description'
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="sysMessageConfig/update1597903931924926465"
+                  onClick={() => {
+                    state.visiablePayment = true
+                    state.paymentOperation = 'edit'
+                    state.paymentData = row
+                  }}
+                >
+                  修改
+                </NButton>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  onClick={() => onRemove(row)}
+                  //v-auth="sysMessageConfig/remove1597904021058080770"
+                >
+                  删除
+                </NButton>
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const onRemove = (row: any): void => {
+      dialog.warning({
+        title: '警告',
+        content: `是否删除?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysMessageConfigRemove({ id: row.id })
+            getList()
+            message.success('删除成功')
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await sysMessageConfigPage({ ...state.pagination })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <>
+        <NSpace style="padding-bottom: 12px">
+          <NButton
+            type="primary"
+            //v-auth="sysMessageConfig/save1597903839524409345"
+            onClick={() => {
+              state.visiablePayment = true
+              state.paymentOperation = 'add'
+              state.paymentData = []
+            }}
+          >
+            新增
+          </NButton>
+        </NSpace>
+        <NDataTable loading={state.loading} columns={columns()} data={state.dataList}></NDataTable>
+        <Pagination
+          v-model:page={state.pagination.page}
+          v-model:pageSize={state.pagination.rows}
+          v-model:pageTotal={state.pagination.pageTotal}
+          onList={getList}
+          sync
+        ></Pagination>
+
+        <NModal
+          v-model:show={state.visiablePayment}
+          preset="dialog"
+          showIcon={false}
+          title={state.paymentOperation === 'add' ? '新增消息通知' : '修改消息通知'}
+          style={{ width: '800px' }}
+        >
+          <MessageOperation
+            type={state.paymentOperation}
+            data={state.paymentData}
+            onClose={() => (state.visiablePayment = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </>
+    )
+  }
+})

+ 401 - 0
src/views/system-manage/param-settings/component/other-rule.tsx

@@ -0,0 +1,401 @@
+import { deepClone } from '@/layout/components/Header/imkit/utils/utils'
+import { NAlert, NButton, NForm, NFormItemGi, NGrid, NInput, NSelect, useMessage } from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref, shallowRef } from 'vue'
+import { sysParamConfigPage, sysParamConfigUpdate } from '../../api'
+import styles from './index.module.less'
+
+export default defineComponent({
+  name: 'other-rule',
+  setup() {
+    const forms = reactive({}) as any
+    const beforeData: any = ref({}) // 储存原始数据
+    const formsRef = ref()
+    const btnLoading = ref(false)
+    const message = useMessage()
+
+    const getList = async () => {
+      try {
+        const { data } = await sysParamConfigPage({ page: 1, rows: 999, group: 'OTHER' })
+
+        const rows = data.rows || []
+        rows.forEach((row: any) => {
+          forms[row.paramName] = row.paramValue
+        })
+        beforeData.value = { ...forms }
+      } catch {}
+    }
+
+    // 数组进行对比
+    const dataCompare = (beforeData: any, afterData: any) => {
+      const changeDate: any = []
+      for (let key in beforeData) {
+        if (beforeData[key] != afterData[key]) {
+          changeDate.push({
+            paramName: key,
+            paramValue: afterData[key]
+          })
+        }
+      }
+      return changeDate || []
+    }
+
+    const onSubmit = async () => {
+      formsRef.value.validate(async (errors: any) => {
+        if (errors) return
+
+        const submitData = dataCompare(beforeData.value, forms)
+        if (submitData && submitData.length > 0) {
+          btnLoading.value = true
+          try {
+            await sysParamConfigUpdate({ configs: [...submitData], group: 'OTHER' })
+            message.success('保存成功')
+            beforeData.value = { ...forms }
+          } catch (e) {
+            console.log(e)
+          }
+          btnLoading.value = false
+        } else {
+          message.success('保存成功')
+        }
+      })
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <>
+        <NForm ref={formsRef} labelPlacement="left" model={forms} requireMarkPlacement="left">
+          <NAlert title="设备限制" showIcon={false} bordered={false} style="margin-bottom: 12px;" />
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="教务端每个账号可登录"
+              path="school_device_num"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入设备数',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput class={[styles.w140, styles.mr14]} v-model:value={forms['school_device_num']}>
+                {{ suffix: () => '台' }}
+              </NInput>
+              设备
+            </NFormItemGi>
+          </NGrid>
+
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="学生端每个账号可登录"
+              path="student_device_num"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入设备数',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput
+                class={[styles.w140, styles.mr14]}
+                v-model:value={forms['student_device_num']}
+              >
+                {{ suffix: () => '台' }}
+              </NInput>
+              设备
+            </NFormItemGi>
+          </NGrid>
+
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="老师端每个账号可登录"
+              path="teacher_device_num"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入设备数',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput
+                class={[styles.w140, styles.mr14]}
+                v-model:value={forms['teacher_device_num']}
+              >
+                {{ suffix: () => '台' }}
+              </NInput>
+              设备
+            </NFormItemGi>
+          </NGrid>
+
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="后端账号登录异常"
+              path="backend_login_locked"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入次数',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput
+                class={[styles.w140, styles.mr14]}
+                v-model:value={forms['backend_login_locked']}
+              >
+                {{ suffix: () => '次' }}
+              </NInput>
+              锁定
+            </NFormItemGi>
+          </NGrid>
+
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="教务端账号登录异常"
+              path="school_login_locked"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入次数',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput
+                class={[styles.w140, styles.mr14]}
+                v-model:value={forms['school_login_locked']}
+              >
+                {{ suffix: () => '次' }}
+              </NInput>
+              锁定
+            </NFormItemGi>
+          </NGrid>
+
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="学生端账号登录异常"
+              path="student_login_locked"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入次数',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput
+                class={[styles.w140, styles.mr14]}
+                v-model:value={forms['student_login_locked']}
+              >
+                {{ suffix: () => '次' }}
+              </NInput>
+              锁定
+            </NFormItemGi>
+          </NGrid>
+
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="老师端账号登录异常"
+              path="teacher_login_locked"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入次数',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput
+                class={[styles.w140, styles.mr14]}
+                v-model:value={forms['teacher_login_locked']}
+              >
+                {{ suffix: () => '次' }}
+              </NInput>
+              锁定
+            </NFormItemGi>
+          </NGrid>
+
+          {/* <NAlert
+            title="学期开始时间"
+            showIcon={false}
+            bordered={false}
+            style="margin-bottom: 12px;"
+          />
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="上学期开始时间"
+              path="last_term_time"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入上学期开始时间',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput class={[styles.w140, styles.mr14]} v-model:value={forms['last_term_time']}></NInput>
+            </NFormItemGi>
+          </NGrid>
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="下学期开始时间"
+              path="next_term_time"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入下学期开始时间',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput class={[styles.w140, styles.mr14]} v-model:value={forms['next_term_time']}></NInput>
+            </NFormItemGi>
+          </NGrid> */}
+
+          <NAlert
+            title="课程最早开始和最晚结束时间"
+            showIcon={false}
+            bordered={false}
+            style="margin-bottom: 12px;"
+          />
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="最早开始时间"
+              path="course_start_time"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入最早开始时间',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput
+                class={[styles.w140, styles.mr14]}
+                v-model:value={forms['course_start_time']}
+              ></NInput>
+            </NFormItemGi>
+          </NGrid>
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="最晚结束时间"
+              path="course_end_time"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入最晚结束时间',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput
+                class={[styles.w140, styles.mr14]}
+                v-model:value={forms['course_end_time']}
+              ></NInput>
+            </NFormItemGi>
+          </NGrid>
+
+          <NAlert
+            title="二维码有效期"
+            showIcon={false}
+            bordered={false}
+            style="margin-bottom: 12px;"
+          />
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="二维码过期时间"
+              path="qr_code_expire_hours"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入二维码过期时间',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput
+                class={[styles.w140, styles.mr14]}
+                v-model:value={forms['qr_code_expire_hours']}
+              >
+                {{ suffix: () => '小时' }}
+              </NInput>
+            </NFormItemGi>
+          </NGrid>
+
+          <NAlert
+            title="酷乐秀按钮设置"
+            showIcon={false}
+            bordered={false}
+            style="margin-bottom: 12px;"
+          />
+          <NGrid class={styles.pl13} cols={1}>
+            <NFormItemGi
+              label="是否开启酷乐秀"
+              path="clx_enable"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入天数',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NSelect
+                class={styles.w140}
+                options={[
+                  {
+                    label: '启用',
+                    value: '1'
+                  },
+                  {
+                    label: '停用',
+                    value: '0'
+                  }
+                ]}
+                v-model:value={forms['clx_enable']}
+              />
+            </NFormItemGi>
+            <NFormItemGi
+              label="开启酷乐秀按钮后,学生上课"
+              path="clx_hidden_day"
+              rule={[
+                {
+                  required: true,
+                  pattern: /^\d+$/,
+                  message: '请输入天数',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput class={[styles.w140, styles.mr14]} v-model:value={forms['clx_hidden_day']}>
+                {{ suffix: () => '天' }}
+              </NInput>
+              后展示按钮
+            </NFormItemGi>
+          </NGrid>
+        </NForm>
+
+        <NButton
+          type="primary"
+          onClick={onSubmit}
+          loading={btnLoading.value}
+          //v-auth="sysParamConfig/update1597903200849350657"
+        >
+          保存设置
+        </NButton>
+      </>
+    )
+  }
+})

+ 168 - 0
src/views/system-manage/param-settings/component/payment-operation.tsx

@@ -0,0 +1,168 @@
+import { getMenus } from '@/api/system/menu'
+import { clientTypeArray, openTypeArray } from '@/utils/searchArray'
+import { SearchOutline } from '@vicons/ionicons5'
+import {
+  NForm,
+  NFormItem,
+  NInput,
+  NSpace,
+  NButton,
+  useMessage,
+  NRadioGroup,
+  NRadio,
+  NSelect
+} from 'naive-ui'
+import { defineComponent, onMounted, PropType, reactive, ref } from 'vue'
+import { sysPaymentConfigSave, sysPaymentConfigUpdate } from '../../api'
+export default defineComponent({
+  name: 'role-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      paramName: null,
+      paramValue: null,
+      clientType: null,
+      openType: null,
+      description: null
+    })
+    const btnLoading = ref(false)
+
+    const formsRef = ref()
+    const message = useMessage()
+
+    const onSubmit = async () => {
+      formsRef.value.validate(async (error: any) => {
+        if (error) return false
+        try {
+          btnLoading.value = true
+          if (props.type === 'add') {
+            await sysPaymentConfigSave({ ...forms })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await sysPaymentConfigUpdate({ ...forms, id: props.data.id })
+            message.success('修改成功')
+          }
+
+          emit('close')
+          emit('getList')
+        } catch (e: any) {
+          console.log(e, 'e')
+        }
+        btnLoading.value = false
+      })
+    }
+
+    onMounted(async () => {
+      if (props.type === 'edit') {
+        const data = props.data
+        forms.paramName = data.paramName
+        forms.paramValue = data.paramValue
+        forms.clientType = data.clientType
+        forms.openType = data.openType
+        forms.description = data.description
+      }
+    })
+
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="100">
+          <NFormItem
+            label="提供方"
+            path="openType"
+            rule={[
+              {
+                required: true,
+                message: '请输入提供方'
+              }
+            ]}
+          >
+            <NSelect
+              placeholder="请输入提供方"
+              v-model:value={forms.openType}
+              options={openTypeArray}
+            />
+          </NFormItem>
+          <NFormItem
+            label="客户端类型"
+            path="clientType"
+            rule={[
+              {
+                required: true,
+                message: '请输入客户端类型'
+              }
+            ]}
+          >
+            <NSelect
+              placeholder="请输入客户端类型"
+              v-model:value={forms.clientType}
+              options={clientTypeArray}
+            />
+          </NFormItem>
+          <NFormItem
+            label="参数名称"
+            path="paramName"
+            rule={[
+              {
+                required: true,
+                message: '请输入参数名称'
+              }
+            ]}
+          >
+            <NInput
+              v-model:value={forms.paramName}
+              placeholder="请输入参数名称"
+              clearable
+              maxlength={100}
+            ></NInput>
+          </NFormItem>
+          <NFormItem
+            label="参数值"
+            path="paramValue"
+            rule={[
+              {
+                required: true,
+                message: '请输入参数值'
+              }
+            ]}
+          >
+            <NInput
+              v-model:value={forms.paramValue}
+              placeholder="请输入参数值"
+              clearable
+              maxlength={100}
+            ></NInput>
+          </NFormItem>
+          <NFormItem label="备注" path="description">
+            <NInput
+              v-model:value={forms.description}
+              maxlength={180}
+              type="textarea"
+              rows={2}
+              placeholder="请输入备注"
+              clearable
+            />
+          </NFormItem>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton type="primary" onClick={onSubmit} loading={btnLoading.value}>
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 167 - 0
src/views/system-manage/param-settings/component/payment-setting.tsx

@@ -0,0 +1,167 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import { NButton, NDataTable, NModal, NSpace, useDialog, useMessage } from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { sysPaymentConfigPage, sysPaymentConfigRemove } from '../../api'
+import { filterClientType, filterOpenType } from '@/utils/filters'
+import PaymentOperation from './payment-operation'
+
+export default defineComponent({
+  name: 'subsidy-list',
+  setup() {
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      dataList: [] as any,
+      visiablePayment: false,
+      paymentOperation: 'add',
+      paymentData: {} as any
+    })
+    const dialog = useDialog()
+    const message = useMessage()
+
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '提供方',
+          key: 'openType',
+          render(row: any) {
+            return filterOpenType(row.openType)
+          }
+        },
+        {
+          title: '客户端类型',
+          key: 'clientType',
+          render(row: any) {
+            return filterClientType(row.clientType)
+          }
+        },
+        {
+          title: '参数名称',
+          key: 'paramName'
+        },
+        {
+          title: '参数值',
+          key: 'paramValue'
+        },
+        {
+          title: '描述',
+          key: 'description'
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="sysPaymentConfig/update1597903527669518338"
+                  onClick={() => {
+                    state.visiablePayment = true
+                    state.paymentOperation = 'edit'
+                    state.paymentData = row
+                  }}
+                >
+                  修改
+                </NButton>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  onClick={() => onRemove(row)}
+                  //v-auth="sysPaymentConfig/remove1597903613933768705"
+                >
+                  删除
+                </NButton>
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const onRemove = (row: any): void => {
+      dialog.warning({
+        title: '警告',
+        content: `是否删除?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysPaymentConfigRemove({ id: row.id })
+            getList()
+            message.success('删除成功')
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await sysPaymentConfigPage({ ...state.pagination })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <>
+        <NSpace style="padding-bottom: 12px">
+          <NButton
+            type="primary"
+            //v-auth="sysPaymentConfig/save1597903459457552386"
+            onClick={() => {
+              state.visiablePayment = true
+              state.paymentOperation = 'add'
+              state.paymentData = []
+            }}
+          >
+            新增
+          </NButton>
+        </NSpace>
+        <NDataTable loading={state.loading} columns={columns()} data={state.dataList}></NDataTable>
+        <Pagination
+          v-model:page={state.pagination.page}
+          v-model:pageSize={state.pagination.rows}
+          v-model:pageTotal={state.pagination.pageTotal}
+          onList={getList}
+          sync
+          saveKey="payemnt-setting"
+        ></Pagination>
+
+        <NModal
+          v-model:show={state.visiablePayment}
+          preset="dialog"
+          showIcon={false}
+          title={state.paymentOperation === 'add' ? '新增支付参数' : '修改支付参数'}
+          style={{ width: '550px' }}
+        >
+          <PaymentOperation
+            type={state.paymentOperation}
+            data={state.paymentData}
+            onClose={() => (state.visiablePayment = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </>
+    )
+  }
+})

+ 91 - 0
src/views/system-manage/param-settings/index.tsx

@@ -0,0 +1,91 @@
+import { NTabPane, NTabs } from 'naive-ui'
+import { defineComponent, reactive } from 'vue'
+import { useRoute } from 'vue-router'
+import AttendanceRule from './component/attendance-rule'
+import FinanceRule from './component/finance-rule'
+import LeaveCategory from './component/leave-category'
+import MessageSetting from './component/message-setting'
+import OtherRule from './component/other-rule'
+import PaymentSetting from './component/payment-setting'
+import Memberfee from './component/member-fee'
+import { getTabsCache, setTabsCaches } from '@/hooks/use-async'
+export default defineComponent({
+  name: 'param-settings',
+  setup() {
+    const state = reactive({
+      tabName: 'attendance' as 'attendance' | 'finance' | 'other' | 'payment' | 'message' | 'leave'
+    })
+    const route = useRoute()
+    getTabsCache((val: any) => {
+      if (val.form.tabName) {
+        state.tabName = val.form.tabName
+      }
+    })
+    const setTabs = (val: any) => {
+      setTabsCaches(val, 'tabName', route)
+    }
+    return () => {
+      return (
+        <div class="system-menu-container">
+          <h2>参数设置</h2>
+
+          <div class={['section-container']} style="padding-top: 0">
+            <NTabs
+              type="line"
+              size="large"
+              v-model:value={state.tabName}
+              onUpdate:value={(val: any) => setTabs(val)}
+            >
+              <NTabPane
+                name="attendance"
+                tab="考勤规则"
+                //v-auth="sysParamConfig/page1597901894499815425"
+              >
+                <AttendanceRule />
+              </NTabPane>
+              <NTabPane
+                name="finance"
+                tab="财务规则"
+                //v-auth="sysParamConfig/page1597902460701495297"
+              >
+                <FinanceRule />
+              </NTabPane>
+              <NTabPane name="other" tab="其它参数" //v-auth="sysParamConfig/page1597902586396397570"
+              >
+                <OtherRule />
+              </NTabPane>
+              {/* <NTabPane
+                name="payment"
+                tab="支付参数"
+                v-auth="sysPaymentConfig/page1597902712028385281"
+              >
+                <PaymentSetting />
+              </NTabPane> */}
+              <NTabPane
+                name="message"
+                tab="消息通知设置"
+                //v-auth="sysMessageConfig/page1597902847743479810"
+              >
+                <MessageSetting />
+              </NTabPane>
+              <NTabPane
+                name="leave"
+                tab="请假类型"
+                //v-auth="sysMessageConfig/page1597902847743479810"
+              >
+                <LeaveCategory />
+              </NTabPane>
+              <NTabPane
+                name="member"
+                tab="会员价格配置"
+                //v-auth="vipPriceSettings/page1623234661835988994"
+              >
+                <Memberfee />
+              </NTabPane>
+            </NTabs>
+          </div>
+        </div>
+      )
+    }
+  }
+})

+ 111 - 0
src/views/system-manage/platform-suggestion/index.tsx

@@ -0,0 +1,111 @@
+import Pagination from '@/components/pagination'
+import { NDataTable } from 'naive-ui'
+import { defineComponent, onMounted, reactive } from 'vue'
+import { sysSuggestionPage } from '../api'
+import { filterClientType, filterSuggestionType } from '@/utils/filters'
+import TheTooltip from '@/components/TheTooltip'
+
+export default defineComponent({
+  name: 'subsidy-list',
+  setup() {
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        keyword: null,
+        status: null
+      },
+      dataList: [] as any
+    })
+    const columns = () => {
+      return [
+        {
+          title: '建议类型',
+          key: 'type',
+          width: 100,
+          render(row: any) {
+            return filterSuggestionType(row.type)
+          }
+        },
+        {
+          title: '学校名称',
+          key: 'schoolName',
+          render(row: any) {
+            return <TheTooltip content={row.schoolName} />
+          }
+        },
+        {
+          title: '反馈时间',
+          key: 'createTime'
+        },
+        {
+          title: '内容',
+          key: 'content',
+          render(row: any) {
+            return <TheTooltip content={row.content} />
+          }
+        },
+        {
+          title: '用户',
+          key: 'nickname'
+        },
+        {
+          title: '手机号',
+          key: 'mobileNo'
+        },
+        {
+          title: '客户端',
+          key: 'clientType',
+          render(row: any) {
+            return filterClientType(row.clientType)
+          }
+        },
+        {
+          title: '设备号',
+          key: 'userAgent',
+          width: '300'
+        }
+      ]
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await sysSuggestionPage({ ...state.pagination, ...state.searchForm })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <h2>意见反馈</h2>
+
+        <div class={['section-container']}>
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+          ></Pagination>
+        </div>
+      </div>
+    )
+  }
+})

+ 226 - 0
src/views/system-manage/protocol-manage/add-protocol.tsx

@@ -0,0 +1,226 @@
+import UploadFile from '@/components/upload-file'
+import { protocolTypeArray } from '@/utils/searchArray'
+import { NForm, NFormItem, NInput, NSelect, NButton, NSpace, NModal, useMessage } from 'naive-ui'
+import { defineComponent, PropType, reactive, ref } from 'vue'
+import { schoolContractTemplateSave } from '../api'
+
+export default defineComponent({
+  name: 'add-protocol',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const message = useMessage()
+    const visiable = ref<boolean>(false)
+    const forms = reactive({
+      name: '',
+      type: null,
+      contractNo: null,
+      origanalFileUrl: null,
+      contractTemplateContent: null,
+      status: false,
+      timeFlag: null,
+      time: null
+    })
+    const formsRef = ref()
+
+    const onSubmit = () => {
+      formsRef.value.validate(async (error: any) => {
+        if (error) return false
+
+        try {
+          await schoolContractTemplateSave({ ...forms, id: props.data.id })
+          message.success('添加成功')
+          emit('getList')
+          emit('close')
+        } catch {}
+      })
+    }
+
+    const displayResult = (result: any) => {
+      let html = result.value
+      let newHTML = html
+        .replace(//g, '')
+        .replace('<h1>', '<h2 style="font-size: 16px;font-weight: bold; padding-top: 15px;">')
+      forms.contractTemplateContent = newHTML
+    }
+
+    const readFileInputEventAsArrayBuffer = (file: any) => {
+      let reader = new FileReader()
+      reader.onload = function (loadEvent: any) {
+        let arrayBuffer = loadEvent.target.result //arrayBuffer
+        // @ts-ignore
+        mammoth
+          .convertToHtml({ arrayBuffer: arrayBuffer })
+          .then(function (result: any) {
+            displayResult(result)
+          })
+          .catch(function (e: any) {
+            forms.origanalFileUrl = null
+            message.error('上传文件有误,请重新上传')
+          })
+          .done()
+      }
+      reader.readAsArrayBuffer(file)
+    }
+
+    // 只能输入数字
+    const onlyAllowNumber = (value: string) =>
+      !value || /^(?:0.\d{0,3}|[0-9][0-9]{0,12}|[0-9]{1,10}.\d{0,3})$/.test(value)
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="110px">
+          <NFormItem
+            label="协议名称"
+            path="name"
+            rule={[
+              {
+                required: true,
+                message: '请输入协议名称'
+              }
+            ]}
+          >
+            <NInput placeholder="请输入协议名称" v-model:value={forms.name} />
+          </NFormItem>
+          <NFormItem
+            label="协议类型"
+            path="type"
+            rule={[
+              {
+                required: true,
+                message: '请选择协议类型'
+              }
+            ]}
+          >
+            <NSelect
+              placeholder="请选择协议类型"
+              options={protocolTypeArray}
+              v-model:value={forms.type}
+            ></NSelect>
+          </NFormItem>
+          <NFormItem
+            label="协议号"
+            path="contractNo"
+            rule={[
+              {
+                required: true,
+                message: '请输入协议号'
+              }
+            ]}
+          >
+            <NInput placeholder="请输入协议号" v-model:value={forms.contractNo} />
+          </NFormItem>
+          <NFormItem
+            label="签署时长状态"
+            path="timeFlag"
+            rule={[
+              {
+                required: true,
+                message: '请输入签署时长状态'
+              }
+            ]}
+          >
+            <NSelect
+              placeholder="请选择签署时长"
+              options={
+                [
+                  { label: '显示', value: true },
+                  { label: '隐藏', value: false }
+                ] as any
+              }
+              v-model:value={forms.timeFlag}
+            ></NSelect>
+          </NFormItem>
+          {/* 显示才要输入时长 */}
+          {forms.timeFlag && (
+            <NFormItem
+              label="签署时长"
+              path="time"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入签署时长'
+                }
+              ]}
+            >
+              <NInput
+                placeholder="请输入签署时长(年)"
+                v-model:value={forms.time}
+                allowInput={onlyAllowNumber}
+                maxlength={2}
+              />
+            </NFormItem>
+          )}
+          <NFormItem
+            label="上传协议"
+            path="origanalFileUrl"
+            rule={[
+              {
+                required: true,
+                message: '请上传协议'
+              }
+            ]}
+          >
+            <UploadFile
+              size={5}
+              listType="image"
+              tips="仅支持上传 docx 格式文件"
+              accept=".docx"
+              v-model:fileList={forms.origanalFileUrl}
+              onReadFileInputEventAsArrayBuffer={readFileInputEventAsArrayBuffer}
+              onRemove={() => {
+                forms.contractTemplateContent = null
+              }}
+            />
+          </NFormItem>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton
+            type="primary"
+            onClick={() => {
+              formsRef.value.validate(async (error: any) => {
+                if (error) return false
+                visiable.value = true
+              })
+            }}
+          >
+            下一步
+          </NButton>
+        </NSpace>
+
+        <NModal
+          v-model:show={visiable.value}
+          preset="dialog"
+          showIcon={false}
+          title="查看协议"
+          style={{ width: '400px' }}
+        >
+          <div
+            style="max-height: 450px; overflow-y: auto;"
+            v-html={forms.contractTemplateContent}
+          ></div>
+          <NSpace justify="end" style="padding-top: 16px">
+            <NButton type="default" onClick={() => (visiable.value = false)}>
+              取消
+            </NButton>
+            <NButton type="primary" onClick={onSubmit}>
+              确定
+            </NButton>
+          </NSpace>
+        </NModal>
+      </div>
+    )
+  }
+})

+ 266 - 0
src/views/system-manage/protocol-manage/index.tsx

@@ -0,0 +1,266 @@
+import Pagination from '@/components/pagination'
+import {
+  NButton,
+  NDataTable,
+  NModal,
+  useMessage,
+  NSpace,
+  NTag,
+  NFormItem,
+  NInput,
+  NSelect
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import AddProtocol from './add-protocol'
+import { filterProtocolType } from '@/utils/filters'
+import {
+  schoolContractTemplateDetail,
+  schoolContractTemplatePage,
+  schoolContractTemplateUpdateStatus
+} from '../api'
+import SaveForm from '@/components/save-form'
+import { protocolTypeArray } from '@/utils/searchArray'
+
+export default defineComponent({
+  name: 'city-list',
+  setup() {
+    const message = useMessage()
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        keyword: '',
+        type: '',
+        status: ''
+      },
+      dataList: [] as any,
+      visiableProtocol: false,
+      protocolOperation: 'add',
+      protocolData: {},
+      contractTemplateContent: null,
+      visiableLook: false
+    })
+
+    const columns = () => {
+      return [
+        {
+          title: '协议编号',
+          key: 'id'
+        },
+        {
+          title: '协议名称',
+          key: 'name'
+        },
+        {
+          title: '协议号',
+          key: 'contractNo'
+        },
+        {
+          title: '协议类型',
+          key: 'type',
+          render(row: any) {
+            return filterProtocolType(row.type)
+          }
+        },
+        {
+          title: '上传时间',
+          key: 'createTime'
+        },
+        {
+          title: '操作人',
+          key: 'username'
+        },
+        {
+          title: '状态',
+          key: 'status',
+          render(row: any) {
+            return row.status ? <NTag type="primary">启用</NTag> : <NTag type="default">停用</NTag>
+          }
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  text
+                  size="small"
+                  //v-auth="schoolContractTemplate/detail1599978249005670402"
+                  onClick={() => lookDetails(row)}
+                >
+                  查看
+                </NButton>
+
+                {!row.status && (
+                  <NButton
+                    text
+                    type="primary"
+                    size="small"
+                    onClick={() => onUpdateState(row)}
+                    //v-auth="schoolContractTemplate/updateStatus1597901264288862209"
+                  >
+                    启用
+                  </NButton>
+                )}
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const onUpdateState = async (row: any) => {
+      try {
+        await schoolContractTemplateUpdateStatus({ id: row.id })
+        message.success('启用成功')
+        getList()
+      } catch {}
+    }
+
+    const lookDetails = async (row: any) => {
+      try {
+        const { data } = await schoolContractTemplateDetail({ id: row.id })
+        state.visiableLook = true
+        state.contractTemplateContent = data.contractTemplateContent
+      } catch {}
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await schoolContractTemplatePage({
+          ...state.pagination,
+          ...state.searchForm
+        })
+        state.dataList = data.rows || []
+        state.pagination.pageTotal = data.total || 0
+        state.loading = false
+      } catch {}
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <h2>协议管理</h2>
+
+        <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          onSubmit={onSubmit}
+          onSetModel={(val: any) => (state.searchForm = val)}
+        >
+          <NFormItem label="关键字" path="keyword">
+            <NInput
+              v-model:value={state.searchForm.keyword}
+              placeholder="请输入协议名称/协议号"
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="协议类型" path="type">
+            <NSelect options={protocolTypeArray} v-model:value={state.searchForm.type} clearable />
+          </NFormItem>
+          <NFormItem label="协议状态" path="status">
+            <NSelect
+              clearable
+              options={[
+                { label: '启用', value: 1 },
+                { label: '停用', value: 0 }
+              ]}
+              v-model:value={state.searchForm.status}
+            />
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="success"
+              //v-auth="schoolContractTemplate/save1597901145271291905"
+              onClick={() => {
+                state.visiableProtocol = true
+              }}
+            >
+              添加协议
+            </NButton>
+          </NSpace>
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+          ></Pagination>
+        </div>
+
+        <NModal
+          v-model:show={state.visiableProtocol}
+          preset="dialog"
+          showIcon={false}
+          title="新增协议"
+          style={{ width: '500px' }}
+        >
+          <AddProtocol
+            type={state.protocolOperation}
+            data={state.protocolData}
+            onClose={() => (state.visiableProtocol = false)}
+            onGetList={getList}
+          />
+        </NModal>
+
+        <NModal
+          v-model:show={state.visiableLook}
+          preset="dialog"
+          showIcon={false}
+          title="查看协议"
+          style={{ width: '400px' }}
+        >
+          <div
+            style="max-height: 450px; overflow-y: auto;"
+            v-html={state.contractTemplateContent}
+          ></div>
+          <NSpace justify="end" style="padding-top: 16px">
+            <NButton type="primary" onClick={() => (state.visiableLook = false)}>
+              确定
+            </NButton>
+          </NSpace>
+        </NModal>
+      </div>
+    )
+  }
+})

+ 251 - 0
src/views/system-manage/role-mange/index.tsx

@@ -0,0 +1,251 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  NButton,
+  NCascader,
+  NDataTable,
+  NFormItem,
+  NInput,
+  NModal,
+  NSpace,
+  NTag,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { sysApplicationPage, sysRolePage, sysRoleRemove } from '../api'
+import RoleOperation from './role-operation'
+import { formatDataList } from '@/utils/urlUtils'
+
+export default defineComponent({
+  name: 'subsidy-list',
+  setup() {
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        appId: null,
+        roleName: null
+      },
+      dataList: [] as any,
+      applyList: [] as any,
+      visiableRole: false,
+      roleOperation: 'add',
+      roleData: {} as any
+    })
+    const dialog = useDialog()
+    const message = useMessage()
+
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '应用名',
+          key: 'appName'
+        },
+        {
+          title: '角色名称',
+          key: 'roleName'
+        },
+        {
+          title: '角色状态',
+          key: 'enable',
+          render(row: any) {
+            return row.enable ? <NTag type="primary">开启</NTag> : <NTag type="default">关闭</NTag>
+          }
+        },
+        {
+          title: '角色描述',
+          key: 'roleDesc'
+        },
+        {
+          title: '创建时间',
+          key: 'createTime'
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="sysRole/update1597890274151223298"
+                  onClick={() => {
+                    state.visiableRole = true
+                    state.roleOperation = 'edit'
+                    state.roleData = row
+                  }}
+                >
+                  修改
+                </NButton>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  onClick={() => onRemove(row)}
+                  //v-auth="sysRole/remove1597890339439759362"
+                >
+                  删除
+                </NButton>
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const onRemove = (row: any): void => {
+      console.log(row, 'row')
+      dialog.warning({
+        title: '警告',
+        content: `删除"${row.roleName}",是否继续?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysRoleRemove({ id: row.id })
+            getList()
+            message.success('删除成功')
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await sysRolePage({ ...state.pagination, ...state.searchForm })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    const getApplyList = async (parentId = 0) => {
+      try {
+        const { data } = await sysApplicationPage({
+          page: 1,
+          rows: 999,
+          leafQuery: true,
+          parentId
+        })
+        state.applyList = formatDataList(data.rows || [], 'bizApps')
+      } catch {}
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    onMounted(() => {
+      getList()
+      getApplyList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <h2>角色管理</h2>
+
+        <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          onSubmit={onSubmit}
+          onSetModel={(val: any) => (state.searchForm = val)}
+        >
+          <NFormItem label="应用分类" path="appId">
+            <NCascader
+              v-model:value={state.searchForm.appId}
+              options={state.applyList}
+              showPath={true}
+              allowCheckingNotLoaded={false}
+              checkStrategy="child"
+              valueField="id"
+              labelField="appName"
+              childrenField="bizApps"
+              expandTrigger="hover"
+              placeholder="请选择应用分类"
+              clearable
+            ></NCascader>
+          </NFormItem>
+          <NFormItem label="角色名称" path="roleName">
+            <NInput v-model:value={state.searchForm.roleName} placeholder="请输入角色名称" />
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm>
+
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="sysRole/save1597890178735001601"
+              onClick={() => {
+                state.visiableRole = true
+                state.roleData = {}
+                state.roleOperation = 'add'
+              }}
+            >
+              添加角色
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+          ></Pagination>
+        </div>
+        <NModal
+          v-model:show={state.visiableRole}
+          preset="dialog"
+          showIcon={false}
+          title={state.roleOperation === 'add' ? '新增角色' : '修改角色'}
+          style={{ width: '600px' }}
+        >
+          <RoleOperation
+            type={state.roleOperation}
+            data={state.roleData}
+            applyList={state.applyList}
+            onClose={() => (state.visiableRole = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </div>
+    )
+  }
+})

+ 299 - 0
src/views/system-manage/role-mange/role-operation.tsx

@@ -0,0 +1,299 @@
+import { getMenus } from '@/api/system/menu'
+import { SearchOutline } from '@vicons/ionicons5'
+import {
+  NForm,
+  NFormItem,
+  NInput,
+  NSpace,
+  NButton,
+  useMessage,
+  NRadioGroup,
+  NRadio,
+  NCascader,
+  NTree,
+  NGrid,
+  NFormItemGi,
+  NIcon,
+  NCheckbox
+} from 'naive-ui'
+import { defineComponent, onMounted, PropType, reactive, ref, shallowRef } from 'vue'
+import { sysRoleSave, sysRoleUpdate, sysRoleDetail } from '../api'
+export default defineComponent({
+  name: 'role-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    applyList: {
+      type: Array as PropType<any>,
+      default: () => []
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      appId: null,
+      roleName: null,
+      menuId: [] as any,
+      enable: false,
+      roleDesc: null
+    })
+    const menuList = shallowRef<any[]>([])
+    const pattern = ref('')
+    const btnLoading = ref(false)
+    const isChecked = ref(false)
+    const idsList = ref<any[]>([]) // 所有结点
+    const indeterminate = ref(false)
+    const indeterminateKeys = ref<any[]>([])
+    const roleDetail = ref<any>({})
+
+    const formsRef = ref()
+    const treeRef = ref()
+    const message = useMessage()
+
+    const onSubmit = async () => {
+      const res = treeRef.value.getCheckedData()
+      const inRes = treeRef.value.getIndeterminateData()
+      const menuId = [...res.keys, ...inRes.keys]
+      // return
+      formsRef.value.validate(async (error: any) => {
+        if (error) return false
+        try {
+          const { ...res } = forms
+          const roleIds = [...new Set(menuId)]
+          btnLoading.value = true
+
+          if (props.type === 'add') {
+            await sysRoleSave({ ...res, menuId: roleIds })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await sysRoleUpdate({ ...res, menuId: roleIds, id: props.data.id })
+            message.success('修改成功')
+          }
+
+          emit('getList')
+          emit('close')
+        } catch {}
+        btnLoading.value = false
+      })
+    }
+
+    // 选中结点
+    const updateCheckedKeys = (options: any) => {
+      // 判断选中条件
+      if (options.length >= idsList.value.length) {
+        isChecked.value = true
+        indeterminate.value = false
+      } else if (options.length > 0) {
+        indeterminate.value = true
+        isChecked.value = false
+      } else {
+        indeterminate.value = false
+        isChecked.value = false
+      }
+      forms.menuId = options
+    }
+
+    // 由于手动添加选中的值,不会触发update事件,所以需要手动去设置按钮状态
+    const onCheckAll = (obj: boolean) => {
+      isChecked.value = obj
+      if (obj) {
+        forms.menuId = [...idsList.value]
+        isChecked.value = true
+      } else {
+        forms.menuId = []
+        isChecked.value = false
+      }
+      indeterminate.value = false
+      indeterminateKeys.value = []
+    }
+
+    const getMenuList = async () => {
+      try {
+        const menus = await getMenus({ delFlag: false })
+        menuList.value = menus.data
+        getMenuLength(menus.data)
+      } catch {}
+    }
+
+    // 获取菜单id数量
+    const getMenuLength = async (menu: any) => {
+      menu.map((m: any) => {
+        idsList.value.push(m.id)
+        if (m.children && m.children.length > 0) {
+          getMenuLength(m.children)
+        }
+      })
+    }
+
+    // 获取角色详情
+    const getRoleDetail = async () => {
+      try {
+        const data = props.data
+        const res = await sysRoleDetail({ id: data.id })
+        roleDetail.value = res.data || {}
+      } catch {
+        //
+      }
+    }
+
+    onMounted(async () => {
+      await getMenuList()
+      if (props.type === 'edit') {
+        await getRoleDetail()
+        const data = props.data
+        forms.appId = data.appId
+        forms.roleName = data.roleName
+        forms.enable = data.enable
+        forms.roleDesc = data.roleDesc
+        forms.menuId = roleDetail.value.menuLeafId
+        if (roleDetail.value.menuId.length >= idsList.value.length) {
+          isChecked.value = true
+          indeterminate.value = false
+        } else if (forms.menuId.length > 0) {
+          indeterminate.value = true
+          isChecked.value = false
+        }
+      }
+    })
+
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="100">
+          <NFormItem
+            label="应用分类"
+            path="appId"
+            rule={[
+              {
+                required: true,
+                message: '请选择应用分类'
+              }
+            ]}
+          >
+            <NCascader
+              v-model:value={forms.appId}
+              options={props.applyList}
+              showPath={true}
+              allowCheckingNotLoaded={false}
+              checkStrategy="child"
+              valueField="id"
+              labelField="appName"
+              childrenField="bizApps"
+              expandTrigger="hover"
+              placeholder="请选择应用分类"
+              clearable
+            ></NCascader>
+          </NFormItem>
+          <NGrid cols={2}>
+            <NFormItemGi
+              label="角色名称"
+              path="roleName"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入角色名称'
+                }
+              ]}
+            >
+              <NInput
+                v-model:value={forms.roleName}
+                placeholder="请输入角色名称"
+                clearable
+                maxlength={100}
+              ></NInput>
+            </NFormItemGi>
+            <NFormItemGi
+              label="角色状态"
+              path="enable"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择角色状态'
+                }
+              ]}
+            >
+              <NRadioGroup v-model:value={forms.enable}>
+                <NSpace>
+                  <NRadio value={true}>启用</NRadio>
+                  <NRadio value={false}>停用</NRadio>
+                </NSpace>
+              </NRadioGroup>
+            </NFormItemGi>
+          </NGrid>
+          <NFormItem label="备注" path="roleDesc">
+            <NInput
+              v-model:value={forms.roleDesc}
+              maxlength={180}
+              type="textarea"
+              rows={2}
+              placeholder="请输入备注"
+              clearable
+            />
+          </NFormItem>
+
+          <NFormItem label="权限" path="status">
+            <NSpace vertical style={{ width: '100%' }}>
+              <NInput
+                placeholder="输入菜单名搜索"
+                v-model:value={pattern.value}
+                clearable
+                v-slots={{
+                  suffix: () => (
+                    <NIcon>
+                      <SearchOutline />
+                    </NIcon>
+                  )
+                }}
+              />
+
+              <NCheckbox
+                indeterminate={indeterminate.value}
+                checked={isChecked.value}
+                onUpdateChecked={onCheckAll}
+              >
+                全选
+              </NCheckbox>
+
+              <NTree
+                style={{
+                  width: '100%',
+                  border: '1px solid #e0e0e6',
+                  height: '250px',
+                  borderRadius: '3px'
+                }}
+                ref={treeRef}
+                virtual-scroll
+                cascade
+                checkable
+                showIrrelevantNodes={false}
+                pattern={pattern.value}
+                data={menuList.value}
+                checkedKeys={forms.menuId}
+                // indeterminateKeys={indeterminateKeys.value}
+                onUpdate:checkedKeys={updateCheckedKeys}
+                // onUpdateIndeterminateKeys={updateIndeterminateKeys}
+                keyField="id"
+                labelField="name"
+                childrenField="children"
+              />
+            </NSpace>
+          </NFormItem>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton type="primary" onClick={() => onSubmit()} loading={btnLoading.value}>
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 174 - 0
src/views/system-manage/song-manage/index.tsx

@@ -0,0 +1,174 @@
+import Pagination from '@/components/pagination'
+import { NButton, NDataTable, NImage, NModal, NSpace, NTag, useDialog, useMessage } from 'naive-ui'
+import { defineComponent, onMounted, reactive } from 'vue'
+import { subjectPage, subjectUpdate } from '../api'
+import SongOperation from './song-operation'
+
+export default defineComponent({
+  name: 'subsidy-list',
+  setup() {
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        keyword: null,
+        status: null
+      },
+      dataList: [] as any,
+      visiableSong: false,
+      songOperation: 'add',
+      songData: {} as any
+    })
+    const dialog = useDialog()
+    const message = useMessage()
+
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '图片',
+          key: 'img',
+          render(row: any) {
+            return <NImage width={70} src={row.img} />
+          }
+        },
+        {
+          title: '乐器名称',
+          key: 'name'
+        },
+        {
+          title: '乐器编码',
+          key: 'code'
+        },
+        // {
+        //   title: '状态',
+        //   key: 'delFlag',
+        //   render(row: any) {
+        //     return !row.delFlag ? <NTag type="primary">启用</NTag> : <NTag type="error">停用</NTag>
+        //   }
+        // },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="subject/update1598205405598932994"
+                  onClick={() => {
+                    state.visiableSong = true
+                    state.songOperation = 'edit'
+                    state.songData = row
+                  }}
+                >
+                  修改
+                </NButton>
+
+                {/* <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  v-auth="subject/update1668958388100288514"
+                  onClick={() => onOperation(row)}
+                >
+                  {row.delFlag ? '启用' : '停用'}
+                </NButton> */}
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    // const onOperation = async (row: any) => {
+    //   const content = row.delFlag ? `您是否启用“${row.name}”?` : `您是否停用“${row.name}”?`
+    //   dialog.warning({
+    //     title: '提示',
+    //     content,
+    //     positiveText: '确定',
+    //     negativeText: '取消',
+    //     onPositiveClick: async () => {
+    //       try {
+    //         await subjectUpdate({ id: row.id, delFlag: !row.delFlag })
+    //         message.success(`${row.delFlag ? '启用' : '停用'}成功`)
+    //         getList()
+    //       } catch {}
+    //     }
+    //   })
+    // }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await subjectPage({ ...state.pagination, ...state.searchForm })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <h2>声部管理</h2>
+
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="subject/save1598205342327857154"
+              onClick={() => {
+                state.visiableSong = true
+                state.songOperation = 'add'
+                state.songData = {}
+              }}
+            >
+              添加声部
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+          ></Pagination>
+        </div>
+        <NModal
+          v-model:show={state.visiableSong}
+          preset="dialog"
+          showIcon={false}
+          title={state.songOperation === 'add' ? '新增声部' : '修改声部'}
+          style={{ width: '500px' }}
+        >
+          <SongOperation
+            type={state.songOperation}
+            data={state.songData}
+            onClose={() => (state.visiableSong = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </div>
+    )
+  }
+})

+ 122 - 0
src/views/system-manage/song-manage/song-operation.tsx

@@ -0,0 +1,122 @@
+import UploadFile from '@/components/upload-file'
+import deepClone from '@/utils/deep.clone'
+import { NForm, NInput, NSpace, NButton, useMessage, NFormItem } from 'naive-ui'
+import { defineComponent, onMounted, PropType, reactive, ref } from 'vue'
+import { subjectSave, subjectUpdate } from '../api'
+export default defineComponent({
+  name: 'role-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    applyList: {
+      type: Array as PropType<any>,
+      default: () => []
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      name: null,
+      code: null,
+      img: null,
+      parentId: 0
+    })
+    const btnLoading = ref(false)
+    const formsRef = ref()
+    const message = useMessage()
+
+    const onSubmit = async () => {
+      formsRef.value.validate(async (error: any) => {
+        if (error) return false
+        try {
+          btnLoading.value = true
+
+          if (props.type === 'add') {
+            await subjectSave({ ...forms })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await subjectUpdate({
+              ...forms,
+              id: props.data.id
+            })
+            message.success('修改成功')
+          }
+
+          emit('close')
+          emit('getList')
+        } catch {}
+        btnLoading.value = false
+      })
+    }
+
+    onMounted(async () => {
+      if (props.type === 'edit') {
+        const data = props.data
+        forms.img = data.img
+        forms.name = data.name
+        forms.code = data.code
+      }
+    })
+
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="auto">
+          <NFormItem
+            label="声部图片"
+            path="img"
+            rule={[
+              {
+                required: true,
+                message: '请输入声部图片'
+              }
+            ]}
+          >
+            <UploadFile
+              size={2}
+              cropper
+              v-model:fileList={forms.img}
+              bucketName="gyt"
+              path="basic/"
+              tips="图片大小2M以内"
+            />
+          </NFormItem>
+          <NFormItem
+            label="声部名称"
+            path="name"
+            rule={[
+              {
+                required: true,
+                message: '请输入声部名称'
+              }
+            ]}
+          >
+            <NInput
+              v-model:value={forms.name}
+              placeholder="请输入声部名称"
+              clearable
+              maxlength={100}
+            ></NInput>
+          </NFormItem>
+          <NFormItem label="声部编码" path="code">
+            <NInput v-model:value={forms.code} placeholder="请输入声部编码" clearable></NInput>
+          </NFormItem>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton type="primary" onClick={() => onSubmit()} loading={btnLoading.value}>
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 281 - 0
src/views/system-manage/song-setting/index.tsx

@@ -0,0 +1,281 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  NButton,
+  NDataTable,
+  NFormItem,
+  NInput,
+  NModal,
+  NSelect,
+  NSpace,
+  NTag,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { subjectBasicConfigPage, subjectBasicConfigUpdateConfigStatus } from '../api'
+import SongOperation from './song-operation'
+import numeral from 'numeral'
+
+export default defineComponent({
+  name: 'subsidy-list',
+  setup() {
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      searchForm: {
+        keyword: null,
+        status: null
+      },
+      dataList: [] as any,
+      visiableSong: false,
+      songOperation: 'add',
+      songData: {} as any
+    })
+    const dialog = useDialog()
+    const message = useMessage()
+
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '声部名称',
+          key: 'subjectName'
+        },
+        {
+          title: '招生人数限制',
+          key: 'studentEnrollmentUpperLimit',
+          render(row: any) {
+            return row.studentEnrollmentUpperLimit ? row.studentEnrollmentUpperLimit : '无限制'
+          }
+        },
+        {
+          title: '开团乐器',
+          key: 'instrumentsName'
+        },
+        {
+          title: '乐器团购价',
+          key: 'instrumentsGroupPrice',
+          render(row: any) {
+            return numeral(row.instrumentsGroupPrice).format('0,0.00') + '元'
+          }
+        },
+        {
+          title: '乐器原价',
+          key: 'instrumentsOriginalPrice',
+          render(row: any) {
+            return numeral(row.instrumentsOriginalPrice).format('0,0.00') + '元'
+          }
+        },
+        {
+          title: '教材',
+          key: 'textbookName'
+        },
+        {
+          title: '教材团购价',
+          key: 'textbookGroupPrice',
+          render(row: any) {
+            return numeral(row.textbookGroupPrice).format('0,0.00') + '元'
+          }
+        },
+        {
+          title: '教材原价',
+          key: 'textbookOriginalPrice',
+          render(row: any) {
+            return numeral(row.textbookOriginalPrice).format('0,0.00') + '元'
+          }
+        },
+        {
+          title: '更新人',
+          key: 'operatorName'
+        },
+        {
+          title: '更新时间',
+          key: 'updateTime'
+        },
+        {
+          title: '状态',
+          key: 'enableFlag',
+          render(row: any) {
+            return row.enableFlag ? (
+              <NTag type="primary">启用</NTag>
+            ) : (
+              <NTag type="error">停用</NTag>
+            )
+          }
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="subjectBasicConfig/update1598501736003452929"
+                  onClick={() => {
+                    state.visiableSong = true
+                    state.songOperation = 'edit'
+                    state.songData = row
+                  }}
+                >
+                  修改
+                </NButton>
+
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="subjectBasicConfig/updateConfigStatus1668958388100288514"
+                  onClick={() => onOperation(row)}
+                >
+                  {!row.enableFlag ? '启用' : '停用'}
+                </NButton>
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const onOperation = async (row: any) => {
+      const content = !row.enableFlag
+        ? `您是否启用“${row.subjectName}”?`
+        : `您是否停用“${row.subjectName}”?`
+      dialog.warning({
+        title: '提示',
+        content,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await subjectBasicConfigUpdateConfigStatus({
+              id: row.id,
+              enableFlag: row.enableFlag ? false : true
+            })
+            message.success(`${!row.enableFlag ? '启用' : '停用'}成功`)
+            getList()
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await subjectBasicConfigPage({ ...state.pagination, ...state.searchForm })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    const saveForm = ref()
+    const onSubmit = () => {
+      state.pagination.page = 1
+      getList()
+    }
+
+    const onSearch = () => {
+      saveForm.value?.submit()
+    }
+    const onBtnReset = () => {
+      saveForm.value?.reset()
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        <h2>声部参数</h2>
+        {/* <SaveForm
+          ref={saveForm}
+          model={state.searchForm}
+          onSubmit={onSubmit}
+          onSetModel={(val: any) => (state.searchForm = val)}
+        >
+          <NFormItem label="关键字" path="search">
+            <NInput
+              v-model:value={state.searchForm.keyword}
+              placeholder="员工编号/姓名/手机号"
+              clearable
+            />
+          </NFormItem>
+          <NFormItem label="状态" path="status">
+            <NSelect
+              v-model:value={state.searchForm.status}
+              placeholder="请选择状态"
+              options={employeeArray}
+              clearable
+            />
+          </NFormItem>
+          <NFormItem>
+            <NSpace>
+              <NButton type="primary" onClick={onSearch}>
+                搜索
+              </NButton>
+              <NButton type="default" onClick={onBtnReset}>
+                重置
+              </NButton>
+            </NSpace>
+          </NFormItem>
+        </SaveForm> */}
+
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="subjectBasicConfig/save1598501676989595649"
+              onClick={() => {
+                state.visiableSong = true
+                state.songOperation = 'add'
+                state.songData = {}
+              }}
+            >
+              添加声部参数
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            sync
+          ></Pagination>
+        </div>
+        <NModal
+          v-model:show={state.visiableSong}
+          preset="dialog"
+          showIcon={false}
+          title={state.songOperation === 'add' ? '新增声部参数' : '修改声部参数'}
+          style={{ width: '800px' }}
+        >
+          <SongOperation
+            type={state.songOperation}
+            data={state.songData}
+            onClose={() => (state.visiableSong = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </div>
+    )
+  }
+})

+ 389 - 0
src/views/system-manage/song-setting/song-operation.tsx

@@ -0,0 +1,389 @@
+import deepClone from '@/utils/deep.clone'
+import { NForm, NInput, NSpace, NButton, useMessage, NGrid, NFormItemGi, NSelect } from 'naive-ui'
+import { defineComponent, nextTick, onMounted, PropType, reactive, ref, shallowReactive } from 'vue'
+import {
+  productDetail,
+  productList,
+  subjectBasicConfigSave,
+  subjectBasicConfigUpdate,
+  subjectPage
+} from '../api'
+export default defineComponent({
+  name: 'role-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    applyList: {
+      type: Array as PropType<any>,
+      default: () => []
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      subjectId: null,
+      studentEnrollmentUpperLimit: null,
+      instrumentsId: null,
+      instrumentsSkuId: null,
+      instrumentsOriginalPrice: null,
+      instrumentsGroupPrice: null,
+      textbookId: null,
+      textbookSkuId: null,
+      textbookOriginalPrice: null,
+      textbookGroupPrice: null
+    })
+    const btnLoading = ref(false)
+    const formsRef = ref()
+    const message = useMessage()
+    const state = shallowReactive({
+      selectSubject: [],
+      selectProduct: [],
+      selectSkuProduct: [],
+      selectSkuTextbook: []
+    })
+
+    const onSubmit = async () => {
+      formsRef.value.validate(async (error: any) => {
+        if (error) return false
+        try {
+          btnLoading.value = true
+
+          if (props.type === 'add') {
+            await subjectBasicConfigSave({ ...forms })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await subjectBasicConfigUpdate({
+              ...forms,
+              id: props.data.id
+            })
+            message.success('修改成功')
+          }
+
+          emit('close')
+          emit('getList')
+        } catch {}
+        btnLoading.value = false
+      })
+    }
+
+    const formatParentId = (id: any, list: any, ids = [] as any) => {
+      for (const item of list) {
+        if (item.children && item.children.length > 0) {
+          const cIds: any = formatParentId(id, item.children, [...ids, item.code])
+          if (cIds.includes(id)) {
+            return cIds
+          }
+        }
+        if (item.code === id) {
+          return [...ids, id]
+        }
+      }
+      return ids
+    }
+
+    const getSubjectList = async () => {
+      try {
+        const { data } = await subjectPage({ page: 1, row: 999 })
+        state.selectSubject = data.rows || []
+      } catch {}
+    }
+
+    const getProductList = async () => {
+      try {
+        const { data } = await productList({ pageNum: 1, pageSize: 999 })
+        const tempList = data.list || []
+        tempList.forEach((item: any) => {
+          item.label = item.name
+          item.value = Number(item.id)
+        })
+        state.selectProduct = tempList
+      } catch {}
+    }
+
+    const getProductDetail = async (id: string | number, type = 'instrument') => {
+      try {
+        const { data } = await productDetail({ id })
+        if (type == 'instrument') {
+          state.selectSkuProduct = skuStockList(data)
+        } else {
+          state.selectSkuTextbook = skuStockList(data)
+        }
+      } catch {}
+    }
+
+    const skuStockList = (data: any) => {
+      // 处理规格
+      const skuStockList = data.skuStockList || []
+      skuStockList.forEach((item: any) => {
+        if (item.spData) {
+          const spData = JSON.parse(item.spData)
+          item.label = spData.reduce((spDataJson: any, value: any) => {
+            spDataJson += value.value
+            return spDataJson
+          }, '')
+        } else {
+          item.label = '默认'
+        }
+        item.value = Number(item.id)
+      })
+      return skuStockList
+    }
+
+    onMounted(async () => {
+      getSubjectList()
+      getProductList()
+      if (props.type === 'edit') {
+        const data = props.data
+        getProductDetail(data.instrumentsId)
+        getProductDetail(data.textbookId, 'textbook')
+        forms.subjectId = data.subjectId
+        forms.studentEnrollmentUpperLimit = data.studentEnrollmentUpperLimit
+        forms.instrumentsId = data.instrumentsId
+        forms.instrumentsSkuId = data.instrumentsSkuId
+        forms.instrumentsOriginalPrice = data.instrumentsOriginalPrice
+        forms.instrumentsGroupPrice = data.instrumentsGroupPrice
+        forms.textbookId = data.textbookId
+        forms.textbookSkuId = data.textbookSkuId
+        forms.textbookOriginalPrice = data.textbookOriginalPrice
+        forms.textbookGroupPrice = data.textbookGroupPrice
+      }
+    })
+
+    // 只能输入数字
+    const onlyAllowNumber = (value: string) => !value || /^\d+$/.test(value)
+
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="auto">
+          <NGrid cols={2} xGap="12">
+            <NFormItemGi
+              label="声部"
+              path="subjectId"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择声部'
+                }
+              ]}
+            >
+              <NSelect
+                v-model:value={forms.subjectId}
+                options={state.selectSubject}
+                valueField="id"
+                labelField="name"
+                placeholder="请选择声部"
+                clearable
+              />
+            </NFormItemGi>
+            <NFormItemGi
+              label="招生人数限制"
+              path="studentEnrollmentUpperLimit"
+              // rule={[
+              //   {
+              //     required: true,
+              //     message: '请输入招生人数限制'
+              //   }
+              // ]}
+            >
+              <NInput
+                v-model:value={forms.studentEnrollmentUpperLimit}
+                allowInput={onlyAllowNumber}
+                placeholder="请输入招生人数限制"
+                clearable
+                maxlength={8}
+              ></NInput>
+            </NFormItemGi>
+            <NFormItemGi
+              label="开团乐器"
+              path="instrumentsId"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择开团乐器'
+                }
+              ]}
+            >
+              <NSelect
+                v-model:value={forms.instrumentsId}
+                options={state.selectProduct}
+                placeholder="请选择开团乐器"
+                filterable
+                onUpdate:value={(value: any, options: any) => {
+                  forms.instrumentsSkuId = null
+                  forms.instrumentsGroupPrice = null
+                  // forms.instrumentsOriginalPrice = null
+                  // console.log(value, options)
+                  forms.instrumentsOriginalPrice = options.originalPrice || 0
+                  getProductDetail(value)
+                }}
+                clearable
+              />
+            </NFormItemGi>
+            <NFormItemGi
+              label="乐器规格"
+              path="instrumentsSkuId"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择乐器规格'
+                }
+              ]}
+            >
+              <NSelect
+                v-model:value={forms.instrumentsSkuId}
+                options={state.selectSkuProduct}
+                placeholder="请选择乐器规格"
+                onUpdate:value={(value: any, options: any) => {
+                  // console.log(value, options)
+                  // forms.instrumentsOriginalPrice = options.originalPrice || 0
+                }}
+                clearable
+              />
+            </NFormItemGi>
+            <NFormItemGi
+              label="乐器原价"
+              path="instrumentsOriginalPrice"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入乐器原价'
+                }
+              ]}
+            >
+              <NInput
+                v-model:value={forms.instrumentsOriginalPrice}
+                disabled
+                placeholder="请输入乐器原价"
+                clearable
+                maxlength={8}
+              ></NInput>
+            </NFormItemGi>
+            <NFormItemGi
+              label="乐器开团价"
+              path="instrumentsGroupPrice"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择输入开团价'
+                },
+                {
+                  pattern: /^(?:0.\d{0,3}|[0-9][0-9]{0,12}|[0-9]{1,10}.\d{0,3})$/,
+                  message: '输入乐器开团价有误',
+                  trigger: ['blur']
+                }
+              ]}
+            >
+              <NInput
+                v-model:value={forms.instrumentsGroupPrice}
+                placeholder="请输入乐器开团价"
+                clearable
+                maxlength={8}
+              ></NInput>
+            </NFormItemGi>
+            <NFormItemGi
+              label="教材"
+              path="textbookId"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择教材'
+                }
+              ]}
+            >
+              <NSelect
+                v-model:value={forms.textbookId}
+                options={state.selectProduct}
+                placeholder="请选择教材"
+                filterable
+                onUpdate:value={(value: any, options: any) => {
+                  forms.textbookSkuId = null
+                  forms.textbookGroupPrice = null
+                  // forms.textbookOriginalPrice = null
+                  forms.textbookOriginalPrice = options.originalPrice || 0
+                  getProductDetail(value, 'textbook')
+                }}
+                clearable
+              />
+            </NFormItemGi>
+            <NFormItemGi
+              label="教材规格"
+              path="textbookSkuId"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择教材规格'
+                }
+              ]}
+            >
+              <NSelect
+                v-model:value={forms.textbookSkuId}
+                options={state.selectSkuTextbook}
+                placeholder="请选择教材规格"
+                onUpdate:value={(value: any, options: any) => {
+                  // console.log(value, options)
+                  // forms.textbookOriginalPrice = options.originalPrice || 0
+                }}
+                clearable
+              />
+            </NFormItemGi>
+            <NFormItemGi
+              label="教材原价"
+              path="textbookOriginalPrice"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入教材原价'
+                }
+              ]}
+            >
+              <NInput
+                v-model:value={forms.textbookOriginalPrice}
+                disabled
+                placeholder="请输入教材原价"
+                clearable
+                maxlength={8}
+              ></NInput>
+            </NFormItemGi>
+            <NFormItemGi
+              label="教材团购价"
+              path="textbookGroupPrice"
+              rule={[
+                {
+                  required: true,
+                  message: '请输入教材团购价'
+                },
+                {
+                  pattern: /^(?:0.\d{0,3}|[0-9][0-9]{0,12}|[0-9]{1,10}.\d{0,3})$/,
+                  message: '输入乐器开团价有误',
+                  trigger: ['blur', 'input']
+                }
+              ]}
+            >
+              <NInput
+                v-model:value={forms.textbookGroupPrice}
+                maxlength={8}
+                placeholder="请输入教材团购价"
+                clearable
+              />
+            </NFormItemGi>
+          </NGrid>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton type="primary" onClick={() => onSubmit()} loading={btnLoading.value}>
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 169 - 0
src/views/system-manage/station-manage/index.tsx

@@ -0,0 +1,169 @@
+import SaveForm from '@/components/save-form'
+import Pagination from '@/components/pagination'
+import {
+  NButton,
+  NDataTable,
+  NFormItem,
+  NInput,
+  NModal,
+  NSpace,
+  useDialog,
+  useMessage
+} from 'naive-ui'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import { sysPositionPage, sysPositionRemove } from '../api'
+import { filterPosition } from '@/utils/filters'
+import StationOperation from './station-operation'
+
+export default defineComponent({
+  name: 'subsidy-list',
+  setup() {
+    const dialog = useDialog()
+    const message = useMessage()
+    const state = reactive({
+      loading: false,
+      pagination: {
+        page: 1,
+        rows: 10,
+        pageTotal: 0
+      },
+      dataList: [] as any,
+      visiableRole: false,
+      roleOperation: 'add',
+      roleData: {} as any
+    })
+
+    const columns = () => {
+      return [
+        {
+          title: '编号',
+          key: 'id'
+        },
+        {
+          title: '岗位名称',
+          key: 'roleName'
+        },
+        {
+          title: '岗位类型',
+          key: 'jobType',
+          render(row: any) {
+            return filterPosition(row.jobType)
+          }
+        },
+        {
+          title: '操作',
+          key: 'operation',
+          render(row: any) {
+            return (
+              <NSpace>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  //v-auth="sysPosition/update1597891012130623489"
+                  onClick={() => {
+                    state.visiableRole = true
+                    state.roleOperation = 'edit'
+                    state.roleData = row
+                  }}
+                >
+                  修改
+                </NButton>
+                <NButton
+                  type="primary"
+                  size="small"
+                  text
+                  onClick={() => onRmove(row)}
+                  //v-auth="sysPosition/remove1597891111267192834"
+                >
+                  删除
+                </NButton>
+              </NSpace>
+            )
+          }
+        }
+      ]
+    }
+
+    const onRmove = (row: any): void => {
+      console.log(row, 'row')
+      dialog.warning({
+        title: '警告',
+        content: `删除"${row.roleName}",是否继续?`,
+        positiveText: '确定',
+        negativeText: '取消',
+        onPositiveClick: async () => {
+          try {
+            await sysPositionRemove({ id: row.id })
+            getList()
+            message.success('删除成功')
+          } catch {}
+        }
+      })
+    }
+
+    const getList = async () => {
+      try {
+        state.loading = true
+        const { data } = await sysPositionPage({ ...state.pagination })
+        state.loading = false
+        state.pagination.pageTotal = Number(data.total)
+        state.dataList = data.rows || []
+      } catch {
+        state.loading = false
+      }
+    }
+
+    onMounted(() => {
+      getList()
+    })
+    return () => (
+      <div class="system-menu-container">
+        {/* <h2>岗位管理</h2> */}
+        <div class={['section-container']}>
+          <NSpace style={{ paddingBottom: '12px' }}>
+            <NButton
+              type="primary"
+              //v-auth="sysPosition/save1597890897835839489"
+              onClick={() => {
+                state.visiableRole = true
+                state.roleOperation = 'add'
+                state.roleData = {}
+              }}
+            >
+              添加岗位
+            </NButton>
+          </NSpace>
+
+          <NDataTable
+            loading={state.loading}
+            columns={columns()}
+            data={state.dataList}
+          ></NDataTable>
+          <Pagination
+            v-model:page={state.pagination.page}
+            v-model:pageSize={state.pagination.rows}
+            v-model:pageTotal={state.pagination.pageTotal}
+            onList={getList}
+            saveKey="StationManage"
+            sync
+          ></Pagination>
+        </div>
+        <NModal
+          v-model:show={state.visiableRole}
+          preset="dialog"
+          showIcon={false}
+          title={state.roleOperation === 'add' ? '新增岗位' : '修改岗位'}
+          style={{ width: '500px' }}
+        >
+          <StationOperation
+            type={state.roleOperation}
+            data={state.roleData}
+            onClose={() => (state.visiableRole = false)}
+            onGetList={getList}
+          />
+        </NModal>
+      </div>
+    )
+  }
+})

+ 113 - 0
src/views/system-manage/station-manage/station-operation.tsx

@@ -0,0 +1,113 @@
+import { positionArray } from '@/utils/searchArray'
+import { NForm, NFormItem, NInput, NSelect, NSpace, NButton, useMessage } from 'naive-ui'
+import { defineComponent, onMounted, PropType, reactive, ref } from 'vue'
+import { sysPositionSave, sysPositionUpdate } from '../api'
+export default defineComponent({
+  name: 'role-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      roleName: null,
+      jobType: null
+    })
+
+    const applyList = ref<any[]>([])
+    const btnLoading = ref(false)
+
+    const formsRef = ref()
+    const message = useMessage()
+
+    const onSubmit = async () => {
+      formsRef.value.validate(async (error: any) => {
+        if (error) return false
+        try {
+          btnLoading.value = true
+
+          if (props.type === 'add') {
+            await sysPositionSave({
+              ...forms
+            })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await sysPositionUpdate({ ...forms, id: props.data.id })
+            message.success('修改成功')
+          }
+
+          emit('close')
+          emit('getList')
+        } catch {}
+        btnLoading.value = false
+      })
+    }
+
+    onMounted(async () => {
+      if (props.type === 'edit') {
+        const data = props.data
+        forms.roleName = data.roleName
+        forms.jobType = data.jobType
+      }
+    })
+
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="auto">
+          <NFormItem
+            label="岗位名称"
+            path="roleName"
+            rule={[
+              {
+                required: true,
+                message: '请输入岗位名称'
+              }
+            ]}
+          >
+            <NInput
+              v-model:value={forms.roleName}
+              placeholder="请输入岗位名称"
+              clearable
+              maxlength={100}
+            ></NInput>
+          </NFormItem>
+          <NFormItem
+            label="岗位类型"
+            path="jobType"
+            rule={[
+              {
+                required: true,
+                message: '岗位类型'
+              }
+            ]}
+          >
+            <NSelect
+              v-model:value={forms.jobType}
+              placeholder="请选择岗位类型"
+              options={positionArray}
+              clearable
+              options-line
+              style={{ width: '100%' }}
+            />
+          </NFormItem>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton type="primary" onClick={() => onSubmit()} loading={btnLoading.value}>
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

+ 263 - 0
src/views/system-manage/system-apply/apply-operation.tsx

@@ -0,0 +1,263 @@
+import {
+  NForm,
+  NFormItem,
+  NInput,
+  NSelect,
+  NSpace,
+  NButton,
+  NTreeSelect,
+  useMessage,
+  NRadioGroup,
+  NRadio
+} from 'naive-ui'
+import type { TreeSelectOption } from 'naive-ui'
+import { defineComponent, onMounted, PropType, reactive, ref } from 'vue'
+import { sysApplicationPage, sysApplicationSave, sysApplicationUpdate } from '../api'
+export default defineComponent({
+  name: 'city-operation',
+  props: {
+    type: {
+      type: String,
+      default: 'add'
+    },
+    parentId: {
+      type: Number,
+      default: 0
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => {}
+    }
+  },
+  emits: ['close', 'getList'],
+  setup(props, { slots, attrs, emit }) {
+    const forms = reactive({
+      applyType: props.parentId == 0 ? 0 : 1, // 顶级分类,子级分类
+      parentId: props.parentId,
+      appName: null,
+      clientIds: null,
+      permissionFlag: true,
+      remark: null
+    })
+
+    const applyList = ref<any[]>([])
+    const btnLoading = ref(false)
+
+    const formsRef = ref()
+    const subsidyList = ref<any>([])
+    const message = useMessage()
+
+    const handleLoad = async (option: TreeSelectOption) => {
+      try {
+        console.log(option)
+        const { data } = await sysApplicationPage({
+          page: 1,
+          rows: 999,
+          parentId: option.key
+        })
+        const tempList = [] as any
+        ;(data.rows || []).forEach((item: any) => {
+          tempList.push({
+            key: item.id,
+            label: item.appName,
+            isLeaf: item.number > 0 ? false : true
+          })
+        })
+        option.children = [...tempList]
+      } catch {}
+    }
+
+    const getApplyList = async (parentId = 0) => {
+      try {
+        const { data } = await sysApplicationPage({
+          page: 1,
+          rows: 999,
+          parentId
+        })
+        const tempList = [] as any
+        ;(data.rows || []).forEach((item: any) => {
+          tempList.push({
+            key: item.id,
+            label: item.appName,
+            isLeaf: item.number > 0 ? false : true
+          })
+        })
+        return tempList
+      } catch {}
+    }
+
+    const onSubmit = async () => {
+      formsRef.value.validate(async (error: any) => {
+        if (error) return false
+        try {
+          btnLoading.value = true
+
+          if (props.type === 'add') {
+            await sysApplicationSave({
+              appName: forms.appName,
+              clientIds: forms.clientIds,
+              parentId: forms.parentId,
+              permissionFlag: forms.permissionFlag,
+              remark: forms.remark
+            })
+            message.success('添加成功')
+          } else if (props.type === 'edit') {
+            await sysApplicationUpdate({ ...forms, id: props.data.id })
+            message.success('修改成功')
+          }
+
+          btnLoading.value = false
+          emit('close')
+          emit('getList')
+        } catch {}
+      })
+    }
+
+    onMounted(async () => {
+      applyList.value = await getApplyList()
+
+      console.log(props.parentId)
+      if (props.type === 'edit') {
+        const data = props.data
+        const tempList = await getApplyList(props.parentId)
+        applyList.value.forEach((apply: any) => {
+          if (apply.key == props.parentId) {
+            apply.children = [...tempList]
+          }
+        })
+
+        forms.appName = data.appName
+        forms.clientIds = data.clientIds
+        forms.permissionFlag = data.permissionFlag
+        forms.remark = data.remark
+      }
+    })
+
+    return () => (
+      <div style="background: #fff; padding-top: 12px">
+        <NForm model={forms} ref={formsRef} label-placement="left" label-width="100">
+          {props.type === 'add' && (
+            <NFormItem
+              label="应用类型"
+              path="applyType"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择应用类型'
+                }
+              ]}
+            >
+              <NRadioGroup v-model:value={forms.applyType}>
+                <NSpace>
+                  <NRadio value={0}>顶级应用</NRadio>
+                  <NRadio value={1}>子级应用</NRadio>
+                </NSpace>
+              </NRadioGroup>
+            </NFormItem>
+          )}
+          {forms.applyType > 0 && props.type === 'add' && (
+            <NFormItem
+              label="应用分类"
+              path="parentId"
+              rule={[
+                {
+                  required: true,
+                  message: '请选择应用分类'
+                }
+              ]}
+            >
+              <NTreeSelect
+                v-model:value={forms.parentId}
+                options={applyList.value}
+                showPath={true}
+                allowCheckingNotLoaded={false}
+                // checkStrategy={'child'}
+                onLoad={handleLoad}
+                placeholder="请选择应用分类"
+                clearable
+              ></NTreeSelect>
+            </NFormItem>
+          )}
+          <NFormItem
+            label="应用名称"
+            path="appName"
+            rule={[
+              {
+                required: true,
+                message: '请输入应用名称'
+              }
+            ]}
+          >
+            <NInput
+              v-model:value={forms.appName}
+              placeholder="请输入应用名称"
+              clearable
+              maxlength={100}
+            ></NInput>
+          </NFormItem>
+          <NFormItem
+            label="权限客户端"
+            path="clientIds"
+            rule={[
+              {
+                required: true,
+                message: '请输入权限客户端'
+              }
+            ]}
+          >
+            <NInput
+              v-model:value={forms.clientIds}
+              maxlength={200}
+              placeholder="请输入权限客户端"
+              clearable
+            ></NInput>
+          </NFormItem>
+          <NFormItem
+            label="权限认证"
+            path="permissionFlag"
+            rule={[
+              {
+                required: true,
+                message: '请选择权限认证'
+              }
+            ]}
+          >
+            <NRadioGroup v-model:value={forms.permissionFlag}>
+              <NSpace>
+                <NRadio value={true}>是</NRadio>
+                <NRadio value={false}>否</NRadio>
+              </NSpace>
+            </NRadioGroup>
+          </NFormItem>
+          <NFormItem
+            label="备注"
+            path="remark"
+            rule={[
+              {
+                required: true,
+                message: '备注'
+              }
+            ]}
+          >
+            <NInput
+              v-model:value={forms.remark}
+              maxlength={180}
+              type="textarea"
+              rows={3}
+              placeholder="请输入备注"
+            />
+          </NFormItem>
+        </NForm>
+
+        <NSpace justify="end">
+          <NButton type="default" onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton type="primary" onClick={() => onSubmit()} loading={btnLoading.value}>
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    )
+  }
+})

部分文件因文件數量過多而無法顯示