Browse Source

提交前端

lusa 3 weeks ago
parent
commit
de7ae0dc5a

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

@@ -28,6 +28,27 @@ export function effectDetailList(params: Record<string, any>) {
   return request({ method: 'get', url: '/effect/detail/list', params })
 }
 
+export function effectContentList(params: Record<string, any>) {
+  return request({ method: 'get', url: '/effect/content/list', params })
+}
+
+export function effectContentGet(id: string | number) {
+  return request({ method: 'get', url: '/effect/content/get', params: { id } })
+}
+
+
+export function effectContentSave(data: Record<string, any>) {
+  return request({ method: 'post', url: '/effect/content/save', data })
+}
+
+export function effectTemplateGet(id: string | number) {
+  return request({ method: 'get', url: '/effect/template/get', params: { id } })
+}
+
+export function effectTemplateSave(data: Record<string, any>) {
+  return request({ method: 'post', url: '/effect/template/save', data })
+}
+
 export function effectIndexList(params: Record<string, any>) {
   return request({ method: 'get', url: '/effect/index/list', params })
 }
@@ -55,3 +76,4 @@ export function effectObjectionList(params: Record<string, any>) {
 export function effectWhitelistList(params: Record<string, any>) {
   return request({ method: 'get', url: '/effect/whitelist/list', params })
 }
+

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

@@ -20,10 +20,78 @@ export function summaryShareList(params: Record<string, any>) {
   return request({ method: 'get', url: '/summary/share/list', params })
 }
 
+export function summaryShareAdd(data: Record<string, any>) {
+  return request({ method: 'post', url: '/summaryShare/add', data })
+}
+
+export function summaryShareTop(data: Record<string, any>) {
+  return request({ method: 'post', url: '/summaryShare/top', data })
+}
+
+export function summaryShareUnTop(data: Record<string, any>) {
+  return request({ method: 'post', url: '/summaryShare/unTop', data })
+}
+
+export function summaryShareDelete(data: Record<string, any>) {
+  return request({ method: 'delete', url: '/summaryShare/share-delete', params: data })
+}
+
 export function summaryAnnouncementList(params: Record<string, any>) {
   return request({ method: 'get', url: '/summary/announcement/list', params })
 }
 
+export function summaryBulletinAdd(data: Record<string, any>) {
+  return request({ method: 'post', url: '/bulletin/add', data })
+}
+
+export function summaryBulletinEdit(data: Record<string, any>) {
+  return request({ method: 'put', url: '/bulletin/edit', data })
+}
+
+export function summaryBulletinDelete(id: string | number) {
+  return request({ method: 'delete', url: `/bulletin/delete?id=${id}` })
+}
+
+export function summarySettingList(params: Record<string, any> = {}) {
+  return request({ method: 'get', url: '/summarySetting/list', params })
+}
+
+export function summarySettingEdit(data: Record<string, any>) {
+  return request({ method: 'put', url: '/summarySetting/edit', data })
+}
+
+export function summarySettingDelete(data: Record<string, any>) {
+  return request({ method: 'delete', url: '/summarySetting/delete', data })
+}
+
+export function summaryRemindTaskList(data: Record<string, any>) {
+  return request({ method: 'post', url: '/dispatch/task/list', data })
+}
+
+export function summaryRemindTaskAdd(data: Record<string, any>) {
+  return request({ method: 'post', url: '/dispatch/task/add', data })
+}
+
+export function summaryRemindTaskEdit(data: Record<string, any>) {
+  return request({ method: 'post', url: '/dispatch/task/edit', data })
+}
+
+export function summaryRemindTaskDelete(data: Record<string, any>) {
+  return request({ method: 'post', url: '/dispatch/task/delete', data })
+}
+
+export function summaryRemindTaskPause(data: Record<string, any>) {
+  return request({ method: 'post', url: '/dispatch/task/pause', data })
+}
+
+export function summaryRemindTaskResume(data: Record<string, any>) {
+  return request({ method: 'post', url: '/dispatch/task/resume', data })
+}
+
+export function summaryRemindTaskTrigger(data: Record<string, any>) {
+  return request({ method: 'post', url: '/dispatch/task/trigger', data })
+}
+
 export function summaryGet(id: string | number) {
   return request({ method: 'get', url: '/summary/get', params: { id } })
 }
@@ -35,3 +103,5 @@ export function summaryAdd(data: Record<string, any>) {
 export function summaryEdit(data: Record<string, any>) {
   return request({ method: 'put', url: '/summary/edit', data })
 }
+
+

+ 69 - 9
ui/sp-user-center/src/api/portalMenu.ts

@@ -27,14 +27,15 @@ export const fetchPortalMenus = () => {
 }
 
 /**
- * 接口未就绪时的本地 mock,方便先跑通导航壳
+ * 接口未就绪时的本地 mock,优先对齐旧版菜单结构与当前已挂路由
  */
 export const getPortalMenusMock = (): PortalModuleMenu[] => ([
   {
     module: ModuleKey.HOME,
     commonMenus: [
       { id: 'home-dashboard', title: '首页', path: '/home/dashboard', icon: 'dashboard' },
-      { id: 'home-otr-role', title: '角色管理', path: '/otr/system/role', icon: 'peoples' },
+      { id: 'home-otr-task', title: '任务管理', path: '/otr/task/list', icon: 'edit' },
+      { id: 'home-otr-summary', title: '我的简报', path: '/otr/summary/list', icon: 'documentation' },
       { id: 'home-sales-index', title: '销售首页', path: '/index', icon: 'chart' },
     ],
     moduleMenus: [],
@@ -44,14 +45,72 @@ export const getPortalMenusMock = (): PortalModuleMenu[] => ([
     commonMenus: [],
     moduleMenus: [
       {
-        id: 'otr-system',
-        title: '系统管理',
-        path: '/otr/system',
-        icon: 'system',
+        id: 'otr-target',
+        title: '目标',
+        path: '/otr/target',
+        icon: 'guide',
         children: [
-          { id: 'otr-system-user', title: '员工管理', path: '/otr/system/user', icon: 'user' },
-          { id: 'otr-system-role', title: '角色管理', path: '/otr/system/role', icon: 'peoples' },
-          { id: 'otr-system-menu', title: '菜单管理', path: '/otr/system/menu', icon: 'tree-table' },
+          { id: 'otr-target-company', title: '公司目标', path: '/otr/target/company', icon: 'office-building' },
+          { id: 'otr-target-depart', title: '部门目标', path: '/otr/target/depart', icon: 'files' },
+          { id: 'otr-target-mine', title: '我的目标', path: '/otr/target/mine', icon: 'user' },
+          { id: 'otr-task-list', title: '任务管理', path: '/otr/task/list', icon: 'edit' },
+          { id: 'otr-award-list', title: '奖励审批', path: '/otr/award/list', icon: 'star' },
+          { id: 'otr-target-report', title: '目标报告', path: '/otr/target/report', icon: 'data-analysis' },
+        ],
+      },
+      {
+        id: 'otr-summary',
+        title: '简报',
+        path: '/otr/summary',
+        icon: 'documentation',
+        children: [
+          { id: 'otr-summary-list', title: '我的简报', path: '/otr/summary/list', icon: 'document' },
+          { id: 'otr-summary-member', title: '成员简报', path: '/otr/summary/member', icon: 'peoples' },
+          { id: 'otr-summary-create', title: '新建简报', path: '/otr/summary/create', icon: 'edit-pen' },
+          { id: 'otr-summary-share', title: '心得分享', path: '/otr/summary/share', icon: 'chat-dot-round' },
+          { id: 'otr-summary-draft', title: '草稿箱', path: '/otr/summary/draft', icon: 'tickets' },
+          { id: 'otr-summary-settings', title: '简报设置', path: '/otr/summary/settings', icon: 'setting' },
+          { id: 'otr-summary-remind-task', title: '简报提醒', path: '/otr/summary/remind-task', icon: 'bell' },
+        ],
+      },
+      {
+        id: 'otr-effect',
+        title: '绩效',
+        path: '/otr/effect',
+        icon: 'chart',
+        children: [
+          { id: 'otr-effect-mine', title: '我的绩效', path: '/otr/effect/mine', icon: 'user' },
+          { id: 'otr-effect-backlog', title: '我的待办', path: '/otr/effect/backlog', icon: 'list' },
+          { id: 'otr-effect-manage', title: '绩效管理', path: '/otr/effect/manage', icon: 'histogram' },
+          { id: 'otr-effect-setting', title: '绩效设置', path: '/otr/effect/setting', icon: 'setting' },
+        ],
+      },
+      {
+        id: 'otr-notice',
+        title: '通知',
+        path: '/otr/notice',
+        icon: 'message',
+        children: [
+          { id: 'otr-notice-list', title: '内部公告', path: '/otr/summary/announcement', icon: 'notification' },
+          { id: 'otr-notice-system', title: '系统通知', path: '/otr/notice/list', icon: 'message' },
+        ],
+      },
+      {
+        id: 'otr-report',
+        title: '报告',
+        path: '/otr/report',
+        icon: 'data-analysis',
+        children: [
+          { id: 'otr-report-person', title: '人员报告', path: '/otr/report/person', icon: 'data-line' },
+        ],
+      },
+      {
+        id: 'otr-ai',
+        title: 'AI工具',
+        path: '/otr/ai',
+        icon: 'magic-stick',
+        children: [
+          { id: 'otr-ai-add', title: 'AI 工具', path: '/otr/ai/add', icon: 'magic-stick' },
         ],
       },
     ],
@@ -70,3 +129,4 @@ export const getPortalMenusMock = (): PortalModuleMenu[] => ([
   { module: ModuleKey.WEBSITE, commonMenus: [], moduleMenus: [] },
 ])
 
+

+ 189 - 5
ui/sp-user-center/src/mock/httpMock.ts

@@ -460,6 +460,70 @@ export const buildMockByRequest = (url = '', method = 'get', data?: any): MockRe
       total: 1,
     })
   }
+  if (url.includes('/effect/content/list')) {
+    return ok({
+      records: [
+        { id: 1, contentMainId: 1, name: '销售季度考核内容', score: 100, description: '围绕目标达成、协同执行与结果复盘进行考核。' },
+        { id: 2, contentMainId: 2, name: '运营月度考核内容', score: 100, description: '围绕交付效率、质量与流程协同进行考核。' },
+      ],
+      total: 2,
+    })
+  }
+  if (url.includes('/effect/content/get')) {
+    return ok({
+      id: 1,
+      name: '销售季度考核内容',
+      score: 100,
+      description: '围绕目标达成、协同执行与结果复盘进行考核。',
+      dimensions: [
+
+        {
+          id: 1,
+          dimensionName: '业绩达成',
+          type: '定量指标',
+          items: [
+            { id: 11, name: '销售额完成率', standard: '按月度签约额完成率评分', weight: 40, scoreLimit: 40, thresholdValue: 80, targetValue: 100, challengeValue: 120, remark: '' },
+            { id: 12, name: '回款达成率', standard: '按回款指标完成率评分', weight: 20, scoreLimit: 20, thresholdValue: 85, targetValue: 100, challengeValue: 110, remark: '' },
+          ],
+        },
+        {
+          id: 2,
+          dimensionName: '团队协同',
+          type: '定性指标',
+          items: [
+            { id: 21, name: '跨部门协作', standard: '关注响应速度与协作结果', weight: 20, scoreLimit: 20, thresholdValue: '', targetValue: '', challengeValue: '', remark: '' },
+            { id: 22, name: '过程管理', standard: '检查计划、复盘和执行闭环', weight: 20, scoreLimit: 20, thresholdValue: '', targetValue: '', challengeValue: '', remark: '' },
+          ],
+        },
+      ],
+    })
+  }
+  if (url.includes('/effect/content/save')) {
+    return ok({ id: 1, contentMainId: 1 }, 'mock content saved')
+  }
+  if (url.includes('/effect/template/get')) {
+    return ok({
+      id: 1,
+      name: '销售经理季度模板',
+      cycleType: '季度',
+      isOpenScore: 1,
+      isOpenRemark: 1,
+      contentId: 1,
+      contentName: '销售季度考核内容',
+      isContentEdit: 1,
+      isResultEntry: 1,
+      confirmerUser: 0,
+      resultEntryUser: 0,
+      appraisers: [
+        { id: 1, roleType: '直属上级', userId: '', weight: 60, isScoreVisibility: 1, isRemarkVisibility: 1 },
+        { id: 2, roleType: '考核对象本人', userId: 0, weight: 40, isScoreVisibility: 0, isRemarkVisibility: 0 },
+      ],
+    })
+  }
+  if (url.includes('/effect/template/save')) {
+    return ok({ id: 1 }, 'mock template saved')
+  }
+
   if (url.includes('/task/list')) {
     return ok({
       records: [{ id: 1, name: '完成周目标复盘', ownerName: '赵六', priority: '高', deadline: '2026-05-10 17:30:00', statusLabel: '进行中' }],
@@ -585,6 +649,107 @@ export const buildMockByRequest = (url = '', method = 'get', data?: any): MockRe
     })
   }
 
+  /** OTR 目标 - 公司/部门列表 */
+  if (url.includes('/lingcun/subCompany/list')) {
+    return ok([
+      { id: '1', name: '领存科技总部' },
+      { id: '2', name: '领存科技上海分公司' },
+      { id: '3', name: '领存科技深圳分公司' },
+    ])
+  }
+
+  if (url.includes('/lingcun/dept/queryDeptBySub')) {
+    return ok([
+      { id: '101', name: '销售部' },
+      { id: '102', name: '市场部' },
+      { id: '103', name: '运营中心' },
+      { id: '104', name: '产品部' },
+      { id: '105', name: '技术部' },
+    ])
+  }
+
+  /** OTR 目标 - 详情/进度 */
+  if (url.includes('/target/get')) {
+    return ok({
+      id: 1,
+      title: 'Q2 销售目标',
+      leaderName: '张三',
+      percent: 65,
+      status: 2,
+      type: 1,
+      remark: 2,
+      startTime: '2026-04-01',
+      endTime: '2026-06-30',
+      content: '完成季度销售指标',
+      percentType: 1,
+      isEditable: true,
+    })
+  }
+
+  if (url.includes('/target/auto-percent-value')) {
+    return ok({
+      linkPercent: 0,
+      taskPercent: 60,
+      krPercent: 70,
+    })
+  }
+
+  if (url.includes('/target/edit-percent-type')) {
+    return ok({ msg: '进度更新成功' })
+  }
+
+  if (url.includes('/target/delete')) {
+    return ok(true, '删除成功')
+  }
+
+  /** OTR 目标模板 */
+  if (url.includes('/target/template/category/list')) {
+    return ok({
+      records: [
+        { categoryId: '1', name: '销售类', sort: 1 },
+        { categoryId: '2', name: '运营类', sort: 2 },
+        { categoryId: '3', name: '产品类', sort: 3 },
+      ],
+      total: 3,
+    })
+  }
+
+  if (url.includes('/target/template/post/list')) {
+    return ok([
+      { propertyId: '1', name: '销售经理' },
+      { propertyId: '2', name: '销售专员' },
+      { propertyId: '3', name: '销售主管' },
+    ])
+  }
+
+  if (url.includes('/target/template/list')) {
+    return ok([
+      {
+        templateDataId: '1',
+        categoryId: '1',
+        propertyId: '1',
+        propertyName: '销售经理',
+        title: 'Q2销售目标模板',
+        content: '完成季度销售指标,提升团队业绩',
+        krs: [
+          { title: '销售额达成率', weightPercent: 40 },
+          { title: '客户满意度', weightPercent: 30 },
+          { title: '团队管理', weightPercent: 30 },
+        ],
+        isEditable: true,
+        isDeletable: true,
+      },
+    ])
+  }
+
+  if (url.includes('/target/template/save')) {
+    return ok({ templateDataId: '1' }, '保存成功')
+  }
+
+  if (url.includes('/target/template/delete')) {
+    return ok(true, '删除成功')
+  }
+
   if (url.includes('/task/member/list')) {
     return ok({ records: [{ memberName: '张三', taskName: '客户跟进复盘', deadline: '2026-05-12 18:00:00', statusLabel: '进行中' }], total: 1 })
   }
@@ -604,15 +769,34 @@ export const buildMockByRequest = (url = '', method = 'get', data?: any): MockRe
   if (url.includes('/summary/statistics/list')) {
     return ok({ records: [{ dimension: '周报提交率', value: 92, ratio: '92%' }], total: 1 })
   }
-  if (url.includes('/summary/draft/list')) {
-    return ok({ records: [{ title: '周报草稿', updatedAt: '2026-05-07 09:18:00', ownerName: '孙八' }], total: 1 })
-  }
   if (url.includes('/summary/share/list')) {
-    return ok({ records: [{ title: '客户洞察方法论', author: '周九', likes: 12, createdAt: '2026-05-01 09:00:00' }], total: 1 })
+    return ok({ records: [{ summaryShareId: 1, staffName: '周九', staffPhoto: '', shareContent: '<p>本周重点客户跟进与需求澄清。</p>', createTime: '2026-05-01 09:00:00', likeCount: 2, isLike: 0, isTop: 1, canUpdate: 1, canDelete: 1, canChoose: 1, userList: [{ id: 1, userName: '张三' }, { id: 2, userName: '李四' }], commits: [] }], total: 1 })
+  }
+  if (url.includes('/summaryShare/add')) {
+    return ok(true, 'mock share saved')
+  }
+  if (url.includes('/summaryShare/top')) {
+    return ok(true, 'mock top success')
+  }
+  if (url.includes('/summaryShare/unTop')) {
+    return ok(true, 'mock untop success')
+  }
+  if (url.includes('/summaryShare/share-delete')) {
+    return ok(true, 'mock delete success')
   }
   if (url.includes('/summary/announcement/list')) {
-    return ok({ records: [{ title: '周报提交通知', publisher: '系统助手', createdAt: '2026-05-07 09:00:00' }], total: 1 })
+    return ok({ records: [{ bulletinId: 1, title: '周报提交通知', staffName: '系统助手', staffPhoto: '', content: '<p>请按时提交本周简报。</p>', createTime: '2026-05-07 09:00:00', deadlineDate: '2026-05-10', bulletinType: 1, canUpdate: true, canDelete: true, readVOs: [{ staffPhoto: '', hasRead: true }, { staffPhoto: '', hasRead: false }], commits: [] }], total: 1 })
+  }
+  if (url.includes('/bulletin/add')) {
+    return ok(true, 'mock bulletin saved')
   }
+  if (url.includes('/bulletin/edit')) {
+    return ok(true, 'mock bulletin updated')
+  }
+  if (url.includes('/bulletin/delete')) {
+    return ok(true, 'mock bulletin deleted')
+  }
+
 
   if (url.includes('/award/pool/list')) {
     return ok({ records: [{ poolName: '季度激励池', amount: 50000, updatedAt: '2026-05-07 08:00:00' }], total: 1 })

+ 499 - 2
ui/sp-user-center/src/modules/otr/effect/views/ContentForm.vue

@@ -1,7 +1,504 @@
 <template>
-  <otr-simple-form-scene title="新建考核内容" />
+  <div class="frame-body adaption-frame-body effect-content-form-page">
+    <div class="page-card">
+      <div class="page-header">
+        <div>
+          <div class="page-title">考核内容</div>
+          <div class="page-subtitle">保留旧版的核心结构:内容基础信息、维度、指标明细和加减分规则。</div>
+        </div>
+        <div class="page-actions">
+          <el-button @click="goBack">返回</el-button>
+          <el-button type="primary" :loading="submitting" @click="submit">保存考核内容</el-button>
+        </div>
+      </div>
+
+      <div class="page-content">
+        <div class="main-panel">
+          <div class="panel">
+            <div class="panel-title">基础信息</div>
+            <el-form ref="formRef" :model="form" :rules="rules" label-position="top">
+              <el-form-item label="内容名称" prop="name">
+                <el-input v-model="form.name" maxlength="120" show-word-limit placeholder="请输入考核内容名称" />
+              </el-form-item>
+              <el-form-item label="评分上限">
+                <el-input-number v-model="form.score" :min="1" :max="200" />
+              </el-form-item>
+              <el-form-item label="内容说明">
+                <el-input v-model="form.description" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请输入考核内容说明" />
+              </el-form-item>
+            </el-form>
+          </div>
+
+          <div class="panel">
+            <div class="panel-headline">
+              <div class="panel-title">考核维度</div>
+              <el-button type="primary" plain @click="appendDimension">新增维度</el-button>
+            </div>
+
+            <div v-for="(dimension, dIndex) in form.dimensions" :key="dimension.localId" class="dimension-card">
+              <div class="dimension-header">
+                <div class="dimension-header-fields">
+                  <el-input v-model="dimension.dimensionName" placeholder="维度名称,如:业绩达成" />
+                  <el-select v-model="dimension.type" style="width: 160px">
+                    <el-option label="定量指标" value="定量指标" />
+                    <el-option label="定性指标" value="定性指标" />
+                    <el-option label="加分项" value="加分项" />
+                    <el-option label="减分项" value="减分项" />
+                  </el-select>
+                </div>
+                <div class="dimension-actions">
+                  <el-button text type="primary" @click="appendItem(dIndex)">新增指标</el-button>
+                  <el-button text type="danger" @click="removeDimension(dIndex)">删除维度</el-button>
+                </div>
+              </div>
+
+              <el-table :data="dimension.items" border class="dimension-table">
+                <el-table-column label="指标名称" min-width="180">
+                  <template #default="scope">
+                    <el-input v-model="scope.row.name" placeholder="请输入指标名称" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="考核标准" min-width="260">
+                  <template #default="scope">
+                    <el-input v-model="scope.row.standard" type="textarea" :rows="2" placeholder="请输入考核标准" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="门槛值" width="110">
+                  <template #default="scope">
+                    <el-input-number v-if="isQuantitative(dimension.type)" v-model="scope.row.thresholdValue" :min="0" :controls="false" style="width: 100%" />
+                    <span v-else>-</span>
+                  </template>
+                </el-table-column>
+                <el-table-column label="目标值" width="110">
+                  <template #default="scope">
+                    <el-input-number v-if="isQuantitative(dimension.type)" v-model="scope.row.targetValue" :min="0" :controls="false" style="width: 100%" />
+                    <span v-else>-</span>
+                  </template>
+                </el-table-column>
+                <el-table-column label="挑战值" width="110">
+                  <template #default="scope">
+                    <el-input-number v-if="isQuantitative(dimension.type)" v-model="scope.row.challengeValue" :min="0" :controls="false" style="width: 100%" />
+                    <span v-else>-</span>
+                  </template>
+                </el-table-column>
+                <el-table-column label="权重" width="110">
+                  <template #default="scope">
+                    <el-input-number
+                      v-if="supportsWeight(dimension.type)"
+                      v-model="scope.row.weight"
+                      :min="1"
+                      :max="100"
+                      :controls="false"
+                      style="width: 100%"
+                    />
+                    <span v-else>-</span>
+                  </template>
+                </el-table-column>
+                <el-table-column label="评分上限" width="120">
+                  <template #default="scope">
+                    <el-input-number v-model="scope.row.scoreLimit" :min="0" :max="100" :controls="false" style="width: 100%" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="备注" min-width="150">
+                  <template #default="scope">
+                    <el-input v-model="scope.row.remark" placeholder="备注" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="操作" width="90" fixed="right">
+                  <template #default="scope">
+                    <el-button text type="danger" @click="removeItem(dIndex, scope.$index)">删除</el-button>
+                  </template>
+                </el-table-column>
+              </el-table>
+
+              <div class="dimension-summary">
+                <span>指标数:{{ dimension.items.length }}</span>
+                <span v-if="supportsWeight(dimension.type)">总权重:{{ sumWeight(dimension.items) }}</span>
+                <span>总评分上限:{{ sumScoreLimit(dimension.items) }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class="side-panel">
+          <div class="panel sticky-panel">
+            <div class="panel-title">结构概览</div>
+            <div class="summary-row"><span>维度数量</span><strong>{{ form.dimensions.length }}</strong></div>
+            <div class="summary-row"><span>指标数量</span><strong>{{ totalItems }}</strong></div>
+            <div class="summary-row"><span>加分项上限</span><strong>{{ bonusScore }}</strong></div>
+            <div class="summary-row"><span>减分项上限</span><strong>{{ penaltyScore }}</strong></div>
+            <div class="summary-row"><span>基础评分上限</span><strong>{{ form.score }}</strong></div>
+            <div class="summary-row"><span>理论总分上限</span><strong>{{ form.score + bonusScore }}</strong></div>
+            <el-alert
+              v-if="bonusScore > 20 || penaltyScore > 20"
+              title="加分项或减分项上限超过 20 分,请调整。"
+              type="warning"
+              :closable="false"
+              show-icon
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
 </template>
 
 <script setup lang="ts">
-import OtrSimpleFormScene from '@/modules/otr/_shared/components/OtrSimpleFormScene.vue'
+import { computed, reactive, ref } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { effectContentGet, effectContentSave } from '@/api/otr/effect/core'
+
+const route = useRoute()
+const router = useRouter()
+const formRef = ref<ElFormInstance>()
+const submitting = ref(false)
+const localSeed = ref(1000)
+
+const form = reactive({
+  id: '',
+  name: '',
+  score: 100,
+  description: '',
+  dimensions: [] as any[],
+})
+
+const rules = reactive({
+  name: [{ required: true, message: '请输入考核内容名称', trigger: 'blur' }],
+})
+
+function nextLocalId() {
+  localSeed.value += 1
+  return localSeed.value
+}
+
+function createItem() {
+  return {
+    localId: nextLocalId(),
+    name: '',
+    standard: '',
+    thresholdValue: undefined,
+    targetValue: undefined,
+    challengeValue: undefined,
+    weight: 10,
+    scoreLimit: 10,
+    remark: '',
+  }
+}
+
+function createDimension() {
+  return {
+    localId: nextLocalId(),
+    dimensionName: '',
+    type: '定量指标',
+    items: [createItem()],
+  }
+}
+
+function supportsWeight(type: string) {
+  return type === '定量指标' || type === '定性指标'
+}
+
+function isQuantitative(type: string) {
+  return type === '定量指标'
+}
+
+function appendDimension() {
+  form.dimensions.push(createDimension())
+}
+
+function removeDimension(index: number) {
+  form.dimensions.splice(index, 1)
+}
+
+function appendItem(index: number) {
+  form.dimensions[index].items.push(createItem())
+}
+
+function removeItem(dIndex: number, iIndex: number) {
+  form.dimensions[dIndex].items.splice(iIndex, 1)
+}
+
+function sumWeight(items: any[]) {
+  return items.reduce((sum, item) => sum + Number(item.weight || 0), 0)
+}
+
+function sumScoreLimit(items: any[]) {
+  return items.reduce((sum, item) => sum + Number(item.scoreLimit || 0), 0)
+}
+
+const totalItems = computed(() => form.dimensions.reduce((sum, item) => sum + item.items.length, 0))
+const bonusScore = computed(() =>
+  form.dimensions.filter((item) => item.type === '加分项').reduce((sum, item) => sum + sumScoreLimit(item.items), 0),
+)
+const penaltyScore = computed(() =>
+  form.dimensions.filter((item) => item.type === '减分项').reduce((sum, item) => sum + sumScoreLimit(item.items), 0),
+)
+
+function normalizeDimension(raw: any) {
+  return {
+    localId: nextLocalId(),
+    id: raw.id,
+    dimensionName: raw.dimensionName || '',
+    type: raw.type || '定量指标',
+    items: (raw.items || raw.performCheckItemList || []).map((item: any) => ({
+      localId: nextLocalId(),
+      id: item.id,
+      name: item.name || '',
+      standard: item.standard || '',
+      thresholdValue: item.thresholdValue ?? undefined,
+      targetValue: item.targetValue ?? undefined,
+      challengeValue: item.challengeValue ?? undefined,
+      weight: Number(item.weight || 0),
+      scoreLimit: Number(item.scoreLimit || 0),
+      remark: item.remark || '',
+    })),
+  }
+}
+
+async function load() {
+  const id = String(route.query.id || '')
+  if (!id) {
+    if (!form.dimensions.length) appendDimension()
+    return
+  }
+  const res: any = await effectContentGet(id)
+  const result = res?.result || {}
+  form.id = String(result.id || id)
+  form.name = result.name || ''
+  form.score = Number(result.score || 100)
+  form.description = result.description || ''
+  form.dimensions = (result.dimensions || []).map(normalizeDimension)
+  if (!form.dimensions.length) appendDimension()
+}
+
+function validateBusiness() {
+  if (!form.dimensions.length) {
+    ElMessage.warning('请至少新增一个考核维度')
+    return false
+  }
+  if (bonusScore.value > 20) {
+    ElMessage.warning('加分项上限不能超过 20 分')
+    return false
+  }
+  if (penaltyScore.value > 20) {
+    ElMessage.warning('减分项上限不能超过 20 分')
+    return false
+  }
+  for (const dimension of form.dimensions) {
+    if (!dimension.dimensionName) {
+      ElMessage.warning('请填写维度名称')
+      return false
+    }
+    if (!dimension.items.length) {
+      ElMessage.warning(`维度“${dimension.dimensionName}”至少需要一个指标`)
+      return false
+    }
+    for (const item of dimension.items) {
+      if (!item.name || !item.standard) {
+        ElMessage.warning(`维度“${dimension.dimensionName}”下存在未填写完整的指标`)
+        return false
+      }
+      if (isQuantitative(dimension.type)) {
+        const quantitativeFields = [item.thresholdValue, item.targetValue, item.challengeValue]
+        if (quantitativeFields.some((field) => field === undefined || field === null || field === '')) {
+          ElMessage.warning(`维度“${dimension.dimensionName}”下的定量指标必须填写门槛值、目标值和挑战值`)
+          return false
+        }
+      }
+    }
+  }
+  return true
+}
+
+async function submit() {
+  const valid = await formRef.value?.validate().catch(() => false)
+  if (!valid || !validateBusiness()) return
+
+  submitting.value = true
+  try {
+    const payload = {
+      id: form.id || undefined,
+      name: form.name,
+      score: form.score,
+      description: form.description,
+      dimensions: form.dimensions.map((dimension) => ({
+        id: dimension.id,
+        dimensionName: dimension.dimensionName,
+        type: dimension.type,
+        items: dimension.items.map((item: any) => ({
+          id: item.id,
+          name: item.name,
+          standard: item.standard,
+          thresholdValue: item.thresholdValue,
+          targetValue: item.targetValue,
+          challengeValue: item.challengeValue,
+          weight: item.weight,
+          scoreLimit: item.scoreLimit,
+          remark: item.remark,
+        })),
+      })),
+    }
+    await effectContentSave(payload)
+    ElMessage.success('考核内容已保存')
+    router.push('/otr/effect/template')
+  } finally {
+    submitting.value = false
+  }
+}
+
+function goBack() {
+  router.back()
+}
+
+load()
 </script>
+
+<style scoped lang="scss">
+.effect-content-form-page {
+  .page-card {
+    background: #fff;
+    border-radius: 10px;
+    padding: 24px;
+  }
+
+  .page-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+    gap: 16px;
+    padding-bottom: 20px;
+    margin-bottom: 20px;
+    border-bottom: 1px solid #eef2f6;
+  }
+
+  .page-title {
+    font-size: 22px;
+    font-weight: 600;
+    color: #111827;
+  }
+
+  .page-subtitle {
+    margin-top: 6px;
+    font-size: 13px;
+    color: #6b7280;
+  }
+
+  .page-actions {
+    display: flex;
+    gap: 12px;
+  }
+
+  .page-content {
+    display: grid;
+    grid-template-columns: minmax(0, 2fr) 320px;
+    gap: 20px;
+  }
+
+  .main-panel,
+  .side-panel {
+    min-width: 0;
+  }
+
+  .panel {
+    padding: 20px;
+    border: 1px solid #e5e7eb;
+    border-radius: 12px;
+    background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
+  }
+
+  .main-panel {
+    display: grid;
+    gap: 20px;
+  }
+
+  .panel-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #111827;
+  }
+
+  .panel-headline {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 16px;
+  }
+
+  .dimension-card {
+    border: 1px solid #e5e7eb;
+    border-radius: 10px;
+    padding: 16px;
+    margin-bottom: 16px;
+    background: #fff;
+  }
+
+  .dimension-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 16px;
+    margin-bottom: 12px;
+  }
+
+  .dimension-header-fields {
+    display: flex;
+    gap: 12px;
+    flex: 1;
+  }
+
+  .dimension-actions {
+    display: flex;
+    gap: 8px;
+    flex-shrink: 0;
+  }
+
+  .dimension-summary {
+    display: flex;
+    gap: 18px;
+    margin-top: 12px;
+    color: #6b7280;
+    font-size: 13px;
+    flex-wrap: wrap;
+  }
+
+  .sticky-panel {
+    position: sticky;
+    top: 16px;
+  }
+
+  .summary-row {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 10px 0;
+    border-bottom: 1px solid #f3f4f6;
+    color: #374151;
+  }
+
+  .summary-row strong {
+    color: #111827;
+  }
+
+  :deep(.el-alert) {
+    margin-top: 16px;
+  }
+
+  @media (max-width: 1200px) {
+    .page-content {
+      grid-template-columns: 1fr;
+    }
+
+    .dimension-header,
+    .page-header {
+      flex-direction: column;
+      align-items: stretch;
+    }
+
+    .dimension-header-fields {
+      flex-direction: column;
+    }
+  }
+}
+</style>
+

+ 569 - 2
ui/sp-user-center/src/modules/otr/effect/views/TemplateForm.vue

@@ -1,7 +1,574 @@
 <template>
-  <otr-simple-form-scene title="新建考核模板" />
+  <div class="frame-body adaption-frame-body effect-template-form-page">
+    <div class="page-card">
+      <div class="page-header">
+        <div>
+          <div class="page-title">考核模板</div>
+          <div class="page-subtitle">保留旧版模板页的核心能力:模板信息、考核内容、流程控制与评分人配置。</div>
+        </div>
+        <div class="page-actions">
+          <el-button @click="goBack">返回</el-button>
+          <el-button type="primary" :loading="submitting" @click="submit">保存模板</el-button>
+        </div>
+      </div>
+
+      <div class="page-content">
+        <div class="main-panel">
+          <div class="panel">
+            <div class="panel-title">模板基础信息</div>
+            <el-form ref="formRef" :model="form" :rules="rules" label-position="top">
+              <el-form-item label="模板名称" prop="name">
+                <el-input v-model="form.name" maxlength="120" show-word-limit placeholder="请输入考核模板名称" />
+              </el-form-item>
+              <el-form-item label="周期类型" prop="cycleType">
+                <el-radio-group v-model="form.cycleType">
+                  <el-radio value="月度">月度</el-radio>
+                  <el-radio value="季度">季度</el-radio>
+                  <el-radio value="半年度">半年度</el-radio>
+                  <el-radio value="年度">年度</el-radio>
+                </el-radio-group>
+              </el-form-item>
+              <el-form-item label="考核对象可见">
+                <div class="visibility-row">
+                  <el-checkbox v-model="isOpenScore">评分人的评分</el-checkbox>
+                  <el-checkbox v-model="isOpenRemark">评分人的评分说明</el-checkbox>
+                </div>
+              </el-form-item>
+            </el-form>
+          </div>
+
+          <div class="panel">
+            <div class="panel-headline">
+              <div class="panel-title">考核内容</div>
+              <div class="content-actions">
+                <el-select v-model="selectedContentId" placeholder="请选择考核内容" style="width: 320px" @change="handleContentChange">
+                  <el-option
+                    v-for="item in contentOptions"
+                    :key="item.contentMainId || item.id"
+                    :label="item.name"
+                    :value="String(item.contentMainId || item.id)"
+                  />
+                </el-select>
+                <el-button type="primary" plain @click="goCreateContent">新建考核内容</el-button>
+              </div>
+            </div>
+
+            <el-empty v-if="!contentDetail" description="请选择考核内容" />
+            <div v-else class="content-preview">
+              <div class="content-summary">
+                <span>内容名称:{{ contentDetail.name }}</span>
+                <span>评分上限:{{ contentDetail.score }} 分</span>
+                <span>维度数量:{{ contentDetail.dimensions?.length || 0 }}</span>
+              </div>
+              <el-table :data="contentPreviewRows" border>
+                <el-table-column prop="dimensionName" label="维度" min-width="160" />
+                <el-table-column prop="type" label="类型" width="100" />
+                <el-table-column prop="name" label="指标名称" min-width="180" />
+                <el-table-column prop="standard" label="考核标准" min-width="240" />
+                <el-table-column prop="weight" label="权重" width="90" />
+                <el-table-column prop="scoreLimit" label="评分上限" width="110" />
+              </el-table>
+            </div>
+          </div>
+
+          <div class="panel">
+            <div class="panel-title">考核流程</div>
+            <div class="process-grid">
+              <div class="process-card">
+                <div class="process-header">
+                  <span>考核内容确认</span>
+                  <el-switch v-model="isContentEdit" />
+                </div>
+                <div v-if="isContentEdit" class="process-body">
+                  <el-radio-group v-model="confirmerMode">
+                    <el-radio value="self">考核对象本人</el-radio>
+                    <el-radio value="assign">指定成员</el-radio>
+                  </el-radio-group>
+                  <SelectUser v-if="confirmerMode === 'assign'" v-model="form.confirmerUser" placeholder="请选择确认人" />
+                </div>
+              </div>
+
+              <div class="process-card">
+                <div class="process-header">
+                  <span>结果值录入</span>
+                  <el-switch v-model="isResultEntry" />
+                </div>
+                <div v-if="isResultEntry" class="process-body">
+                  <el-radio-group v-model="resultEntryMode">
+                    <el-radio value="self">考核对象本人</el-radio>
+                    <el-radio value="assign">指定成员</el-radio>
+                  </el-radio-group>
+                  <SelectUser v-if="resultEntryMode === 'assign'" v-model="form.resultEntryUser" placeholder="请选择录入人" />
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <div class="panel">
+            <div class="panel-headline">
+              <div class="panel-title">评分人配置</div>
+              <el-button type="primary" plain @click="appendAppraiser">新增评分人</el-button>
+            </div>
+            <el-table :data="form.appraisers" border>
+              <el-table-column label="评分人角色" width="180">
+                <template #default="scope">
+                  <el-select v-model="scope.row.roleType" style="width: 100%" @change="handleRoleTypeChange(scope.row)">
+                    <el-option label="考核对象本人" value="考核对象本人" />
+                    <el-option label="直属上级" value="直属上级" />
+                    <el-option label="指定成员" value="指定成员" />
+                  </el-select>
+                </template>
+              </el-table-column>
+              <el-table-column label="评分人" min-width="240">
+                <template #default="scope">
+                  <span v-if="scope.row.roleType === '考核对象本人'" class="inline-text">系统自动取考核对象本人</span>
+                  <SelectUser v-else v-model="scope.row.userId" placeholder="请选择评分人" />
+                </template>
+              </el-table-column>
+              <el-table-column label="权重" width="120">
+                <template #default="scope">
+                  <el-input-number v-model="scope.row.weight" :min="1" :max="100" :controls="false" style="width: 100%" />
+                </template>
+              </el-table-column>
+              <el-table-column label="评分可见" width="110">
+                <template #default="scope">
+                  <el-checkbox v-model="scope.row.scoreVisible" />
+                </template>
+              </el-table-column>
+              <el-table-column label="评分说明可见" width="130">
+                <template #default="scope">
+                  <el-checkbox v-model="scope.row.remarkVisible" />
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" width="90" fixed="right">
+                <template #default="scope">
+                  <el-button text type="danger" @click="removeAppraiser(scope.$index)">删除</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </div>
+
+        <div class="side-panel">
+          <div class="panel sticky-panel">
+            <div class="panel-title">模板概览</div>
+            <div class="summary-row"><span>关联内容</span><strong>{{ contentDetail?.name || '未选择' }}</strong></div>
+            <div class="summary-row"><span>内容满分</span><strong>{{ contentDetail?.score || 0 }}</strong></div>
+            <div class="summary-row"><span>评分人数</span><strong>{{ form.appraisers.length }}</strong></div>
+            <div class="summary-row"><span>评分总权重</span><strong>{{ totalWeight }}</strong></div>
+            <div class="summary-row"><span>内容确认</span><strong>{{ isContentEdit ? '开启' : '关闭' }}</strong></div>
+            <div class="summary-row"><span>结果录入</span><strong>{{ isResultEntry ? '开启' : '关闭' }}</strong></div>
+            <el-alert
+              v-if="totalWeight !== 100 && form.appraisers.length"
+              title="评分人权重建议合计为 100%。"
+              type="warning"
+              :closable="false"
+              show-icon
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
 </template>
 
 <script setup lang="ts">
-import OtrSimpleFormScene from '@/modules/otr/_shared/components/OtrSimpleFormScene.vue'
+import { computed, reactive, ref } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import SelectUser from '@/components/SelectUser/SelectUser.vue'
+import {
+  effectContentGet,
+  effectContentList,
+  effectTemplateGet,
+  effectTemplateSave,
+} from '@/api/otr/effect/core'
+
+const route = useRoute()
+const router = useRouter()
+const formRef = ref<ElFormInstance>()
+const submitting = ref(false)
+const selectedContentId = ref('')
+const contentOptions = ref<any[]>([])
+const contentDetail = ref<any>(null)
+const localSeed = ref(2000)
+
+const form = reactive({
+  id: '',
+  name: '',
+  cycleType: '月度',
+  confirmerUser: undefined as string | number | undefined,
+  resultEntryUser: undefined as string | number | undefined,
+  appraisers: [] as any[],
+})
+
+const isOpenScore = ref(true)
+const isOpenRemark = ref(true)
+const isContentEdit = ref(false)
+const isResultEntry = ref(false)
+const confirmerMode = ref<'self' | 'assign'>('self')
+const resultEntryMode = ref<'self' | 'assign'>('self')
+
+const rules = reactive({
+  name: [{ required: true, message: '请输入考核模板名称', trigger: 'blur' }],
+  cycleType: [{ required: true, message: '请选择周期类型', trigger: 'change' }],
+})
+
+function nextLocalId() {
+  localSeed.value += 1
+  return localSeed.value
+}
+
+function createAppraiser() {
+  return {
+    localId: nextLocalId(),
+    roleType: '直属上级',
+    userId: undefined as string | number | undefined,
+    weight: 100,
+    scoreVisible: false,
+    remarkVisible: false,
+  }
+}
+
+function normalizeAppraiser(item: any) {
+  return {
+    localId: nextLocalId(),
+    id: item.id,
+    roleType: item.roleType || '直属上级',
+    userId: item.userId === 0 ? undefined : item.userId,
+    weight: Number(item.weight || 0),
+    scoreVisible: Number(item.isScoreVisibility || 0) === 1,
+    remarkVisible: Number(item.isRemarkVisibility || 0) === 1,
+  }
+}
+
+function appendAppraiser() {
+  form.appraisers.push(createAppraiser())
+}
+
+function removeAppraiser(index: number) {
+  form.appraisers.splice(index, 1)
+}
+
+function handleRoleTypeChange(row: any) {
+  if (row.roleType === '考核对象本人') {
+    row.userId = undefined
+  }
+}
+
+async function loadContentOptions() {
+  const res: any = await effectContentList({ pageNo: 1, pageSize: 999 })
+  const result = res?.result || {}
+  contentOptions.value = result.records || result.list || []
+}
+
+async function loadContentDetail(id: string | number) {
+  if (!id) {
+    contentDetail.value = null
+    return
+  }
+  const res: any = await effectContentGet(id)
+  const result = res?.result || {}
+  contentDetail.value = {
+    ...result,
+    dimensions: result.dimensions || [],
+  }
+}
+
+async function handleContentChange(id: string) {
+  selectedContentId.value = id
+  await loadContentDetail(id)
+}
+
+const contentPreviewRows = computed(() => {
+  const dimensions = contentDetail.value?.dimensions || []
+  return dimensions.flatMap((dimension: any) =>
+    (dimension.items || []).map((item: any) => ({
+      dimensionName: dimension.dimensionName,
+      type: dimension.type,
+      name: item.name,
+      standard: item.standard,
+      weight: item.weight ?? '-',
+      scoreLimit: item.scoreLimit ?? '-',
+    })),
+  )
+})
+
+const totalWeight = computed(() => form.appraisers.reduce((sum, item) => sum + Number(item.weight || 0), 0))
+
+async function load() {
+  await loadContentOptions()
+  const id = String(route.query.id || '')
+  if (!id) {
+    if (!form.appraisers.length) appendAppraiser()
+    return
+  }
+
+  const res: any = await effectTemplateGet(id)
+  const result = res?.result || {}
+  form.id = String(result.id || id)
+  form.name = result.name || ''
+  form.cycleType = result.cycleType || '月度'
+  isOpenScore.value = Number(result.isOpenScore || 0) === 1
+  isOpenRemark.value = Number(result.isOpenRemark || 0) === 1
+  isContentEdit.value = Number(result.isContentEdit || 0) === 1
+  isResultEntry.value = Number(result.isResultEntry || 0) === 1
+
+  confirmerMode.value = result.confirmerUser === 0 || result.confirmerUser === '0' || !result.confirmerUser ? 'self' : 'assign'
+  resultEntryMode.value = result.resultEntryUser === 0 || result.resultEntryUser === '0' || !result.resultEntryUser ? 'self' : 'assign'
+  form.confirmerUser = confirmerMode.value === 'assign' ? result.confirmerUser : undefined
+  form.resultEntryUser = resultEntryMode.value === 'assign' ? result.resultEntryUser : undefined
+
+  const contentId = String(result.contentId || result.contentMainId || '')
+  selectedContentId.value = contentId
+  if (contentId) {
+    await loadContentDetail(contentId)
+  }
+
+  form.appraisers = (result.appraisers || []).map(normalizeAppraiser)
+  if (!form.appraisers.length) appendAppraiser()
+}
+
+function validateBusiness() {
+  if (!selectedContentId.value) {
+    ElMessage.warning('请选择考核内容')
+    return false
+  }
+  if (isContentEdit.value && confirmerMode.value === 'assign' && !form.confirmerUser) {
+    ElMessage.warning('请选择考核内容确认人')
+    return false
+  }
+  if (isResultEntry.value && resultEntryMode.value === 'assign' && !form.resultEntryUser) {
+    ElMessage.warning('请选择结果值录入人')
+    return false
+  }
+  if (!form.appraisers.length) {
+    ElMessage.warning('请至少配置一个评分人')
+    return false
+  }
+  for (const item of form.appraisers) {
+    if (!item.roleType) {
+      ElMessage.warning('评分人角色不能为空')
+      return false
+    }
+    if (item.roleType !== '考核对象本人' && !item.userId) {
+      ElMessage.warning('请补全评分人信息')
+      return false
+    }
+  }
+  return true
+}
+
+async function submit() {
+  const valid = await formRef.value?.validate().catch(() => false)
+  if (!valid || !validateBusiness()) return
+
+  submitting.value = true
+  try {
+    const payload = {
+      id: form.id || undefined,
+      name: form.name,
+      cycleType: form.cycleType,
+      isOpenScore: isOpenScore.value ? 1 : 0,
+      isOpenRemark: isOpenRemark.value ? 1 : 0,
+      isContentEdit: isContentEdit.value ? 1 : 0,
+      isResultEntry: isResultEntry.value ? 1 : 0,
+      contentId: selectedContentId.value,
+      contentMainId: selectedContentId.value,
+      confirmerUser: isContentEdit.value ? (confirmerMode.value === 'assign' ? form.confirmerUser : 0) : 0,
+      resultEntryUser: isResultEntry.value ? (resultEntryMode.value === 'assign' ? form.resultEntryUser : 0) : 0,
+      appraisers: form.appraisers.map((item) => ({
+        id: item.id,
+        roleType: item.roleType,
+        userId: item.roleType === '考核对象本人' ? 0 : item.userId,
+        weight: item.weight,
+        isScoreVisibility: item.scoreVisible ? 1 : 0,
+        isRemarkVisibility: item.remarkVisible ? 1 : 0,
+      })),
+    }
+    await effectTemplateSave(payload)
+    ElMessage.success('考核模板已保存')
+    router.push('/otr/effect/template')
+  } finally {
+    submitting.value = false
+  }
+}
+
+function goCreateContent() {
+  router.push('/otr/effect/content-form')
+}
+
+function goBack() {
+  router.back()
+}
+
+load()
 </script>
+
+<style scoped lang="scss">
+.effect-template-form-page {
+  .page-card {
+    background: #fff;
+    border-radius: 10px;
+    padding: 24px;
+  }
+
+  .page-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+    gap: 16px;
+    padding-bottom: 20px;
+    margin-bottom: 20px;
+    border-bottom: 1px solid #eef2f6;
+  }
+
+  .page-title {
+    font-size: 22px;
+    font-weight: 600;
+    color: #111827;
+  }
+
+  .page-subtitle {
+    margin-top: 6px;
+    font-size: 13px;
+    color: #6b7280;
+  }
+
+  .page-actions {
+    display: flex;
+    gap: 12px;
+  }
+
+  .page-content {
+    display: grid;
+    grid-template-columns: minmax(0, 2fr) 320px;
+    gap: 20px;
+  }
+
+  .main-panel {
+    display: grid;
+    gap: 20px;
+    min-width: 0;
+  }
+
+  .panel {
+    padding: 20px;
+    border: 1px solid #e5e7eb;
+    border-radius: 12px;
+    background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
+  }
+
+  .panel-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #111827;
+  }
+
+  .panel-headline {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    gap: 16px;
+    margin-bottom: 16px;
+  }
+
+  .content-actions {
+    display: flex;
+    gap: 12px;
+    align-items: center;
+    flex-wrap: wrap;
+  }
+
+  .visibility-row {
+    display: flex;
+    gap: 24px;
+    flex-wrap: wrap;
+  }
+
+  .content-preview {
+    display: grid;
+    gap: 16px;
+  }
+
+  .content-summary {
+    display: flex;
+    gap: 20px;
+    flex-wrap: wrap;
+    color: #6b7280;
+    font-size: 13px;
+  }
+
+  .process-grid {
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    gap: 16px;
+  }
+
+  .process-card {
+    padding: 16px;
+    border: 1px solid #e5e7eb;
+    border-radius: 10px;
+    background: #fff;
+  }
+
+  .process-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 12px;
+  }
+
+  .process-body {
+    margin-top: 14px;
+    display: grid;
+    gap: 12px;
+  }
+
+  .inline-text {
+    color: #6b7280;
+    font-size: 13px;
+  }
+
+  .sticky-panel {
+    position: sticky;
+    top: 16px;
+  }
+
+  .summary-row {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 10px 0;
+    border-bottom: 1px solid #f3f4f6;
+    gap: 12px;
+    color: #374151;
+  }
+
+  .summary-row strong {
+    color: #111827;
+    text-align: right;
+  }
+
+  :deep(.el-alert) {
+    margin-top: 16px;
+  }
+
+  @media (max-width: 1200px) {
+    .page-content {
+      grid-template-columns: 1fr;
+    }
+
+    .page-header,
+    .panel-headline {
+      flex-direction: column;
+      align-items: stretch;
+    }
+
+    .process-grid {
+      grid-template-columns: 1fr;
+    }
+  }
+}
+</style>
+

+ 330 - 2
ui/sp-user-center/src/modules/otr/summary/views/Announcement.vue

@@ -1,7 +1,335 @@
 <template>
-  <summary-workspace title="公告列表" scene="announcement" />
+  <div class="frame-body adaption-frame-body summary-announcement-page">
+    <el-card shadow="never">
+      <template #header>
+        <div class="page-header">
+          <div>
+            <div class="page-title">内部公告</div>
+            <div class="page-subtitle">按旧版公告页方式展示发布内容,保留时间筛选和发布维护能力。</div>
+          </div>
+          <div class="page-actions">
+            <el-button @click="reset">重置</el-button>
+            <el-button type="primary" @click="openCreate">发布公告</el-button>
+          </div>
+        </div>
+      </template>
+
+      <el-form :inline="true" @submit.prevent class="toolbar">
+        <el-form-item>
+          <el-date-picker
+            v-model="dateRange"
+            type="daterange"
+            value-format="YYYY-MM-DD"
+            start-placeholder="开始时间"
+            end-placeholder="结束时间"
+            @change="searchList"
+          />
+        </el-form-item>
+      </el-form>
+
+      <div v-loading="loading" class="announcement-list">
+        <el-empty v-if="!rows.length && !loading" description="暂无公告" />
+
+        <article v-for="item in rows" :key="item.bulletinId || item.id" class="announcement-card">
+          <div class="announcement-top">
+            <div class="publisher-block">
+              <el-avatar :size="36">{{ getAvatarText(item.staffName) }}</el-avatar>
+              <div>
+                <div class="publisher-name">{{ item.staffName || item.publisher || '-' }}</div>
+                <div class="publisher-time">发布时间:{{ item.createTime || item.createdAt || '-' }}</div>
+              </div>
+            </div>
+            <div class="meta-actions">
+              <el-button v-if="item.canUpdate" link type="primary" @click="openEdit(item)">编辑</el-button>
+              <el-button v-if="item.canDelete" link type="danger" @click="remove(item)">删除</el-button>
+            </div>
+          </div>
+
+          <div class="announcement-headline">
+            <h3>{{ item.title || '-' }}</h3>
+            <span>截止时间:{{ item.deadlineDate || '-' }}</span>
+          </div>
+
+          <div class="announcement-content ql-editor" v-html="item.content || '-'" />
+
+          <div class="announcement-foot">
+            <span>通知范围:{{ Number(item.bulletinType) === 1 ? '全体员工' : '指定人员' }}</span>
+            <span>已读人数:{{ countRead(item.readVOs) }}</span>
+            <span>通知人数:{{ Array.isArray(item.readVOs) ? item.readVOs.length : 0 }}</span>
+          </div>
+        </article>
+      </div>
+    </el-card>
+
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="960px" destroy-on-close>
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="标题" prop="title">
+          <el-input v-model="form.title" placeholder="请输入公告标题" />
+        </el-form-item>
+        <el-form-item label="截止时间" prop="deadlineDate">
+          <el-date-picker v-model="form.deadlineDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择截止时间" />
+        </el-form-item>
+        <el-form-item label="接收范围" prop="bulletinType">
+          <el-radio-group v-model="form.bulletinType">
+            <el-radio :value="1">全体员工</el-radio>
+            <el-radio :value="2">指定人员</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="公告内容" prop="content">
+          <Editor v-model="form.content" :min-height="240" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submit">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
 </template>
 
 <script setup lang="ts">
-import SummaryWorkspace from '@/modules/otr/summary/components/SummaryWorkspace.vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { reactive, ref } from 'vue'
+import moment from 'moment'
+import Editor from '@/components/Editor/index.vue'
+import {
+  summaryAnnouncementList,
+  summaryBulletinAdd,
+  summaryBulletinDelete,
+  summaryBulletinEdit,
+} from '@/api/otr/summary/core'
+
+const loading = ref(false)
+const submitting = ref(false)
+const dialogVisible = ref(false)
+const dialogTitle = ref('发布公告')
+const editingId = ref<string | number | null>(null)
+const rows = ref<any[]>([])
+const formRef = ref<ElFormInstance>()
+const dateRange = ref<[string, string] | string[]>([])
+
+const form = reactive({
+  title: '',
+  deadlineDate: '',
+  bulletinType: 1,
+  content: '',
+})
+
+const rules = reactive({
+  title: [{ required: true, message: '标题不能为空', trigger: 'blur' }],
+  deadlineDate: [{ required: true, message: '截止时间不能为空', trigger: 'change' }],
+  bulletinType: [{ required: true, message: '接收范围不能为空', trigger: 'change' }],
+  content: [{ required: true, message: '公告内容不能为空', trigger: 'blur' }],
+})
+
+function getAvatarText(name?: string) {
+  return name ? name.slice(-1) : '公'
+}
+
+function countRead(list: any[]) {
+  if (!Array.isArray(list)) return 0
+  return list.filter((item) => item?.hasRead).length
+}
+
+async function load() {
+  loading.value = true
+  try {
+    const res: any = await summaryAnnouncementList({
+      pageIndex: 1,
+      pageSize: 20,
+      startDate: dateRange.value?.[0] || '',
+      endDate: dateRange.value?.[1] || '',
+    })
+    const result = res?.result || {}
+    rows.value = result.records || result.list || []
+  } finally {
+    loading.value = false
+  }
+}
+
+function resetForm() {
+  form.title = ''
+  form.deadlineDate = ''
+  form.bulletinType = 1
+  form.content = ''
+}
+
+function searchList() {
+  load()
+}
+
+function reset() {
+  dateRange.value = []
+  load()
+}
+
+function openCreate() {
+  editingId.value = null
+  dialogTitle.value = '发布公告'
+  resetForm()
+  dialogVisible.value = true
+}
+
+function openEdit(row: any) {
+  editingId.value = row.bulletinId || row.id
+  dialogTitle.value = '编辑公告'
+  form.title = row.title || ''
+  form.deadlineDate = row.deadlineDate || ''
+  form.bulletinType = Number(row.bulletinType || 1)
+  form.content = row.content || ''
+  dialogVisible.value = true
+}
+
+async function submit() {
+  const valid = await formRef.value?.validate().catch(() => false)
+  if (!valid) return
+  submitting.value = true
+  try {
+    const payload: Record<string, any> = {
+      title: form.title,
+      deadlineDate: form.deadlineDate,
+      bulletinType: form.bulletinType,
+      content: form.content,
+      noticeTime: moment().format('YYYY-MM-DD'),
+    }
+    if (editingId.value) {
+      payload.id = editingId.value
+      payload.bulletinId = editingId.value
+      await summaryBulletinEdit(payload)
+      ElMessage.success('编辑成功')
+    } else {
+      await summaryBulletinAdd(payload)
+      ElMessage.success('发布成功')
+    }
+    dialogVisible.value = false
+    load()
+  } finally {
+    submitting.value = false
+  }
+}
+
+function remove(row: any) {
+  ElMessageBox.confirm('确定删除这条公告吗?', '系统提示', {
+    confirmButtonText: '提交',
+    cancelButtonText: '取消',
+    type: 'warning',
+  })
+    .then(async () => {
+      await summaryBulletinDelete(row.bulletinId || row.id)
+      ElMessage.success('删除成功')
+      load()
+    })
+    .catch(() => undefined)
+}
+
+load()
 </script>
+
+<style scoped lang="scss">
+.summary-announcement-page {
+  .page-header {
+    display: flex;
+    align-items: flex-start;
+    justify-content: space-between;
+    gap: 16px;
+  }
+
+  .page-title {
+    font-size: 18px;
+    font-weight: 600;
+    color: #111827;
+  }
+
+  .page-subtitle {
+    margin-top: 4px;
+    font-size: 13px;
+    color: #6b7280;
+  }
+
+  .page-actions {
+    display: flex;
+    gap: 12px;
+  }
+
+  .toolbar {
+    margin-bottom: 16px;
+  }
+
+  .announcement-list {
+    display: grid;
+    gap: 16px;
+  }
+
+  .announcement-card {
+    padding: 22px;
+    border: 1px solid #e5e7eb;
+    border-radius: 14px;
+    background: linear-gradient(180deg, #ffffff 0%, #fafcfe 100%);
+  }
+
+  .announcement-top {
+    display: flex;
+    align-items: flex-start;
+    justify-content: space-between;
+    gap: 16px;
+  }
+
+  .publisher-block {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+  }
+
+  .publisher-name {
+    font-size: 15px;
+    font-weight: 600;
+    color: #111827;
+  }
+
+  .publisher-time {
+    margin-top: 4px;
+    color: #6b7280;
+    font-size: 12px;
+  }
+
+  .meta-actions {
+    display: flex;
+    gap: 8px;
+  }
+
+  .announcement-headline {
+    margin-top: 16px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 16px;
+  }
+
+  .announcement-headline h3 {
+    margin: 0;
+    font-size: 20px;
+    color: #111827;
+  }
+
+  .announcement-headline span {
+    color: #6b7280;
+    font-size: 13px;
+  }
+
+  .announcement-content {
+    margin-top: 18px;
+    padding: 0;
+    color: #374151;
+  }
+
+  .announcement-foot {
+    margin-top: 18px;
+    display: flex;
+    gap: 20px;
+    flex-wrap: wrap;
+    color: #6b7280;
+    font-size: 13px;
+  }
+}
+</style>
+

+ 311 - 2
ui/sp-user-center/src/modules/otr/summary/views/RemindTask.vue

@@ -1,7 +1,316 @@
 <template>
-  <otr-simple-form-scene title="简报提醒" />
+  <div class="frame-body adaption-frame-body summary-remind-page">
+    <el-card shadow="never">
+      <template #header>
+        <div class="page-header">
+          <div>
+            <div class="page-title">简报提醒</div>
+            <div class="page-subtitle">维护简报相关定时任务,保留旧版页面的任务增删改和执行控制。</div>
+          </div>
+          <div class="page-actions">
+            <el-button @click="load">刷新</el-button>
+            <el-button type="primary" @click="openCreate">新增定时任务</el-button>
+          </div>
+        </div>
+      </template>
+
+      <el-form :inline="true" @submit.prevent>
+        <el-form-item>
+          <el-input v-model="search.jobName" placeholder="请输入任务名称" clearable @keyup.enter="searchList" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="searchList">搜索</el-button>
+          <el-button @click="reset">重置</el-button>
+        </el-form-item>
+      </el-form>
+
+      <el-table v-loading="loading" :data="rows" border>
+        <el-table-column prop="jobName" label="定时任务" min-width="180" />
+        <el-table-column prop="jobGroup" label="任务组" min-width="120" />
+        <el-table-column prop="cronExpression" label="Cron表达式" min-width="180" />
+        <el-table-column prop="jobDescription" label="任务描述" min-width="220" />
+        <el-table-column prop="previousFireTime" label="上次执行时间" min-width="160" />
+        <el-table-column prop="nextFireTime" label="下次执行时间" min-width="160" />
+        <el-table-column prop="createTime" label="创建时间" min-width="160" />
+        <el-table-column label="状态" width="100" align="center">
+          <template #default="{ row }">
+            <el-tag :type="row.jobStatus === 'NORMAL' ? 'success' : 'danger'">
+              {{ row.jobStatus === 'NORMAL' ? '执行中' : '已停止' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="320" align="center" fixed="right">
+          <template #default="{ row }">
+            <el-button v-if="row.jobStatus === 'NORMAL'" link type="warning" @click="pause(row)">暂停</el-button>
+            <el-button v-else link type="success" @click="resume(row)">恢复执行</el-button>
+            <el-button link type="primary" @click="openEdit(row)">编辑</el-button>
+            <el-button link type="danger" @click="remove(row)">删除</el-button>
+            <el-button link @click="trigger(row)">立即执行</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="pager-wrap">
+        <el-pagination
+          v-model:current-page="search.pageIndex"
+          v-model:page-size="search.pageSize"
+          background
+          :page-sizes="[10, 20, 50, 100]"
+          :total="total"
+          layout="total, sizes, prev, pager, next"
+          @size-change="load"
+          @current-change="load"
+        />
+      </div>
+    </el-card>
+
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px">
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="任务执行类" prop="jobName">
+          <el-input v-model="form.jobName" clearable />
+        </el-form-item>
+        <el-form-item label="任务组名" prop="jobGroup">
+          <el-input v-model="form.jobGroup" clearable />
+        </el-form-item>
+        <el-form-item label="Cron表达式" prop="cronExpression">
+          <el-input v-model="form.cronExpression" clearable />
+          <a class="cron-link" target="_blank" href="https://www.matools.com/cron/">在线生成 Cron 表达式</a>
+        </el-form-item>
+        <el-form-item label="任务描述" prop="jobDescription">
+          <el-input v-model="form.jobDescription" clearable />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submit">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
 </template>
 
 <script setup lang="ts">
-import OtrSimpleFormScene from '@/modules/otr/_shared/components/OtrSimpleFormScene.vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { reactive, ref } from 'vue'
+import {
+  summaryRemindTaskAdd,
+  summaryRemindTaskDelete,
+  summaryRemindTaskEdit,
+  summaryRemindTaskList,
+  summaryRemindTaskPause,
+  summaryRemindTaskResume,
+  summaryRemindTaskTrigger,
+} from '@/api/otr/summary/core'
+
+const loading = ref(false)
+const submitting = ref(false)
+const dialogVisible = ref(false)
+const dialogTitle = ref('新增定时任务')
+const editing = ref(false)
+const rows = ref<any[]>([])
+const total = ref(0)
+const formRef = ref<ElFormInstance>()
+
+const search = reactive({
+  pageIndex: 1,
+  pageSize: 10,
+  jobName: '',
+})
+
+const form = reactive({
+  id: undefined as string | number | undefined,
+  jobName: '',
+  jobGroup: '',
+  cronExpression: '',
+  jobDescription: '',
+})
+
+const rules = reactive({
+  jobName: [{ required: true, message: '任务名不能为空', trigger: 'blur' }],
+  jobGroup: [{ required: true, message: '任务组名不能为空', trigger: 'blur' }],
+  cronExpression: [{ required: true, message: 'Cron表达式不能为空', trigger: 'blur' }],
+  jobDescription: [{ required: true, message: '任务描述不能为空', trigger: 'blur' }],
+})
+
+function resetForm() {
+  form.id = undefined
+  form.jobName = ''
+  form.jobGroup = ''
+  form.cronExpression = ''
+  form.jobDescription = ''
+}
+
+async function load() {
+  loading.value = true
+  try {
+    const res: any = await summaryRemindTaskList({
+      pageIndex: search.pageIndex,
+      pageSize: search.pageSize,
+      jobName: search.jobName,
+    })
+    const result = res?.result || {}
+    rows.value = result.records || result.list || []
+    total.value = Number(result.total || rows.value.length)
+  } finally {
+    loading.value = false
+  }
+}
+
+function searchList() {
+  search.pageIndex = 1
+  load()
+}
+
+function reset() {
+  search.pageIndex = 1
+  search.pageSize = 10
+  search.jobName = ''
+  load()
+}
+
+function openCreate() {
+  editing.value = false
+  dialogTitle.value = '新增定时任务'
+  resetForm()
+  dialogVisible.value = true
+}
+
+function openEdit(row: any) {
+  editing.value = true
+  dialogTitle.value = '编辑定时任务'
+  form.id = row.id
+  form.jobName = row.jobName || ''
+  form.jobGroup = row.jobGroup || ''
+  form.cronExpression = row.cronExpression || ''
+  form.jobDescription = row.jobDescription || ''
+  dialogVisible.value = true
+}
+
+async function submit() {
+  const valid = await formRef.value?.validate().catch(() => false)
+  if (!valid) return
+  submitting.value = true
+  try {
+    if (editing.value) {
+      await summaryRemindTaskEdit({ ...form })
+      ElMessage.success('编辑成功')
+    } else {
+      await summaryRemindTaskAdd({ ...form })
+      ElMessage.success('新增成功')
+    }
+    dialogVisible.value = false
+    load()
+  } finally {
+    submitting.value = false
+  }
+}
+
+function buildTaskPayload(row: any) {
+  if (row?.keyId) {
+    return { keyId: row.keyId }
+  }
+  return {
+    id: row?.id,
+    jobName: row?.jobName,
+    jobGroup: row?.jobGroup,
+  }
+}
+
+function pause(row: any) {
+  ElMessageBox.confirm(`确定暂停任务 ${row.jobName} 吗?`, '系统提示', {
+    confirmButtonText: '提交',
+    cancelButtonText: '取消',
+    type: 'warning',
+  })
+    .then(async () => {
+      await summaryRemindTaskPause(buildTaskPayload(row))
+      ElMessage.success('暂停成功')
+      load()
+    })
+    .catch(() => undefined)
+}
+
+function resume(row: any) {
+  ElMessageBox.confirm(`确定恢复任务 ${row.jobName} 吗?`, '系统提示', {
+    confirmButtonText: '提交',
+    cancelButtonText: '取消',
+    type: 'warning',
+  })
+    .then(async () => {
+      await summaryRemindTaskResume(buildTaskPayload(row))
+      ElMessage.success('恢复成功')
+      load()
+    })
+    .catch(() => undefined)
+}
+
+function trigger(row: any) {
+  ElMessageBox.confirm(`确定立即执行任务 ${row.jobName} 吗?`, '系统提示', {
+    confirmButtonText: '提交',
+    cancelButtonText: '取消',
+    type: 'warning',
+  })
+    .then(async () => {
+      await summaryRemindTaskTrigger(buildTaskPayload(row))
+      ElMessage.success('执行成功')
+    })
+    .catch(() => undefined)
+}
+
+function remove(row: any) {
+  ElMessageBox.confirm(`确定删除任务 ${row.jobName} 吗?`, '系统提示', {
+    confirmButtonText: '提交',
+    cancelButtonText: '取消',
+    type: 'warning',
+  })
+    .then(async () => {
+      await summaryRemindTaskDelete(buildTaskPayload(row))
+      ElMessage.success('删除成功')
+      load()
+    })
+    .catch(() => undefined)
+}
+
+load()
 </script>
+
+<style scoped lang="scss">
+.summary-remind-page {
+  .page-header {
+    display: flex;
+    align-items: flex-start;
+    justify-content: space-between;
+    gap: 16px;
+  }
+
+  .page-title {
+    font-size: 18px;
+    font-weight: 600;
+    color: #111827;
+  }
+
+  .page-subtitle {
+    margin-top: 4px;
+    font-size: 13px;
+    color: #6b7280;
+  }
+
+  .page-actions {
+    display: flex;
+    gap: 12px;
+  }
+
+  .pager-wrap {
+    display: flex;
+    justify-content: flex-end;
+    margin-top: 16px;
+  }
+
+  .cron-link {
+    display: inline-block;
+    margin-top: 6px;
+    color: #409eff;
+    text-decoration: none;
+  }
+}
+</style>
+

+ 154 - 2
ui/sp-user-center/src/modules/otr/summary/views/Settings.vue

@@ -1,7 +1,159 @@
 <template>
-  <otr-simple-form-scene title="简报设置" />
+  <div class="frame-body adaption-frame-body summary-settings-page">
+    <el-card shadow="never">
+      <template #header>
+        <div class="page-header">
+          <div>
+            <div class="page-title">简报设置</div>
+            <div class="page-subtitle">维护各类简报提醒场景的人员名单,保留旧版页面的核心配置能力。</div>
+          </div>
+          <el-button :loading="loading" @click="load">刷新</el-button>
+        </div>
+      </template>
+
+      <el-table v-loading="loading" :data="rows" border>
+        <el-table-column prop="settingType" label="设置属性" min-width="220" />
+        <el-table-column label="人员名单" min-width="360">
+          <template #default="{ row }">
+            <div v-if="normalizeUsers(row).length" class="user-tags">
+              <el-tag v-for="user in normalizeUsers(row)" :key="user.id || user.realName" type="info" effect="plain">
+                {{ user.realName || user.name || '-' }}
+              </el-tag>
+            </div>
+            <span v-else class="empty-text">暂无人员</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="220" align="center">
+          <template #default="{ row }">
+            <el-button link type="primary" @click="openEdit(row)">编辑</el-button>
+            <el-button link type="danger" @click="remove(row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <el-empty v-if="!rows.length && !loading" description="暂无设置" />
+    </el-card>
+
+    <el-dialog v-model="dialogVisible" :title="`选择${currentSettingLabel}人员`" width="720px">
+      <el-form label-width="90px">
+        <el-form-item label="选择人员">
+          <SelectUser v-model="selectedUserIds" multiple placeholder="请选择人员" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submit">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
 </template>
 
 <script setup lang="ts">
-import OtrSimpleFormScene from '@/modules/otr/_shared/components/OtrSimpleFormScene.vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { computed, ref } from 'vue'
+import SelectUser from '@/components/SelectUser/SelectUser.vue'
+import { summarySettingDelete, summarySettingEdit, summarySettingList } from '@/api/otr/summary/core'
+
+const loading = ref(false)
+const submitting = ref(false)
+const dialogVisible = ref(false)
+const rows = ref<any[]>([])
+const currentSetting = ref<any>(null)
+const selectedUserIds = ref<Array<string | number>>([])
+
+const currentSettingLabel = computed(() => currentSetting.value?.settingType || '设置')
+
+function normalizeUsers(row: any) {
+  const users = row?.summarySettingVoList || row?.userList || row?.users || []
+  return Array.isArray(users) ? users : []
+}
+
+function extractIds(row: any) {
+  if (Array.isArray(row?.summarySettingIds)) return row.summarySettingIds
+  const users = normalizeUsers(row)
+  return users.map((item: any) => item.id).filter(Boolean)
+}
+
+async function load() {
+  loading.value = true
+  try {
+    const res: any = await summarySettingList()
+    const result = res?.result
+    rows.value = Array.isArray(result) ? result : result?.records || result?.list || []
+  } finally {
+    loading.value = false
+  }
+}
+
+function openEdit(row: any) {
+  currentSetting.value = row
+  selectedUserIds.value = extractIds(row)
+  dialogVisible.value = true
+}
+
+async function submit() {
+  if (!currentSetting.value?.settingType) return
+  submitting.value = true
+  try {
+    await summarySettingEdit({
+      settingType: currentSetting.value.settingType,
+      summarySettingIds: selectedUserIds.value,
+    })
+    ElMessage.success('设置成功')
+    dialogVisible.value = false
+    load()
+  } finally {
+    submitting.value = false
+  }
+}
+
+function remove(row: any) {
+  ElMessageBox.confirm(`确定删除${row.settingType}人员吗?`, '系统提示', {
+    confirmButtonText: '提交',
+    cancelButtonText: '取消',
+    type: 'warning',
+  })
+    .then(async () => {
+      await summarySettingDelete({ settingType: row.settingType })
+      ElMessage.success('删除成功')
+      load()
+    })
+    .catch(() => undefined)
+}
+
+load()
 </script>
+
+<style scoped lang="scss">
+.summary-settings-page {
+  .page-header {
+    display: flex;
+    align-items: flex-start;
+    justify-content: space-between;
+    gap: 16px;
+  }
+
+  .page-title {
+    font-size: 18px;
+    font-weight: 600;
+    color: #111827;
+  }
+
+  .page-subtitle {
+    margin-top: 4px;
+    font-size: 13px;
+    color: #6b7280;
+  }
+
+  .user-tags {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+  }
+
+  .empty-text {
+    color: #9ca3af;
+  }
+}
+</style>
+

+ 288 - 2
ui/sp-user-center/src/modules/otr/summary/views/Share.vue

@@ -1,7 +1,293 @@
 <template>
-  <summary-workspace title="心得分享" scene="share" />
+  <div class="frame-body adaption-frame-body summary-share-page">
+    <el-card shadow="never">
+      <template #header>
+        <div class="page-header">
+          <div>
+            <div class="page-title">心得分享</div>
+            <div class="page-subtitle">按旧版页面习惯展示团队心得,保留筛选、置顶和内容维护能力。</div>
+          </div>
+          <div class="page-actions">
+            <el-button @click="reset">重置</el-button>
+            <el-button type="primary" @click="openCreate">新增心得分享</el-button>
+          </div>
+        </div>
+      </template>
+
+      <el-form :inline="true" @submit.prevent class="toolbar">
+        <el-form-item>
+          <el-date-picker
+            v-model="dateRange"
+            type="daterange"
+            value-format="YYYY-MM-DD"
+            start-placeholder="开始时间"
+            end-placeholder="结束时间"
+            @change="searchList"
+          />
+        </el-form-item>
+      </el-form>
+
+      <div v-loading="loading" class="share-list">
+        <el-empty v-if="!rows.length && !loading" description="暂无心得分享" />
+
+        <article v-for="item in rows" :key="item.summaryShareId || item.id" class="share-card">
+          <div class="share-meta">
+            <div class="author-block">
+              <el-avatar :size="36">{{ getAvatarText(item.staffName) }}</el-avatar>
+              <div>
+                <div class="author-row">
+                  <span class="author-name">{{ item.staffName || item.author || '-' }}</span>
+                  <el-tag v-if="item.isTop === 1" size="small" type="warning">置顶</el-tag>
+                </div>
+                <div class="author-time">{{ item.createTime || item.createdAt || '-' }}</div>
+              </div>
+            </div>
+            <div class="meta-actions">
+              <el-button v-if="item.canChoose" link type="warning" @click="toggleTop(item)">
+                {{ item.isTop === 1 ? '取消置顶' : '置顶' }}
+              </el-button>
+              <el-button v-if="item.canUpdate" link type="primary" @click="openEdit(item)">编辑</el-button>
+              <el-button v-if="item.canDelete" link type="danger" @click="remove(item)">删除</el-button>
+            </div>
+          </div>
+
+          <div class="share-content ql-editor" v-html="item.shareContent || item.content || '-'" />
+
+          <div class="share-footer">
+            <span class="like-count">点赞 {{ item.likeCount || item.likes || 0 }}</span>
+            <span class="like-users">{{ formatLikeUsers(item.userList) }}</span>
+          </div>
+        </article>
+      </div>
+    </el-card>
+
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="960px" destroy-on-close>
+      <el-form label-width="90px">
+        <el-form-item label="分享内容">
+          <Editor v-model="form.shareContent" :min-height="220" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="submit">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
 </template>
 
 <script setup lang="ts">
-import SummaryWorkspace from '@/modules/otr/summary/components/SummaryWorkspace.vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { reactive, ref } from 'vue'
+import moment from 'moment'
+import Editor from '@/components/Editor/index.vue'
+import {
+  summaryShareAdd,
+  summaryShareDelete,
+  summaryShareList,
+  summaryShareTop,
+  summaryShareUnTop,
+} from '@/api/otr/summary/core'
+
+const loading = ref(false)
+const submitting = ref(false)
+const dialogVisible = ref(false)
+const dialogTitle = ref('新增心得分享')
+const editingId = ref<string | number | null>(null)
+const rows = ref<any[]>([])
+const dateRange = ref<[string, string] | string[]>([
+  moment().subtract(1, 'month').format('YYYY-MM-DD'),
+  moment().format('YYYY-MM-DD'),
+])
+
+const form = reactive({
+  shareContent: '',
+})
+
+function getAvatarText(name?: string) {
+  return name ? name.slice(-1) : '心'
+}
+
+function formatLikeUsers(users: any[]) {
+  if (!Array.isArray(users) || !users.length) return '暂无点赞'
+  return users.map((item) => item.userName || item.realName || item.name).filter(Boolean).join('、')
+}
+
+async function load() {
+  loading.value = true
+  try {
+    const res: any = await summaryShareList({
+      pageNo: 1,
+      pageSize: 20,
+      startDate: dateRange.value?.[0] || '',
+      endDate: dateRange.value?.[1] || '',
+    })
+    const result = res?.result || {}
+    rows.value = result.records || result.list || []
+  } finally {
+    loading.value = false
+  }
+}
+
+function searchList() {
+  load()
+}
+
+function reset() {
+  dateRange.value = [moment().subtract(1, 'month').format('YYYY-MM-DD'), moment().format('YYYY-MM-DD')]
+  load()
+}
+
+function openCreate() {
+  editingId.value = null
+  dialogTitle.value = '新增心得分享'
+  form.shareContent = ''
+  dialogVisible.value = true
+}
+
+function openEdit(row: any) {
+  editingId.value = row.summaryShareId || row.id
+  dialogTitle.value = '编辑心得分享'
+  form.shareContent = row.shareContent || row.content || ''
+  dialogVisible.value = true
+}
+
+async function submit() {
+  if (!form.shareContent.trim() || form.shareContent === '<p></p>') {
+    ElMessage.warning('心得内容不能为空')
+    return
+  }
+  submitting.value = true
+  try {
+    const payload: Record<string, any> = { shareContent: form.shareContent }
+    if (editingId.value) payload.summaryShareId = editingId.value
+    await summaryShareAdd(payload)
+    ElMessage.success(editingId.value ? '编辑成功' : '新增成功')
+    dialogVisible.value = false
+    load()
+  } finally {
+    submitting.value = false
+  }
+}
+
+async function toggleTop(row: any) {
+  if (row.isTop === 1) {
+    await summaryShareUnTop({ summaryShareId: row.summaryShareId || row.id })
+    ElMessage.success('已取消置顶')
+  } else {
+    await summaryShareTop({ summaryShareId: row.summaryShareId || row.id })
+    ElMessage.success('已置顶')
+  }
+  load()
+}
+
+function remove(row: any) {
+  ElMessageBox.confirm('确定删除这条心得分享吗?', '系统提示', {
+    confirmButtonText: '提交',
+    cancelButtonText: '取消',
+    type: 'warning',
+  })
+    .then(async () => {
+      await summaryShareDelete({ summaryShareId: row.summaryShareId || row.id })
+      ElMessage.success('删除成功')
+      load()
+    })
+    .catch(() => undefined)
+}
+
+load()
 </script>
+
+<style scoped lang="scss">
+.summary-share-page {
+  .page-header {
+    display: flex;
+    align-items: flex-start;
+    justify-content: space-between;
+    gap: 16px;
+  }
+
+  .page-title {
+    font-size: 18px;
+    font-weight: 600;
+    color: #111827;
+  }
+
+  .page-subtitle {
+    margin-top: 4px;
+    font-size: 13px;
+    color: #6b7280;
+  }
+
+  .page-actions {
+    display: flex;
+    gap: 12px;
+  }
+
+  .toolbar {
+    margin-bottom: 16px;
+  }
+
+  .share-list {
+    display: grid;
+    gap: 16px;
+  }
+
+  .share-card {
+    padding: 20px 22px;
+    border: 1px solid #e5e7eb;
+    border-radius: 14px;
+    background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
+  }
+
+  .share-meta {
+    display: flex;
+    align-items: flex-start;
+    justify-content: space-between;
+    gap: 16px;
+  }
+
+  .author-block {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+  }
+
+  .author-row {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+
+  .author-name {
+    font-size: 15px;
+    font-weight: 600;
+    color: #111827;
+  }
+
+  .author-time {
+    margin-top: 4px;
+    color: #6b7280;
+    font-size: 12px;
+  }
+
+  .meta-actions {
+    display: flex;
+    gap: 8px;
+  }
+
+  .share-content {
+    margin-top: 16px;
+    padding: 0;
+    color: #374151;
+  }
+
+  .share-footer {
+    margin-top: 16px;
+    display: flex;
+    gap: 16px;
+    color: #6b7280;
+    font-size: 13px;
+  }
+}
+</style>
+

+ 8 - 2
ui/sp-user-center/src/modules/otr/task/views/AtList.vue

@@ -1,7 +1,13 @@
 <template>
-  <task-workspace title="@我的任务" scene="at-list" />
+  <task-list-panel
+    scene="at-list"
+    :allow-batch-delete="true"
+    switch-route="/otr/task/list"
+    default-range="empty"
+  />
 </template>
 
 <script setup lang="ts">
-import TaskWorkspace from '@/modules/otr/task/components/TaskWorkspace.vue'
+import TaskListPanel from '@/modules/otr/task/components/TaskListPanel.vue'
 </script>
+

+ 422 - 75
ui/sp-user-center/src/modules/otr/task/views/Detail.vue

@@ -1,89 +1,164 @@
 <template>
-  <el-card shadow="never">
-    <template #header>
-      <div class="header">
-        <span>任务详情</span>
-        <div>
-          <el-input v-model="taskIdInput" placeholder="任务ID" style="width: 140px; margin-right: 8px" />
-          <el-button type="primary" @click="load">查询</el-button>
+  <div class="task-detail-page frame-body adaption-frame-body">
+    <div class="task-detail-card">
+      <div class="task-detail-header">
+        <div class="task-detail-header-left">
+          <el-button text @click="goBack">返回</el-button>
+          <div>
+            <div class="task-detail-title">{{ detail.title || detail.name || '任务详情' }}</div>
+            <div class="task-detail-subtitle">任务ID: {{ taskIdInput || '-' }}</div>
+          </div>
+        </div>
+        <div class="task-detail-header-right">
+          <el-input v-model="taskIdInput" placeholder="任务ID" style="width: 160px" />
+          <el-button type="primary" :loading="loading" @click="load">刷新</el-button>
         </div>
       </div>
-    </template>
-
-    <el-descriptions v-loading="loading" :column="2" border>
-      <el-descriptions-item label="任务ID">{{ detail.id || '-' }}</el-descriptions-item>
-      <el-descriptions-item label="任务标题">{{ detail.title || detail.name || '-' }}</el-descriptions-item>
-      <el-descriptions-item label="负责人">{{ detail.leaderName || detail.ownerName || '-' }}</el-descriptions-item>
-      <el-descriptions-item label="状态">{{ detail.statusLabel || '-' }}</el-descriptions-item>
-      <el-descriptions-item label="开始时间">{{ detail.startTime || '-' }}</el-descriptions-item>
-      <el-descriptions-item label="结束时间">{{ detail.endTime || '-' }}</el-descriptions-item>
-      <el-descriptions-item label="内容" :span="2">{{ detail.content || '-' }}</el-descriptions-item>
-    </el-descriptions>
-
-    <el-tabs v-model="activeTab" style="margin-top: 16px">
-      <el-tab-pane label="日志记录" name="log">
-        <el-timeline>
-          <el-timeline-item v-for="(item, idx) in logs" :key="`l-${idx}`" :timestamp="item.createTime || '-'">
-            <div>{{ item.content || '-' }}</div>
-          </el-timeline-item>
-        </el-timeline>
-      </el-tab-pane>
-
-      <el-tab-pane label="评论" name="comment">
-        <div class="comment-editor">
-          <el-input v-model="commentContent" type="textarea" :rows="3" placeholder="输入评论内容" />
-          <div style="margin-top: 8px">
-            <el-button type="primary" :loading="commentSubmitting" @click="submitComment">发表评论</el-button>
+
+      <div class="task-detail-content" v-loading="loading">
+        <div class="task-detail-main">
+          <div class="panel hero-panel">
+            <div class="hero-top">
+              <div class="hero-status">
+                <span class="status-chip">{{ detail.statusLabel || formatStatus(detail.status) }}</span>
+                <span class="progress-text">进度 {{ formatPercent(detail.percent || detail.progress) }}</span>
+              </div>
+              <el-progress :percentage="toNumber(detail.percent || detail.progress)" :stroke-width="14" color="#2d8cf0" />
+            </div>
+            <div class="hero-grid">
+              <div class="hero-item">
+                <span class="hero-label">执行人</span>
+                <span class="hero-value">{{ detail.leaderName || detail.executeName || detail.ownerName || '-' }}</span>
+              </div>
+              <div class="hero-item">
+                <span class="hero-label">创建人</span>
+                <span class="hero-value">{{ detail.createName || detail.fromName || '-' }}</span>
+              </div>
+              <div class="hero-item">
+                <span class="hero-label">开始时间</span>
+                <span class="hero-value">{{ detail.startTime || detail.startDate || '-' }}</span>
+              </div>
+              <div class="hero-item">
+                <span class="hero-label">结束时间</span>
+                <span class="hero-value">{{ detail.endTime || detail.endDate || '-' }}</span>
+              </div>
+            </div>
+          </div>
+
+          <div class="panel">
+            <div class="panel-title">任务说明</div>
+            <div class="rich-content" v-html="detail.content || '<p>-</p>'"></div>
+          </div>
+
+          <div class="panel">
+            <div class="panel-title">参与人</div>
+            <div v-if="participantUsers.length" class="user-list">
+              <div v-for="(user, idx) in participantUsers" :key="idx" class="user-chip">
+                <el-avatar :size="28">{{ getAvatarText(user.staffName || user.realName || user.name) }}</el-avatar>
+                <span>{{ user.staffName || user.realName || user.name || '-' }}</span>
+              </div>
+            </div>
+            <el-empty v-else description="暂无参与人" />
           </div>
         </div>
-        <el-empty v-if="!comments.length" description="暂无评论" />
-        <el-card v-for="(item, idx) in comments" :key="`c-${idx}`" class="comment-item" shadow="never">
-          <div class="comment-meta">
-            <span>{{ item.staffName || '匿名' }}</span>
-            <span>{{ item.createTime || '-' }}</span>
+
+        <div class="task-detail-side">
+          <div class="panel">
+            <div class="panel-title">基础信息</div>
+            <el-descriptions :column="1" border>
+              <el-descriptions-item label="任务标题">{{ detail.title || detail.name || '-' }}</el-descriptions-item>
+              <el-descriptions-item label="优先级">{{ formatPriority(detail.priorityType) }}</el-descriptions-item>
+              <el-descriptions-item label="任务日期">{{ detail.dateName || detail.deadline || '-' }}</el-descriptions-item>
+              <el-descriptions-item label="提醒">{{ formatRemind(detail.remindType) }}</el-descriptions-item>
+            </el-descriptions>
+          </div>
+
+          <div class="panel">
+            <div class="panel-title">附件</div>
+            <el-empty v-if="!resources.length" description="暂无附件" />
+            <div v-else class="resource-list">
+              <div v-for="(item, idx) in resources" :key="idx" class="resource-item">
+                <div class="resource-name">{{ item.fileName || item.resourceName || '-' }}</div>
+                <div class="resource-meta">{{ item.fileSize || item.resourceSize || '-' }} · {{ item.createTime || '-' }}</div>
+              </div>
+            </div>
           </div>
-          <div class="comment-content">{{ item.content || '-' }}</div>
-        </el-card>
-      </el-tab-pane>
-
-      <el-tab-pane label="附件" name="resource">
-        <el-table :data="resources" border>
-          <el-table-column prop="fileName" label="文件名" min-width="220" />
-          <el-table-column prop="fileSize" label="大小" width="120" />
-          <el-table-column prop="createTime" label="上传时间" width="180" />
-        </el-table>
-      </el-tab-pane>
-    </el-tabs>
-  </el-card>
+        </div>
+      </div>
+
+      <div class="task-detail-tabs">
+        <el-tabs v-model="activeTab">
+          <el-tab-pane label="评论" name="comment">
+            <div class="comment-editor">
+              <Editor v-model="commentContent" :min-height="120" />
+              <div class="comment-actions">
+                <el-button type="primary" :loading="commentSubmitting" @click="submitComment">发表评论</el-button>
+              </div>
+            </div>
+            <el-empty v-if="!comments.length" description="暂无评论" />
+            <div v-else class="comment-list">
+              <el-card v-for="(item, idx) in comments" :key="`c-${idx}`" class="comment-item" shadow="never">
+                <div class="comment-meta">
+                  <div class="comment-author">
+                    <el-avatar :size="28">{{ getAvatarText(item.staffName) }}</el-avatar>
+                    <span>{{ item.staffName || '匿名' }}</span>
+                  </div>
+                  <span>{{ item.createTime || '-' }}</span>
+                </div>
+                <div class="comment-content" v-html="item.content || '-'"></div>
+              </el-card>
+            </div>
+          </el-tab-pane>
+
+          <el-tab-pane label="日志记录" name="log">
+            <el-timeline>
+              <el-timeline-item v-for="(item, idx) in logs" :key="`l-${idx}`" :timestamp="item.createTime || '-'">
+                <div>{{ item.content || '-' }}</div>
+              </el-timeline-item>
+            </el-timeline>
+            <el-empty v-if="!logs.length" description="暂无日志" />
+          </el-tab-pane>
+        </el-tabs>
+      </div>
+    </div>
+  </div>
 </template>
 
 <script setup lang="ts">
 import { ElMessage } from 'element-plus'
-import { onMounted, reactive, ref } from 'vue'
-import { useRoute } from 'vue-router'
+import { computed, onMounted, reactive, ref } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import Editor from '@/components/Editor/index.vue'
 import { taskCommentAdd, taskCommentList, taskGet, taskLogList, taskResourceList } from '@/api/otr/task/core'
 
 const route = useRoute()
+const router = useRouter()
 const loading = ref(false)
 const taskIdInput = ref(String(route.query.id || '1'))
 const detail = reactive<Record<string, any>>({})
-const activeTab = ref('log')
+const activeTab = ref('comment')
 const logs = ref<any[]>([])
 const comments = ref<any[]>([])
 const resources = ref<any[]>([])
 const commentContent = ref('')
 const commentSubmitting = ref(false)
 
+const participantUsers = computed(() => {
+  const list = detail.participants || detail.participantList || []
+  return Array.isArray(list) ? list : []
+})
+
 async function load() {
   loading.value = true
   try {
     const id = taskIdInput.value || '1'
     const [res, logRes, commentRes, resourceRes]: any = await Promise.all([
       taskGet(id),
-      taskLogList({ taskId: id, page: 1, psize: 20 }),
-      taskCommentList({ taskId: id, page: 1, pageSize: 20 }),
-      taskResourceList({ taskId: id }),
+      taskLogList({ taskId: id, taskDateId: id, page: 1, psize: 20, pageSize: 20 }),
+      taskCommentList({ taskId: id, taskDateId: id, page: 1, pageSize: 20 }),
+      taskResourceList({ taskId: id, taskDateId: id }),
     ])
+    Object.keys(detail).forEach((key) => delete detail[key])
     Object.assign(detail, res?.result || {})
     logs.value = logRes?.result?.records || []
     comments.value = commentRes?.result?.records || []
@@ -94,7 +169,7 @@ async function load() {
 }
 
 async function submitComment() {
-  if (!commentContent.value.trim()) {
+  if (!commentContent.value.replace(/<[^>]+>/g, '').trim()) {
     ElMessage.warning('请先输入评论内容')
     return
   }
@@ -102,36 +177,308 @@ async function submitComment() {
   try {
     await taskCommentAdd({
       taskId: taskIdInput.value || '1',
+      taskDateId: taskIdInput.value || '1',
       content: commentContent.value,
     })
     ElMessage.success('评论已提交')
     commentContent.value = ''
-    const commentRes: any = await taskCommentList({ taskId: taskIdInput.value || '1', page: 1, pageSize: 20 })
+    const commentRes: any = await taskCommentList({ taskId: taskIdInput.value || '1', taskDateId: taskIdInput.value || '1', page: 1, pageSize: 20 })
     comments.value = commentRes?.result?.records || []
   } finally {
     commentSubmitting.value = false
   }
 }
 
-onMounted(load)
-</script>
+function goBack() {
+  router.back()
+}
+
+function toNumber(value: any) {
+  const raw = String(value ?? '0').replace('%', '')
+  const num = Number(raw)
+  return Number.isNaN(num) ? 0 : Math.max(0, Math.min(100, num))
+}
+
+function formatPercent(value: any) {
+  return `${toNumber(value)}%`
+}
+
+function formatStatus(status: any) {
+  switch (Number(status)) {
+    case 1:
+      return '未开始'
+    case 2:
+      return '进行中'
+    case 3:
+    case 4:
+      return '已完成'
+    case 5:
+      return '已逾期'
+    default:
+      return '-'
+  }
+}
 
-<style scoped>
-.header {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
+function formatPriority(value: any) {
+  switch (Number(value)) {
+    case 1:
+      return '重要紧急'
+    case 2:
+      return '重要不紧急'
+    case 3:
+      return '不重要紧急'
+    case 4:
+      return '不重要不紧急'
+    default:
+      return '-'
+  }
 }
-.comment-item {
-  margin-top: 10px;
+
+function formatRemind(value: any) {
+  const num = Number(value)
+  if (num === -1 || Number.isNaN(num)) return '不提醒'
+  if (num === 5) return '开始前5分钟'
+  if (num === 30) return '开始前30分钟'
+  if (num === 60) return '开始前1小时'
+  if (num === 1440) return '开始前1天'
+  return `${value}`
 }
-.comment-meta {
-  display: flex;
-  justify-content: space-between;
-  color: var(--el-text-color-secondary);
-  font-size: 12px;
+
+function getAvatarText(name?: string) {
+  return name ? String(name).slice(-1) : '人'
 }
-.comment-content {
-  margin-top: 6px;
+
+onMounted(load)
+</script>
+
+<style scoped lang="scss">
+.task-detail-page {
+  .task-detail-card {
+    background: #fff;
+    border-radius: 10px;
+    padding: 24px;
+  }
+
+  .task-detail-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 16px;
+    padding-bottom: 20px;
+    margin-bottom: 20px;
+    border-bottom: 1px solid #eef2f6;
+  }
+
+  .task-detail-header-left {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+  }
+
+  .task-detail-header-right {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+  }
+
+  .task-detail-title {
+    font-size: 22px;
+    font-weight: 600;
+    color: #111827;
+  }
+
+  .task-detail-subtitle {
+    margin-top: 4px;
+    color: #6b7280;
+    font-size: 13px;
+  }
+
+  .task-detail-content {
+    display: grid;
+    grid-template-columns: minmax(0, 2fr) minmax(300px, 1fr);
+    gap: 20px;
+  }
+
+  .panel {
+    padding: 20px;
+    border: 1px solid #e5e7eb;
+    border-radius: 10px;
+    background: #fff;
+  }
+
+  .hero-panel {
+    background: linear-gradient(135deg, #f8fbff 0%, #eef6ff 100%);
+  }
+
+  .hero-top {
+    margin-bottom: 18px;
+  }
+
+  .hero-status {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 10px;
+  }
+
+  .status-chip {
+    display: inline-flex;
+    align-items: center;
+    height: 28px;
+    padding: 0 12px;
+    color: #2563eb;
+    background: rgba(37, 99, 235, 0.1);
+    border-radius: 999px;
+    font-size: 13px;
+    font-weight: 600;
+  }
+
+  .progress-text {
+    color: #4b5563;
+    font-size: 13px;
+  }
+
+  .hero-grid {
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    gap: 14px 18px;
+  }
+
+  .hero-item {
+    display: flex;
+    flex-direction: column;
+    gap: 4px;
+  }
+
+  .hero-label {
+    color: #6b7280;
+    font-size: 12px;
+  }
+
+  .hero-value {
+    color: #111827;
+    font-size: 14px;
+    font-weight: 500;
+  }
+
+  .panel-title {
+    margin-bottom: 16px;
+    font-size: 16px;
+    font-weight: 600;
+    color: #111827;
+  }
+
+  .rich-content {
+    min-height: 160px;
+    color: #374151;
+    line-height: 1.8;
+  }
+
+  .user-list {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 12px;
+  }
+
+  .user-chip {
+    display: inline-flex;
+    align-items: center;
+    gap: 8px;
+    padding: 8px 12px;
+    border-radius: 999px;
+    background: #f3f4f6;
+    color: #374151;
+  }
+
+  .resource-list {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  }
+
+  .resource-item {
+    padding: 12px 14px;
+    border-radius: 8px;
+    background: #f8fafc;
+    border: 1px solid #edf2f7;
+  }
+
+  .resource-name {
+    color: #111827;
+    font-weight: 500;
+  }
+
+  .resource-meta {
+    margin-top: 4px;
+    color: #6b7280;
+    font-size: 12px;
+  }
+
+  .task-detail-tabs {
+    margin-top: 20px;
+  }
+
+  .comment-editor {
+    margin-bottom: 18px;
+  }
+
+  .comment-actions {
+    display: flex;
+    justify-content: flex-end;
+    margin-top: 12px;
+  }
+
+  .comment-list {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  }
+
+  .comment-item {
+    border-radius: 10px;
+  }
+
+  .comment-meta {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    color: #6b7280;
+    font-size: 12px;
+  }
+
+  .comment-author {
+    display: inline-flex;
+    align-items: center;
+    gap: 8px;
+    color: #111827;
+    font-size: 13px;
+    font-weight: 500;
+  }
+
+  .comment-content {
+    margin-top: 12px;
+    color: #374151;
+    line-height: 1.8;
+  }
+
+  @media (max-width: 1100px) {
+    .task-detail-header {
+      flex-direction: column;
+      align-items: flex-start;
+    }
+
+    .task-detail-header-right {
+      width: 100%;
+    }
+
+    .task-detail-content {
+      grid-template-columns: 1fr;
+    }
+
+    .hero-grid {
+      grid-template-columns: 1fr;
+    }
+  }
 }
 </style>
+

+ 265 - 54
ui/sp-user-center/src/modules/otr/task/views/Form.vue

@@ -1,71 +1,210 @@
 <template>
-  <el-card shadow="never">
-    <template #header>
-      <div class="header">新建任务</div>
-    </template>
-    <el-form label-width="100px" style="max-width: 760px">
-      <el-form-item label="任务标题" required>
-        <el-input v-model="form.title" placeholder="请输入任务标题" />
-      </el-form-item>
-      <el-form-item label="任务内容">
-        <el-input v-model="form.content" type="textarea" :rows="4" placeholder="请输入任务内容" />
-      </el-form-item>
-      <el-form-item label="负责人">
-        <el-input v-model="form.leaderName" placeholder="负责人(示例)" />
-      </el-form-item>
-      <el-form-item label="优先级">
-        <el-select v-model="form.priorityType" style="width: 180px">
-          <el-option label="高" :value="1" />
-          <el-option label="中" :value="2" />
-          <el-option label="低" :value="3" />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="计划时间">
-        <el-date-picker
-          v-model="range"
-          type="datetimerange"
-          start-placeholder="开始时间"
-          end-placeholder="结束时间"
-          value-format="YYYY-MM-DD HH:mm:ss"
-          style="width: 360px"
-        />
-      </el-form-item>
-      <el-form-item>
-        <el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
-        <el-button @click="goBack">返回</el-button>
-      </el-form-item>
-    </el-form>
-  </el-card>
+  <div class="task-form-page frame-body adaption-frame-body">
+    <div class="task-form-card">
+      <div class="task-form-header">
+        <div>
+          <div class="task-form-title">新建任务</div>
+          <div class="task-form-subtitle">补齐执行人、参与人、时间、优先级、附件和任务说明。</div>
+        </div>
+        <div class="task-form-actions">
+          <el-button @click="goBack">返回</el-button>
+          <el-button type="primary" :loading="submitting" @click="submit">保存任务</el-button>
+        </div>
+      </div>
+
+      <div class="task-form-content">
+        <div class="task-form-main">
+          <div class="panel">
+            <div class="panel-title">任务内容</div>
+            <el-form ref="formRef" :model="form" :rules="rules" label-position="top">
+              <el-form-item label="任务标题" prop="title">
+                <el-input v-model="form.title" maxlength="120" show-word-limit placeholder="请输入任务标题" />
+              </el-form-item>
+              <el-form-item label="任务说明" prop="content">
+                <Editor v-model="form.content" :min-height="260" />
+              </el-form-item>
+              <el-form-item label="上传附件">
+                <Upload v-model="resourceIdList" :resource-list="resourceList" :is-tips="false" />
+              </el-form-item>
+            </el-form>
+          </div>
+        </div>
+
+        <div class="task-form-side">
+          <div class="panel">
+            <div class="panel-title">任务属性</div>
+            <el-form ref="metaFormRef" :model="form" :rules="rules" label-position="top">
+              <el-form-item label="执行人" prop="executeId">
+                <SelectUser v-model="form.executeId" placeholder="请选择执行人" />
+              </el-form-item>
+              <el-form-item label="参与人">
+                <SelectUser v-model="participantIds" multiple placeholder="请选择参与人" />
+              </el-form-item>
+              <el-form-item label="日期范围" prop="dateRange">
+                <el-date-picker
+                  v-model="dateRange"
+                  type="daterange"
+                  value-format="YYYY-MM-DD"
+                  start-placeholder="开始日期"
+                  end-placeholder="结束日期"
+                  style="width: 100%"
+                  @change="syncDateFields"
+                />
+              </el-form-item>
+              <el-form-item label="时间范围" prop="timeRange">
+                <el-time-picker
+                  v-model="timeRange"
+                  is-range
+                  value-format="HH:mm"
+                  range-separator="-"
+                  start-placeholder="开始时间"
+                  end-placeholder="结束时间"
+                  style="width: 100%"
+                  @change="syncTimeFields"
+                />
+              </el-form-item>
+              <el-form-item label="优先级" prop="priorityType">
+                <el-select v-model="form.priorityType" style="width: 100%">
+                  <el-option label="重要紧急" :value="1" />
+                  <el-option label="重要不紧急" :value="2" />
+                  <el-option label="不重要紧急" :value="3" />
+                  <el-option label="不重要不紧急" :value="4" />
+                </el-select>
+              </el-form-item>
+              <el-form-item label="提醒时间">
+                <el-select v-model="form.remindType" clearable style="width: 100%">
+                  <el-option label="不提醒" :value="-1" />
+                  <el-option label="开始前5分钟" :value="5" />
+                  <el-option label="开始前30分钟" :value="30" />
+                  <el-option label="开始前1小时" :value="60" />
+                  <el-option label="开始前1天" :value="1440" />
+                </el-select>
+              </el-form-item>
+              <el-form-item label="父任务ID">
+                <el-input v-model="form.parentId" placeholder="如为子任务可自动带入" disabled />
+              </el-form-item>
+            </el-form>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
 </template>
 
 <script setup lang="ts">
 import { ElMessage } from 'element-plus'
-import { ref } from 'vue'
-import { useRouter } from 'vue-router'
+import { computed, reactive, ref } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import Editor from '@/components/Editor/index.vue'
+import SelectUser from '@/components/SelectUser/SelectUser.vue'
+import Upload from '@/components/FileUpload/upload.vue'
 import { taskAdd } from '@/api/otr/task/core'
 
 const router = useRouter()
+const route = useRoute()
 const submitting = ref(false)
-const range = ref<string[]>([])
-const form = ref({
+const formRef = ref<ElFormInstance>()
+const metaFormRef = ref<ElFormInstance>()
+const resourceIdList = ref<Array<string | number>>([])
+const resourceList = ref<any[]>([])
+const participantIds = ref<Array<string | number>>([])
+const dateRange = ref<string[]>([])
+const timeRange = ref<string[]>([])
+const currentUser = readUserInfo()
+
+const form = reactive({
   title: '',
   content: '',
-  leaderName: '',
-  priorityType: 2,
+  executeId: currentUser.id || currentUser.userId || undefined,
+  participantId: '',
+  startDate: '',
+  endDate: '',
+  startTime: '09:00',
+  endTime: '18:00',
+  dateName: '',
+  dateType: '7',
+  recycleContent: '',
+  recycleType: null as string | number | null,
+  recycleInterval: null as string | number | null,
+  priorityType: 4,
+  remindType: -1,
+  parentId: String(route.query.ptaskId || ''),
+  resourceIdList: [] as Array<string | number>,
 })
 
-async function submit() {
-  if (!form.value.title) {
-    ElMessage.warning('请先填写任务标题')
+const rules = reactive({
+  title: [{ required: true, message: '请填写任务标题', trigger: 'blur' }],
+  content: [{ required: true, message: '请填写任务说明', trigger: 'blur' }],
+  executeId: [{ required: true, message: '请选择执行人', trigger: 'change' }],
+  dateRange: [{ validator: validateDateRange, trigger: 'change' }],
+  timeRange: [{ validator: validateTimeRange, trigger: 'change' }],
+})
+
+function readUserInfo() {
+  try {
+    return JSON.parse(localStorage.getItem('userInfo') || '{}')
+  } catch {
+    return {}
+  }
+}
+
+function validateDateRange(_: any, __: any, callback: (error?: Error) => void) {
+  if (!dateRange.value?.length || !dateRange.value[0] || !dateRange.value[1]) {
+    callback(new Error('请选择日期范围'))
     return
   }
+  callback()
+}
+
+function validateTimeRange(_: any, __: any, callback: (error?: Error) => void) {
+  if (!timeRange.value?.length || !timeRange.value[0] || !timeRange.value[1]) {
+    callback(new Error('请选择时间范围'))
+    return
+  }
+  callback()
+}
+
+function syncDateFields() {
+  form.startDate = dateRange.value?.[0] || ''
+  form.endDate = dateRange.value?.[1] || ''
+  form.dateName = form.startDate && form.endDate ? `${form.startDate}~${form.endDate}` : ''
+  form.recycleContent = form.dateName
+}
+
+function syncTimeFields() {
+  form.startTime = timeRange.value?.[0] || ''
+  form.endTime = timeRange.value?.[1] || ''
+}
+
+const submitPayload = computed(() => ({
+  title: form.title,
+  content: form.content,
+  executeId: form.executeId,
+  participantId: participantIds.value.join(','),
+  startDate: form.startDate,
+  endDate: form.endDate,
+  startTime: form.startTime,
+  endTime: form.endTime,
+  dateName: form.dateName,
+  dateType: form.dateType,
+  recycleContent: form.recycleContent,
+  recycleType: form.recycleType,
+  recycleInterval: form.recycleInterval,
+  priorityType: form.priorityType,
+  remindType: form.remindType,
+  parentId: form.parentId || null,
+  resourceIdList: resourceIdList.value,
+  leaderId: form.executeId,
+}))
+
+async function submit() {
+  const validMain = await formRef.value?.validate().catch(() => false)
+  const validMeta = await metaFormRef.value?.validate().catch(() => false)
+  if (!validMain || !validMeta) return
+
   submitting.value = true
   try {
-    await taskAdd({
-      ...form.value,
-      startTime: range.value?.[0] || '',
-      endTime: range.value?.[1] || '',
-    })
+    await taskAdd(submitPayload.value)
     ElMessage.success('任务已保存')
     router.push('/otr/task/list')
   } finally {
@@ -76,10 +215,82 @@ async function submit() {
 function goBack() {
   router.back()
 }
+
+dateRange.value = []
+timeRange.value = ['09:00', '18:00']
+syncDateFields()
+syncTimeFields()
 </script>
 
-<style scoped>
-.header {
-  font-weight: 600;
+<style scoped lang="scss">
+.task-form-page {
+  .task-form-card {
+    background: #fff;
+    border-radius: 10px;
+    padding: 24px;
+  }
+
+  .task-form-header {
+    display: flex;
+    align-items: flex-start;
+    justify-content: space-between;
+    gap: 16px;
+    margin-bottom: 24px;
+    padding-bottom: 20px;
+    border-bottom: 1px solid #eef2f6;
+  }
+
+  .task-form-title {
+    font-size: 22px;
+    font-weight: 600;
+    color: #1f2937;
+  }
+
+  .task-form-subtitle {
+    margin-top: 6px;
+    color: #6b7280;
+    font-size: 13px;
+  }
+
+  .task-form-actions {
+    display: flex;
+    gap: 12px;
+  }
+
+  .task-form-content {
+    display: grid;
+    grid-template-columns: minmax(0, 2fr) minmax(320px, 1fr);
+    gap: 20px;
+  }
+
+  .panel {
+    padding: 20px;
+    border: 1px solid #e5e7eb;
+    border-radius: 10px;
+    background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
+  }
+
+  .panel-title {
+    margin-bottom: 16px;
+    font-size: 16px;
+    font-weight: 600;
+    color: #111827;
+  }
+
+  :deep(.el-form-item__label) {
+    color: #374151;
+    font-weight: 500;
+  }
+
+  @media (max-width: 1100px) {
+    .task-form-content {
+      grid-template-columns: 1fr;
+    }
+
+    .task-form-header {
+      flex-direction: column;
+    }
+  }
 }
 </style>
+

+ 1328 - 2
ui/sp-user-center/src/modules/otr/task/views/List.vue

@@ -1,7 +1,1333 @@
 <template>
-  <task-workspace title="任务管理" scene="list" />
+  <div class="frame-body adaption-frame-body task-page">
+    <div class="frame-card">
+      <div class="frame-card-header border-radius" :style="{ minHeight: viewType === 1 ? '110px' : '56px' }">
+        <div class="frame-search">
+          <el-form :inline="true" :model="search" class="demo-form-inline">
+            <div class="search-line">
+              <div class="item2">
+                <el-form-item>
+                  <el-input
+                    v-model="kws"
+                    placeholder="请输入关键字"
+                    clearable
+                    @keyup.enter="keydownSearch"
+                    @blur="keydownSearch"
+                  />
+                </el-form-item>
+                <el-form-item>
+                  <el-select v-model="search.searchType" placeholder="全部状态" clearable @change="onSearch">
+                    <el-option v-for="item in searchTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item>
+                  <el-select
+                    v-model="search.status"
+                    multiple
+                    collapse-tags
+                    collapse-tags-tooltip
+                    placeholder="全部状态"
+                    style="width: 160px"
+                    @change="onSearch"
+                  >
+                    <el-option label="未开始" value="1" />
+                    <el-option label="进行中" value="2" />
+                    <el-option label="已完成" value="3" />
+                    <el-option label="已逾期" value="5" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item v-if="viewType === 3">
+                  <el-date-picker
+                    v-model="dt2"
+                    type="daterange"
+                    value-format="YYYY-MM-DD"
+                    start-placeholder="开始时间"
+                    end-placeholder="结束时间"
+                    :clearable="false"
+                    @change="handleChangeTime"
+                  />
+                </el-form-item>
+                <el-form-item v-if="viewType === 1">
+                  <el-date-picker
+                    v-if="toggleActive === 'day'"
+                    v-model="dt"
+                    type="date"
+                    value-format="YYYY-MM-DD"
+                    placeholder="选择日期"
+                    :clearable="false"
+                    @change="handleChangeTime"
+                  />
+                  <el-date-picker
+                    v-else-if="toggleActive === 'week'"
+                    v-model="dt"
+                    type="week"
+                    format="YYYY 第 WW 周"
+                    value-format="YYYY-MM-DD"
+                    placeholder="选择日期"
+                    :clearable="false"
+                    @change="handleChangeTime"
+                  />
+                  <el-date-picker
+                    v-else
+                    v-model="dt"
+                    type="month"
+                    value-format="YYYY-MM"
+                    placeholder="选择日期"
+                    :clearable="false"
+                    @change="handleChangeTime"
+                  />
+                </el-form-item>
+                <el-form-item>
+                  <el-button type="primary" @click="reset">重置</el-button>
+                </el-form-item>
+              </div>
+              <div class="item3">
+                <el-form-item>
+                  <span class="search-right">
+                    <el-button
+                      circle
+                      :type="viewType === 3 ? 'primary' : undefined"
+                      style="margin-right: 20px"
+                      @click="setViewType(3)"
+                    >
+                      <el-icon><Tickets /></el-icon>
+                    </el-button>
+                    <el-button
+                      circle
+                      :type="viewType === 1 ? 'primary' : undefined"
+                      style="margin-right: 20px"
+                      @click="setViewType(1)"
+                    >
+                      <el-icon><OfficeBuilding /></el-icon>
+                    </el-button>
+                    <el-button type="primary" style="margin-right: 20px" @click="delTasks">批量删除</el-button>
+                    <el-button type="primary" @click="openDialogForm">新建任务</el-button>
+                  </span>
+                </el-form-item>
+              </div>
+            </div>
+            <div v-if="viewType === 1" class="search-line search-line-toggle">
+              <div class="item1">
+                <el-form-item>
+                  <div class="toggle-group">
+                    <button
+                      v-for="item in toggleData"
+                      :key="item.value"
+                      type="button"
+                      class="toggle-btn"
+                      :class="{ 'is-active': toggleActive === item.value }"
+                      @click="handleChangeType(item.value)"
+                    >
+                      {{ item.label }}
+                    </button>
+                  </div>
+                </el-form-item>
+              </div>
+            </div>
+          </el-form>
+        </div>
+      </div>
+
+      <template v-if="viewType === 1">
+        <div class="frame-card calendar-card">
+          <div class="frame-card-content">
+            <div v-if="toggleActive === 'day'" class="calendar-day-view">
+              <div class="calendar-header">
+                <div class="column-time">周次{{ dayInfo.week }}</div>
+                <div class="column-content">{{ dayInfo.weekdayLabel }} {{ dayInfo.day }}</div>
+              </div>
+              <div class="calendar-content-list">
+                <div class="calendar-item">
+                  <div class="column-time">全天</div>
+                  <div class="column-content"></div>
+                </div>
+                <div v-for="slot in daySlots" :key="slot.time" class="calendar-item">
+                  <div class="column-time">{{ slot.time }}</div>
+                  <div class="column-content">
+                    <div v-if="!slot.data.length" class="calendar-empty">暂无任务</div>
+                    <div v-for="row in slot.data" :key="row.taskDateId || row.id" class="calendar-task-chip" @click="goDetail(row)">
+                      <span class="calendar-task-chip__title" v-html="row.title || row.name"></span>
+                      <span class="calendar-task-chip__meta">{{ formatPercent(row.percent) }}</span>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+            <div v-else-if="toggleActive === 'week'" class="calendar-week-view">
+              <div class="calendar-week-header">
+                <div v-for="item in weekData" :key="item.dayKey" class="column-week">{{ item.day }}({{ item.weekdayLabel }})</div>
+              </div>
+              <div class="calendar-week-body">
+                <div v-for="item in weekData" :key="item.dayKey" class="calendar-week-column">
+                  <div v-if="!item.data.length" class="calendar-empty">暂无任务</div>
+                  <div v-for="row in item.data" :key="row.taskDateId || row.id" class="summary-card" @click="goDetail(row)">
+                    <span class="summary-card__line" :class="`priority-${formatPriority(row.priorityType)}`"></span>
+                    <div class="summary-card__title" v-html="row.title || row.name"></div>
+                    <div class="summary-card__meta">{{ row.dateName || row.deadline || '-' }}</div>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+            <div v-else class="calendar-month-view">
+              <div class="calendar-month-header">
+                <div v-for="label in monthWeekLabels" :key="label" class="column-week">{{ label }}</div>
+              </div>
+              <div class="calendar-month-grid">
+                <div v-for="item in monthData" :key="item.dayKey" class="calendar-month-cell">
+                  <div class="column-day">
+                    <span class="day">{{ item.day }}</span>
+                  </div>
+                  <div class="column-week month-task-list">
+                    <div v-if="!item.data.length" class="calendar-empty">暂无任务</div>
+                    <div v-for="row in item.data.slice(0, 3)" :key="row.taskDateId || row.id" class="summary-card summary-card--month" @click="goDetail(row)">
+                      <span class="summary-card__line" :class="`priority-${formatPriority(row.priorityType)}`"></span>
+                      <div class="summary-card__title" v-html="row.title || row.name"></div>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </template>
+
+      <div v-else class="frame-card-content table-area">
+        <div class="calendar-day h-100">
+          <div class="target_list h-100">
+                        <div class="list-header">
+              <div class="name-col">
+                <el-checkbox
+                  v-model="checkouts"
+                  :disabled="allDisabled"
+                  style="padding: 0 10px"
+                  :indeterminate="allIndeterminate"
+                  @change="checkAllIn"
+                />
+                任务名称
+              </div>
+
+              <div class="person-col">执行人</div>
+              <div class="progress-col">进度</div>
+              <div class="person-col">状态</div>
+              <div class="time-col">任务时间</div>
+              <div class="person-col">来源</div>
+              <div class="handle-col">操作</div>
+            </div>
+
+            <div ref="cardContentRef" class="card-content" v-loading="loading">
+              <div v-for="item in pageData" :key="item.taskDateId || item.id" class="card-box">
+                <div class="card-row-main">
+                  <div class="icon-col">
+                    <el-icon v-if="item.sonTasks && item.sonTasks.length" class="expand-icon" :class="{ 'is-close': !item.show }" @click="item.show = !item.show">
+                      <ArrowDown />
+                    </el-icon>
+                  </div>
+                  <div class="box-target-col text-overflow">
+                    <el-checkbox
+                      v-model="item.checks"
+                      :disabled="!item.isEditable"
+                      :indeterminate="item.indeterminate"
+                      style="padding-right: 10px"
+                      @change="checkChange(item)"
+                    />
+                    <p class="title" v-html="item.title || item.name" @click="goDetail(item)"></p>
+                    <span v-if="formatTaskLabel(item.taskLabel)" class="task-label">{{ formatTaskLabel(item.taskLabel) }}</span>
+                    <span v-for="(tag, idx) in normalizeTags(item.taskTagls)" :key="idx" class="task-tag">{{ tag }}</span>
+                  </div>
+                  <div class="person-col person-col-flex">
+                    <el-avatar :size="28">{{ getAvatarText(item.executeName || item.ownerName) }}</el-avatar>
+                    <span class="person-name">{{ item.executeName || item.ownerName || '-' }}</span>
+                  </div>
+                  <div class="progress-col">
+                    <div class="progress-click" @click="openDialogTaskProgress(item.taskDateId || item.id)">
+                      <el-progress :percentage="toNumber(item.percent)" :stroke-width="12" color="#2d8cf0" />
+                    </div>
+                  </div>
+                  <div class="person-col">
+                    <div>{{ formatStatus(item.status, item.statusLabel) }}</div>
+                    <div v-if="Number(item.status) === 5 && item.overdueDays" class="overdue-text">{{ `(已逾期${item.overdueDays})` }}</div>
+                  </div>
+                  <div class="time-col">
+                    <span v-if="item.dateType && Number(item.dateType) < 5">{{ item.dateStartDate }}</span>
+                    <span v-if="item.dateType && Number(item.dateType) < 5">(</span>
+                    {{ item.dateName || item.deadline || '-' }}
+                    <span v-if="item.dateType && Number(item.dateType) < 5">)</span>
+                  </div>
+                  <div class="person-col">{{ item.createName || item.fromName || '-' }}</div>
+                  <div class="handle-col handle-col-btn">
+                    <button type="button" class="img-btn" title="添加子任务" @click="openDialogForm(item.id)">+</button>
+                    <button type="button" class="img-btn look-btn" title="查看详情" @click="goDetail(item)">看</button>
+                    <button type="button" class="img-btn del-btn" title="删除任务" @click="del(item.id)">删</button>
+                  </div>
+                </div>
+
+                <div v-show="item.show" class="box-content">
+                                    <div v-for="task in item.sonTasks || []" :key="task.taskDateId || task.id" class="card-row-sub">
+                    <div class="icon-col icon-col-placeholder"></div>
+                    <div class="content-target-col text-overflow">
+
+                      <el-checkbox
+                        v-model="task.checks"
+                        :disabled="!task.isEditable"
+                        style="padding-right: 10px"
+                        @change="checkChangeSon(item, task)"
+                      />
+                      <p class="title" v-html="task.title || task.name" @click="goDetail(task)"></p>
+                      <span v-if="formatTaskLabel(task.taskLabel)" class="task-label">{{ formatTaskLabel(task.taskLabel) }}</span>
+                      <span v-for="(tag, idx) in normalizeTags(task.taskTagls)" :key="idx" class="task-tag">{{ tag }}</span>
+                    </div>
+                    <div class="person-col person-col-flex">
+                      <el-avatar :size="28">{{ getAvatarText(task.executeName || task.ownerName) }}</el-avatar>
+                      <span class="person-name">{{ task.executeName || task.ownerName || '-' }}</span>
+                    </div>
+                    <div class="progress-col">
+                      <div class="progress-click" @click="openDialogTaskProgress(task.taskDateId || task.id)">
+                        <el-progress :percentage="toNumber(task.percent)" :stroke-width="12" color="#2d8cf0" />
+                      </div>
+                    </div>
+                    <div class="person-col">
+                      <div>{{ formatStatus(task.status, task.statusLabel) }}</div>
+                      <div v-if="Number(task.status) === 5 && task.overdueDays" class="overdue-text">{{ `(已逾期${task.overdueDays})` }}</div>
+                    </div>
+                    <div class="time-col">
+                      <span v-if="task.dateType && Number(task.dateType) < 5">{{ task.dateStartDate }}</span>
+                      <span v-if="task.dateType && Number(task.dateType) < 5">(</span>
+                      {{ task.dateName || task.deadline || '-' }}
+                      <span v-if="task.dateType && Number(task.dateType) < 5">)</span>
+                    </div>
+                    <div class="person-col">{{ task.createName || task.fromName || '-' }}</div>
+                    <div class="handle-col handle-col-btn">
+                      <button type="button" class="img-btn look-btn" title="查看详情" @click="goDetail(task)">看</button>
+                      <button type="button" class="img-btn del-btn" title="删除任务" @click="del(task.id)">删</button>
+                    </div>
+                  </div>
+                </div>
+              </div>
+
+              <el-empty v-if="!pageData.length && !loading" description="暂无任务" />
+            </div>
+
+            <div class="pager-wrap">
+              <el-pagination
+                v-model:current-page="pagination.pageNo"
+                v-model:page-size="pagination.pageSize"
+                background
+                :page-sizes="pagination.psizes"
+                :total="pagination.total"
+                layout="total, sizes, prev, pager, next"
+                @size-change="loadListData"
+                @current-change="loadListData"
+              />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
 </template>
 
 <script setup lang="ts">
-import TaskWorkspace from '@/modules/otr/task/components/TaskWorkspace.vue'
+import { computed, nextTick, onActivated, onMounted, reactive, ref, watch } from 'vue'
+import moment from 'moment'
+
+import { ArrowDown, OfficeBuilding, Tickets } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useRouter } from 'vue-router'
+import { taskListByTable, taskListMineGantt } from '@/api/otr/task/core'
+
+type TaskRow = Record<string, any>
+
+
+const router = useRouter()
+const loading = ref(false)
+const viewType = ref<number>(Number(localStorage.getItem('viewType') || 3))
+const toggleActive = ref<string>(localStorage.getItem('calendarActive') || 'day')
+const kws = ref('')
+const dt = ref('')
+const dt2 = ref<[string, string] | string[]>([
+    moment().format('YYYY-MM-DD'),
+  moment().endOf('month').format('YYYY-MM-DD'),
+
+])
+const pageData = ref<TaskRow[]>([])
+const checkouts = ref(false)
+const allIndeterminate = ref(false)
+const allDisabled = ref(false)
+const scrollFlag = ref(false)
+const cardContentRef = ref<HTMLElement | null>(null)
+
+const searchTypeOptions = [
+  { label: '全部', value: '' },
+  { label: '我负责的', value: '1' },
+  { label: '我参与的', value: '2' },
+  { label: '我分配的', value: '3' },
+  { label: '分配给我的', value: '4' },
+]
+
+const toggleData = [
+  { label: '日', value: 'day' },
+  { label: '周', value: 'week' },
+  { label: '月', value: 'month' },
+]
+
+const monthWeekLabels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
+
+const pagination = reactive({
+  total: 0,
+  psizes: [10, 20, 50, 100],
+  pageNo: 1,
+  pageSize: 10,
+})
+
+const search = reactive({
+  orderBy: '',
+  sort: '',
+  deptId: '',
+  subCompanyId: '',
+  searchType: '',
+  keyWord: '',
+    endDate: moment().endOf('month').format('YYYY-MM-DD'),
+  startDate: moment().format('YYYY-MM-DD'),
+
+  sta: [] as string[],
+  status: [] as string[],
+  userId: '',
+  pageNo: 1,
+  pageSize: 10,
+})
+
+const params = reactive({
+  kws: '',
+  start: '',
+  end: '',
+  deptId: '',
+  subCompanyId: '',
+  status: '',
+  userId: '',
+  searchType: '',
+  scope: 'mine',
+})
+
+const dayInfo = computed(() => {
+    const base = moment(params.start || moment().format('YYYY-MM-DD'))
+
+  const weekLabels = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
+  return {
+    day: `${base.year()}年${base.format('MM')}月${base.format('DD')}日`,
+    week: String(base.week()),
+    weekdayLabel: weekLabels[base.day()] || '周一',
+  }
+})
+
+const daySlots = computed(() => {
+  const rows = pageData.value
+  return [
+    { time: '上午', data: rows.filter((item) => isMorning(item)) },
+    { time: '下午', data: rows.filter((item) => !isMorning(item)) },
+  ]
+})
+
+const weekData = computed(() => buildWeekData(pageData.value, params.start || moment().format('YYYY-MM-DD')))
+const monthData = computed(() => buildMonthData(pageData.value, params.start || moment().format('YYYY-MM-DD')))
+
+
+watch(viewType, (value) => {
+  localStorage.setItem('viewType', String(value))
+})
+
+watch(
+  () => pageData.value,
+  async () => {
+    await nextTick()
+    if (cardContentRef.value) {
+      scrollFlag.value = cardContentRef.value.scrollWidth > cardContentRef.value.clientWidth
+    }
+  },
+  { deep: true },
+)
+
+function setViewType(value: number) {
+  viewType.value = value
+  if (viewType.value === 1) {
+    initData(toggleActive.value, dt.value || getDateDefault(toggleActive.value))
+  }
+  loadData()
+}
+
+function getDateDefault(type: string) {
+    const now = moment()
+
+  if (type === 'day') return now.format('YYYY-MM-DD')
+  if (type === 'week') return now.startOf('isoWeek').format('YYYY-MM-DD')
+  return now.format('YYYY-MM')
+}
+
+function initData(type: string, value: string) {
+  params.deptId = search.deptId
+  params.subCompanyId = search.subCompanyId
+  params.status = (search.status || []).join(',')
+  params.userId = search.userId
+  params.searchType = search.searchType
+  params.kws = search.keyWord
+
+  if (type === 'day') {
+    dt.value = value
+    params.start = value
+    params.end = value
+    return
+  }
+  if (type === 'week') {
+    dt.value = value
+    params.start = value
+        params.end = moment(value).add(6, 'day').format('YYYY-MM-DD')
+
+    return
+  }
+  dt.value = value
+  params.start = `${value}-01`
+    params.end = moment(`${value}-01`).endOf('month').format('YYYY-MM-DD')
+
+}
+
+function handleChangeType(value: string) {
+  toggleActive.value = value
+  localStorage.setItem('calendarActive', value)
+  const defaultDate = getDateDefault(value)
+  initData(value, defaultDate)
+  loadData()
+}
+
+function handleChangeTime(value: any) {
+  if (viewType.value === 3) {
+    const range = Array.isArray(value) ? value : []
+        search.startDate = range[0] || moment().format('YYYY-MM-DD')
+    search.endDate = range[1] || moment().endOf('month').format('YYYY-MM-DD')
+
+    loadListData()
+    return
+  }
+
+  if (typeof value === 'string') {
+    initData(toggleActive.value, value)
+  }
+  loadCalendarData()
+}
+
+function keydownSearch() {
+  search.keyWord = kws.value
+  params.kws = kws.value
+  if (viewType.value === 1) {
+    initData(toggleActive.value, dt.value || getDateDefault(toggleActive.value))
+    loadCalendarData()
+  } else {
+    loadListData()
+  }
+}
+
+function onSearch() {
+  search.sta = [...(search.status || [])]
+  if (viewType.value === 1) {
+    initData(toggleActive.value, dt.value || getDateDefault(toggleActive.value))
+    loadCalendarData()
+  } else {
+    loadListData()
+  }
+}
+
+function reset() {
+  search.keyWord = ''
+  search.searchType = ''
+  search.status = []
+  search.sta = []
+    search.startDate = moment().format('YYYY-MM-DD')
+  search.endDate = moment().endOf('month').format('YYYY-MM-DD')
+  dt2.value = [moment().format('YYYY-MM-DD'), moment().endOf('month').format('YYYY-MM-DD')]
+
+  dt.value = getDateDefault(toggleActive.value)
+  kws.value = ''
+  if (viewType.value === 1) {
+    initData(toggleActive.value, dt.value)
+  }
+  loadData()
+}
+
+function openDialogForm(parentTaskId?: string | number) {
+  if (parentTaskId) {
+    router.push({ path: '/otr/task/form', query: { ptaskId: String(parentTaskId) } })
+    return
+  }
+  router.push('/otr/task/form')
+}
+
+function goDetail(row: TaskRow) {
+  router.push({ path: '/otr/task/detail', query: { id: String(row.taskDateId || row.id || '') } })
+}
+
+function openDialogTaskProgress(taskId?: string | number) {
+  if (!taskId) return
+  router.push({ path: '/otr/task/detail', query: { id: String(taskId) } })
+}
+
+function normalizeTaskRows(records: TaskRow[]): TaskRow[] {
+  return (records || []).map((item) => ({
+    ...item,
+    title: item.title || item.name || '-',
+    executeName: item.executeName || item.ownerName || item.memberName || '-',
+    createName: item.createName || item.fromName || '-',
+    percent: toNumber(item.percent || item.progress || 0),
+    status: item.status ?? inferStatus(item.statusLabel),
+    dateName: item.dateName || item.deadline || item.createdAt || '-',
+    dateStartDate: item.dateStartDate || item.startDate || item.deadline || item.createdAt || '',
+    show: item.show ?? true,
+    isEditable: item.isEditable ?? true,
+    sonTasks: normalizeTaskRows(item.sonTasks || []),
+    checks: false,
+    indeterminate: false,
+  }))
+}
+
+async function loadListData() {
+  loading.value = true
+  try {
+    const res: any = await taskListByTable({
+      ...search,
+      pageNo: pagination.pageNo,
+      pageSize: pagination.pageSize,
+      sta: search.status,
+      keyWord: search.keyWord,
+      startDate: search.startDate,
+      endDate: search.endDate,
+    })
+    const result = res?.result || {}
+    const records = result.records || result.list || []
+    pageData.value = normalizeTaskRows(records)
+    pagination.total = Number(result.total || pageData.value.length)
+    syncCheckState()
+  } finally {
+    loading.value = false
+  }
+}
+
+async function loadCalendarData() {
+  loading.value = true
+  try {
+    const res: any = await taskListMineGantt({
+      page: 1,
+      psize: 1000,
+      start: params.start,
+      end: params.end,
+      kws: params.kws,
+      deptId: params.deptId,
+      subCompanyId: params.subCompanyId,
+      status: params.status,
+      userId: params.userId,
+      searchType: params.searchType,
+      scope: params.scope,
+    })
+    const result = res?.result || {}
+    const records = result.records || result.list || []
+    pageData.value = normalizeTaskRows(records)
+  } finally {
+    loading.value = false
+  }
+}
+
+function loadData() {
+  if (viewType.value === 1) {
+    loadCalendarData()
+  } else {
+    loadListData()
+  }
+}
+
+function checkAllIn() {
+  allIndeterminate.value = false
+  pageData.value.forEach((item) => {
+    if (item.isEditable) {
+      item.checks = checkouts.value
+      item.indeterminate = false
+    }
+    ;(item.sonTasks || []).forEach((child: TaskRow) => {
+      if (child.isEditable) child.checks = checkouts.value
+    })
+  })
+  syncCheckState()
+}
+
+function checkChange(row: TaskRow) {
+  row.indeterminate = false
+  ;(row.sonTasks || []).forEach((child: TaskRow) => {
+    if (child.isEditable) child.checks = !!row.checks
+  })
+  syncCheckState()
+}
+
+function checkChangeSon(parent: TaskRow, task: TaskRow) {
+  const editableChildren = (parent.sonTasks || []).filter((child: TaskRow) => child.isEditable)
+  const checkedCount = editableChildren.filter((child: TaskRow) => child.checks).length
+  parent.checks = checkedCount > 0 && checkedCount === editableChildren.length
+  parent.indeterminate = checkedCount > 0 && checkedCount < editableChildren.length
+  task.checks = !!task.checks
+  syncCheckState()
+}
+
+function getChooseIds() {
+  const ids: Array<string | number> = []
+  pageData.value.forEach((item) => {
+    if (item.checks) ids.push(item.id)
+    ;(item.sonTasks || []).forEach((child: TaskRow) => {
+      if (child.checks) ids.push(child.id)
+    })
+  })
+  return ids.filter(Boolean)
+}
+
+function syncCheckState() {
+  const editableRows = pageData.value.flatMap((item) => {
+    const rows = item.isEditable ? [item] : []
+    const children = (item.sonTasks || []).filter((child: TaskRow) => child.isEditable)
+    return rows.concat(children)
+  })
+  const checkedRows = editableRows.filter((row) => row.checks)
+  allDisabled.value = editableRows.length === 0
+  checkouts.value = editableRows.length > 0 && checkedRows.length === editableRows.length
+  allIndeterminate.value = checkedRows.length > 0 && checkedRows.length < editableRows.length
+}
+
+function delTasks() {
+  const ids = getChooseIds()
+  if (!ids.length) {
+    ElMessage.warning('请先选择任务')
+    return
+  }
+  ElMessageBox.confirm('确认批量删除已选择任务吗?', '系统提示', {
+    confirmButtonText: '提交',
+    cancelButtonText: '取消',
+    type: 'warning',
+  }).then(() => {
+    pageData.value = pageData.value.filter((item) => !ids.includes(item.id))
+    pageData.value.forEach((item) => {
+      item.sonTasks = (item.sonTasks || []).filter((child: TaskRow) => !ids.includes(child.id))
+    })
+    pagination.total = Math.max(0, pagination.total - ids.length)
+    syncCheckState()
+    ElMessage.success('删除成功')
+  }).catch(() => undefined)
+}
+
+function del(id?: string | number) {
+  if (!id) return
+  ElMessageBox.confirm('确认删除该任务吗?', '系统提示', {
+    confirmButtonText: '提交',
+    cancelButtonText: '取消',
+    type: 'warning',
+  }).then(() => {
+    pageData.value = pageData.value.filter((item) => item.id !== id)
+    pageData.value.forEach((item) => {
+      item.sonTasks = (item.sonTasks || []).filter((child: TaskRow) => child.id !== id)
+    })
+    pagination.total = Math.max(0, pagination.total - 1)
+    syncCheckState()
+    ElMessage.success('删除成功')
+  }).catch(() => undefined)
+}
+
+function toNumber(value: any) {
+  const raw = String(value ?? '0').replace('%', '')
+  const num = Number(raw)
+  return Number.isNaN(num) ? 0 : Math.max(0, Math.min(100, num))
+}
+
+function formatPercent(value: any) {
+  return `${toNumber(value)}%`
+}
+
+function inferStatus(label?: string) {
+  if (!label) return 1
+  if (label.includes('未开始') || label.includes('未读')) return 1
+  if (label.includes('进行中') || label.includes('待处理') || label.includes('待确认')) return 2
+  if (label.includes('已完成') || label.includes('已发布')) return 3
+  if (label.includes('已逾期')) return 5
+  return 1
+}
+
+function formatStatus(status: any, statusLabel?: string) {
+  if (statusLabel) return statusLabel
+  switch (Number(status)) {
+    case 1:
+      return '未开始'
+    case 2:
+      return '进行中'
+    case 3:
+    case 4:
+      return '已完成'
+    case 5:
+      return '已逾期'
+    default:
+      return '-'
+  }
+}
+
+function formatTaskLabel(label: any) {
+  const value = Number(label)
+  if (!value || value === 1) return ''
+  if (value === 2) return '主任务'
+  if (value === 3) return '子任务'
+  return '任务'
+}
+
+function normalizeTags(tags: any) {
+  if (!Array.isArray(tags)) return []
+  return tags.map((item) => item?.name || item?.label || item?.title || String(item)).filter(Boolean)
+}
+
+function getAvatarText(name?: string) {
+  return name ? name.slice(-1) : '人'
+}
+
+function isMorning(item: TaskRow) {
+  const dateText = item.dateStartDate || item.startDate || item.deadline || item.createdAt || item.dateName || ''
+  if (!dateText) return true
+    const hour = moment(dateText).hour()
+
+  if (Number.isNaN(hour)) return true
+  return hour < 12
+}
+
+function formatPriority(priorityType: any) {
+  const num = Number(priorityType)
+  if (num === 1) return 'purple'
+  if (num === 2) return 'blue'
+  if (num === 3) return 'yellow'
+  return 'green'
+}
+
+function buildWeekData(records: TaskRow[], startDate: string) {
+    const start = moment(startDate).startOf('isoWeek')
+
+  return new Array(7).fill(null).map((_, index) => {
+    const current = start.add(index, 'day')
+    const dayKey = current.format('YYYY-MM-DD')
+    return {
+      dayKey,
+      day: current.format('MM.DD'),
+      weekdayLabel: monthWeekLabels[index].replace('周', ''),
+      data: records.filter((item) => matchDay(item, dayKey)),
+    }
+  })
+}
+
+function buildMonthData(records: TaskRow[], startDate: string) {
+    const base = moment(startDate).startOf('month')
+
+  const start = base.startOf('isoWeek')
+  return new Array(42).fill(null).map((_, index) => {
+    const current = start.add(index, 'day')
+    const dayKey = current.format('YYYY-MM-DD')
+    return {
+      dayKey,
+      day: current.format('DD'),
+      data: records.filter((item) => matchDay(item, dayKey)),
+    }
+  })
+}
+
+function matchDay(item: TaskRow, dayKey: string) {
+  const candidates = [item.dateStartDate, item.startDate, item.deadline, item.createdAt, item.dateName]
+  return candidates.some((value) => {
+    if (!value) return false
+        return moment(value).format('YYYY-MM-DD') === dayKey
+
+  })
+}
+
+onMounted(() => {
+  const defaultDate = getDateDefault(toggleActive.value)
+  dt.value = defaultDate
+  initData(toggleActive.value, defaultDate)
+  loadData()
+})
+
+onActivated(() => {
+  loadData()
+})
 </script>
+
+<style scoped lang="scss">
+.task-page {
+  .frame-card {
+    background: #fff;
+    border-radius: 8px;
+  }
+
+  .frame-card-header {
+    padding: 16px 20px 10px;
+    border-bottom: 1px solid #f0f2f5;
+  }
+
+  .frame-card-content {
+    padding: 0;
+  }
+
+  .frame-search {
+    width: 100%;
+  }
+
+  .search-line {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+    :deep(.el-form-item) {
+      margin-bottom: 0;
+      margin-right: 12px;
+    }
+
+    :deep(.el-select) {
+      width: 140px;
+    }
+
+    :deep(.el-date-editor) {
+      width: 240px;
+    }
+  }
+
+  .search-line-toggle {
+    margin-top: 16px;
+  }
+
+  .toggle-group {
+    display: inline-flex;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    overflow: hidden;
+  }
+
+  .toggle-btn {
+    min-width: 44px;
+    height: 32px;
+    border: 0;
+    background: #fff;
+    color: #606266;
+    cursor: pointer;
+    padding: 0 14px;
+  }
+
+  .toggle-btn + .toggle-btn {
+    border-left: 1px solid #dcdfe6;
+  }
+
+  .toggle-btn.is-active {
+    background: #409eff;
+    color: #fff;
+  }
+
+  .calendar-card {
+    margin-top: 0;
+    overflow: auto;
+    margin-bottom: 0;
+  }
+
+  .calendar-day-view,
+  .calendar-week-view,
+  .calendar-month-view {
+    padding: 16px 20px 20px;
+  }
+
+  .calendar-header,
+  .calendar-week-header,
+  .calendar-month-header {
+    display: grid;
+    border: 1px solid #ebeef5;
+    background: #f8fafc;
+    font-weight: 600;
+    color: #606266;
+  }
+
+  .calendar-header {
+    grid-template-columns: 120px 1fr;
+  }
+
+  .calendar-item {
+    display: grid;
+    grid-template-columns: 120px 1fr;
+    border-left: 1px solid #ebeef5;
+    border-right: 1px solid #ebeef5;
+    border-bottom: 1px solid #ebeef5;
+    min-height: 88px;
+  }
+
+  .column-time,
+  .column-content,
+  .column-week,
+  .column-day {
+    padding: 12px;
+  }
+
+  .calendar-content-list {
+    background: #fff;
+  }
+
+  .calendar-empty {
+    color: #b1b7c3;
+    font-size: 12px;
+  }
+
+  .calendar-task-chip {
+    display: flex;
+    justify-content: space-between;
+    gap: 8px;
+    padding: 8px 10px;
+    margin-bottom: 8px;
+    border-radius: 6px;
+    background: #f5f8fd;
+    cursor: pointer;
+  }
+
+  .calendar-task-chip__title {
+    color: #303133;
+  }
+
+  .calendar-task-chip__meta {
+    color: #409eff;
+    white-space: nowrap;
+  }
+
+  .calendar-week-header,
+  .calendar-week-body {
+    grid-template-columns: repeat(7, minmax(0, 1fr));
+  }
+
+  .calendar-week-header {
+    display: grid;
+  }
+
+  .calendar-week-body {
+    display: grid;
+    border-left: 1px solid #ebeef5;
+    border-right: 1px solid #ebeef5;
+    border-bottom: 1px solid #ebeef5;
+  }
+
+  .calendar-week-column {
+    min-height: 220px;
+    border-right: 1px solid #ebeef5;
+    padding: 12px;
+  }
+
+  .calendar-week-column:last-child {
+    border-right: 0;
+  }
+
+  .summary-card {
+    position: relative;
+    padding: 10px 12px 10px 16px;
+    margin-bottom: 8px;
+    background: #fff;
+    border: 1px solid #ebeef5;
+    border-radius: 6px;
+    cursor: pointer;
+  }
+
+  .summary-card__line {
+    position: absolute;
+    left: 0;
+    top: 0;
+    bottom: 0;
+    width: 4px;
+    border-radius: 6px 0 0 6px;
+  }
+
+  .priority-purple {
+    background: #8b5cf6;
+  }
+
+  .priority-blue {
+    background: #409eff;
+  }
+
+  .priority-yellow {
+    background: #e6a23c;
+  }
+
+  .priority-green {
+    background: #67c23a;
+  }
+
+  .summary-card__title {
+    color: #303133;
+    font-size: 13px;
+    line-height: 20px;
+  }
+
+  .summary-card__meta {
+    color: #909399;
+    font-size: 12px;
+    margin-top: 4px;
+  }
+
+  .calendar-month-grid {
+    display: grid;
+    grid-template-columns: repeat(7, minmax(0, 1fr));
+    border-left: 1px solid #ebeef5;
+    border-right: 1px solid #ebeef5;
+    border-bottom: 1px solid #ebeef5;
+  }
+
+  .calendar-month-cell {
+    min-height: 160px;
+    border-right: 1px solid #ebeef5;
+    border-bottom: 1px solid #ebeef5;
+    background: #fff;
+  }
+
+  .calendar-month-cell:nth-child(7n) {
+    border-right: 0;
+  }
+
+  .column-day {
+    display: flex;
+    justify-content: space-between;
+    padding-bottom: 0;
+  }
+
+  .day {
+    font-weight: 600;
+    color: #303133;
+  }
+
+  .month-task-list {
+    padding-top: 8px;
+  }
+
+  .summary-card--month {
+    padding-top: 8px;
+    padding-bottom: 8px;
+  }
+
+  .table-area {
+    background: #f8fafc;
+    margin: 0;
+    overflow: hidden;
+  }
+
+  .target_list {
+    padding: 0 20px 20px;
+  }
+
+    .list-header,
+  .card-row-main,
+  .card-row-sub {
+    display: grid;
+    grid-template-columns: 32px minmax(320px, 2.4fr) 140px 180px 100px 180px 80px 120px;
+    align-items: center;
+    column-gap: 20px;
+  }
+
+
+  .list-header {
+    height: 56px;
+    color: #606266;
+    font-weight: 600;
+  }
+
+    .name-col,
+  .normal-name-col,
+  .small-name-col,
+  .box-target-col,
+  .content-target-col {
+    display: flex;
+    align-items: center;
+    min-width: 0;
+  }
+
+  .name-col {
+    grid-column: 1 / 3;
+  }
+
+  .icon-col {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .icon-col-placeholder {
+    visibility: hidden;
+  }
+
+
+  .card-content {
+    min-height: 320px;
+  }
+
+  .card-box {
+    margin-bottom: 12px;
+    background: #fff;
+    border: 1px solid #ebeef5;
+    border-radius: 8px;
+    overflow: hidden;
+  }
+
+    .card-row-main,
+  .card-row-sub {
+    min-height: 68px;
+    padding: 0 16px;
+  }
+
+  .card-row-main {
+    background: #fff;
+  }
+
+
+  .card-row-sub {
+    border-top: 1px solid #f3f4f6;
+    background: #fbfcfe;
+  }
+
+    .title {
+    margin: 0;
+    color: #303133;
+    cursor: pointer;
+    line-height: 20px;
+    font-size: 14px;
+  }
+
+
+    .text-overflow .title {
+    flex: 1;
+    min-width: 0;
+    max-width: 100%;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+
+  .task-label {
+    display: inline-flex;
+    align-items: center;
+    height: 22px;
+    margin-left: 8px;
+    padding: 0 8px;
+    font-size: 12px;
+    color: #409eff;
+    background: #ecf5ff;
+    border-radius: 11px;
+  }
+
+  .task-tag {
+    display: inline-flex;
+    align-items: center;
+    height: 22px;
+    margin-left: 8px;
+    padding: 0 8px;
+    font-size: 12px;
+    color: #606266;
+    background: #f4f4f5;
+    border-radius: 11px;
+  }
+
+  .person-col-flex {
+    display: flex;
+    align-items: center;
+  }
+
+    .person-name {
+    margin-left: 8px;
+    color: #606266;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+
+    .progress-click {
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+  }
+
+  :deep(.progress-col .el-progress) {
+    width: 100%;
+  }
+
+
+  .overdue-text {
+    color: #e71e19;
+    font-size: 12px;
+    margin-top: 2px;
+  }
+
+  .expand-icon {
+    cursor: pointer;
+    color: #909399;
+    transition: transform 0.2s;
+  }
+
+  .expand-icon.is-close {
+    transform: rotate(-90deg);
+  }
+
+    .handle-col-btn {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    justify-content: flex-start;
+  }
+
+
+  .img-btn {
+    width: 28px;
+    height: 28px;
+    border: 0;
+    border-radius: 50%;
+    color: #fff;
+    background: #409eff;
+    cursor: pointer;
+    font-size: 12px;
+  }
+
+  .look-btn {
+    background: #67c23a;
+  }
+
+  .del-btn {
+    background: #f56c6c;
+  }
+
+    .time-col,
+  .progress-col,
+  .handle-col,
+  .person-col {
+    min-width: 0;
+    color: #606266;
+    font-size: 13px;
+  }
+
+  .time-col {
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  .pager-wrap {
+    display: flex;
+    justify-content: flex-end;
+    margin-top: 16px;
+  }
+
+
+    @media (max-width: 1280px) {
+    .list-header,
+    .card-row-main,
+    .card-row-sub {
+      grid-template-columns: 28px minmax(240px, 2fr) 120px 150px 88px 160px 72px 108px;
+      column-gap: 12px;
+    }
+  }
+
+}
+</style>
+

+ 20 - 2
ui/sp-user-center/src/modules/otr/task/views/Member.vue

@@ -1,7 +1,25 @@
 <template>
-  <task-workspace title="成员任务" scene="member" />
+  <task-list-panel
+    scene="member"
+    :show-search-type="true"
+    :show-company-filter="true"
+    :show-dept-filter="true"
+    :show-user-filter="true"
+    :allow-batch-delete="true"
+    switch-route="/otr/task/table"
+    :search-type-options="searchTypeOptions"
+  />
 </template>
 
 <script setup lang="ts">
-import TaskWorkspace from '@/modules/otr/task/components/TaskWorkspace.vue'
+import TaskListPanel from '@/modules/otr/task/components/TaskListPanel.vue'
+
+const searchTypeOptions = [
+  { label: '全部', value: '' },
+  { label: 'Ta负责的', value: '1' },
+  { label: 'Ta参与的', value: '2' },
+  { label: 'Ta分配的', value: '3' },
+  { label: '分配给Ta的', value: '4' },
+]
 </script>
+

+ 17 - 2
ui/sp-user-center/src/modules/otr/task/views/Table.vue

@@ -1,7 +1,22 @@
 <template>
-  <task-workspace title="任务看板" scene="table" />
+  <task-list-panel
+    scene="table"
+    :show-search-type="true"
+    :allow-create="true"
+    switch-route="/otr/task/list"
+    :search-type-options="searchTypeOptions"
+  />
 </template>
 
 <script setup lang="ts">
-import TaskWorkspace from '@/modules/otr/task/components/TaskWorkspace.vue'
+import TaskListPanel from '@/modules/otr/task/components/TaskListPanel.vue'
+
+const searchTypeOptions = [
+  { label: '全部', value: '' },
+  { label: '我负责的', value: '1' },
+  { label: '我参与的', value: '2' },
+  { label: '我分配的', value: '3' },
+  { label: '分配给我的', value: '4' },
+]
 </script>
+

+ 60 - 44
ui/sp-user-center/src/router/modules/otr.ts

@@ -36,67 +36,73 @@ const otrRoutes: RouteRecordRaw[] = [
                     {
                         path: 'company',
                         name: 'OtrTargetCompany',
-                        component: () => import('@/modules/otr/target/views/Company.vue'),
+                        component: () => import('@/views/target/company.vue'),
                         meta: { module: ModuleKey.OTR, title: '公司目标' },
                     },
                     {
                         path: 'depart',
                         name: 'OtrTargetDepart',
-                        component: () => import('@/modules/otr/target/views/Depart.vue'),
+                        component: () => import('@/views/target/depart.vue'),
                         meta: { module: ModuleKey.OTR, title: '部门目标' },
                     },
                     {
                         path: 'mine',
                         name: 'OtrTargetMine',
-                        component: () => import('@/modules/otr/target/views/Mine.vue'),
+                        component: () => import('@/views/target/mine.vue'),
                         meta: { module: ModuleKey.OTR, title: '我的目标' },
                     },
                     {
                         path: 'template',
                         name: 'OtrTargetTemplate',
-                        component: () => import('@/modules/otr/target/views/Template.vue'),
+                        component: () => import('@/views/target/targetTemplate.vue'),
                         meta: { module: ModuleKey.OTR, title: '目标模板' },
                     },
                     {
                         path: 'category',
                         name: 'OtrTargetCategory',
-                        component: () => import('@/modules/otr/target/views/Category.vue'),
+                        component: () => import('@/views/target/templateCategory.vue'),
                         meta: { module: ModuleKey.OTR, title: '模板类目' },
                     },
                     {
                         path: 'report',
                         name: 'OtrTargetReport',
-                        component: () => import('@/modules/otr/target/views/Report.vue'),
+                        component: () => import('@/views/target/report.vue'),
                         meta: { module: ModuleKey.OTR, title: '目标报告' },
                     },
                     {
                         path: 'tree-demo',
                         name: 'OtrTargetTreeDemo',
-                        component: () => import('@/modules/otr/target/views/TreeDemo.vue'),
+                        component: () => import('@/views/target/treeDemo.vue'),
                         meta: { module: ModuleKey.OTR, title: '首页目标' },
                     },
                     {
                         path: 'tree',
                         name: 'OtrTargetTree',
-                        component: () => import('@/modules/otr/target/views/Tree.vue'),
+                        component: () => import('@/views/target/targetTree.vue'),
                         meta: { module: ModuleKey.OTR, title: '目标看板' },
                     },
                     {
                         path: 'form',
                         name: 'OtrTargetForm',
-                        component: () => import('@/modules/otr/target/views/Form.vue'),
+                        component: () => import('@/views/target/targetForm.vue'),
+                        meta: { module: ModuleKey.OTR, title: '目标详情' },
+                    },
+                    {
+                        path: 'detail',
+                        name: 'OtrTargetDetail',
+                        component: () => import('@/views/target/minedetail.vue'),
                         meta: { module: ModuleKey.OTR, title: '目标详情' },
                     },
                     {
                         path: 'draft',
                         name: 'OtrTargetDraft',
-                        component: () => import('@/modules/otr/target/views/Draft.vue'),
+                        component: () => import('@/views/target/draft.vue'),
                         meta: { module: ModuleKey.OTR, title: '目标草稿' },
                     },
                     {
                         path: 'approval',
                         name: 'OtrTargetApproval',
-                        component: () => import('@/modules/otr/target/views/Approval.vue'),
+                        component: () => import('@/views/target/approval.vue'),
                         meta: { module: ModuleKey.OTR, title: '目标审批' },
                     },
                 ],
@@ -212,40 +218,50 @@ const otrRoutes: RouteRecordRaw[] = [
 
             // ============ 任务管理(迁自 sp-tems-ui / task)============
             {
-                path: 'task/list',
-                name: 'OtrTaskList',
-                component: () => import('@/modules/otr/task/views/List.vue'),
+                path: 'task',
+                component: ParentView,
+                redirect: '/otr/task/list',
+                alwaysShow: true,
+                name: 'OtrTask',
                 meta: { module: ModuleKey.OTR, title: '任务管理', icon: 'edit' },
-            },
-            {
-                path: 'task/member',
-                name: 'OtrTaskMember',
-                component: () => import('@/modules/otr/task/views/Member.vue'),
-                meta: { module: ModuleKey.OTR, title: '任务成员' },
-            },
-            {
-                path: 'task/table',
-                name: 'OtrTaskTable',
-                component: () => import('@/modules/otr/task/views/Table.vue'),
-                meta: { module: ModuleKey.OTR, title: '任务看板' },
-            },
-            {
-                path: 'task/detail',
-                name: 'OtrTaskDetail',
-                component: () => import('@/modules/otr/task/views/Detail.vue'),
-                meta: { module: ModuleKey.OTR, title: '任务详情' },
-            },
-            {
-                path: 'task/form',
-                name: 'OtrTaskForm',
-                component: () => import('@/modules/otr/task/views/Form.vue'),
-                meta: { module: ModuleKey.OTR, title: '新建任务' },
-            },
-            {
-                path: 'task/at-list',
-                name: 'OtrTaskAtList',
-                component: () => import('@/modules/otr/task/views/AtList.vue'),
-                meta: { module: ModuleKey.OTR, title: '@我的任务' },
+                children: [
+                    {
+                        path: 'list',
+                        name: 'OtrTaskList',
+                        component: () => import('@/modules/otr/task/views/List.vue'),
+                        meta: { module: ModuleKey.OTR, title: '我的任务' },
+                    },
+                    {
+                        path: 'member',
+                        name: 'OtrTaskMember',
+                        component: () => import('@/modules/otr/task/views/Member.vue'),
+                        meta: { module: ModuleKey.OTR, title: '成员任务' },
+                    },
+                    {
+                        path: 'table',
+                        name: 'OtrTaskTable',
+                        component: () => import('@/modules/otr/task/views/Table.vue'),
+                        meta: { module: ModuleKey.OTR, title: '任务看板' },
+                    },
+                    {
+                        path: 'detail',
+                        name: 'OtrTaskDetail',
+                        component: () => import('@/modules/otr/task/views/Detail.vue'),
+                        meta: { module: ModuleKey.OTR, title: '任务详情' },
+                    },
+                    {
+                        path: 'form',
+                        name: 'OtrTaskForm',
+                        component: () => import('@/modules/otr/task/views/Form.vue'),
+                        meta: { module: ModuleKey.OTR, title: '新建任务' },
+                    },
+                    {
+                        path: 'at-list',
+                        name: 'OtrTaskAtList',
+                        component: () => import('@/modules/otr/task/views/AtList.vue'),
+                        meta: { module: ModuleKey.OTR, title: '@我的任务' },
+                    },
+                ],
             },
 
             // ============ 工作总结(迁自 sp-tems-ui / summary)============