Commit c1615014 authored by David Yang's avatar David Yang

feat: 部分字段改成选项,优化 ui

parent 12c2303f
......@@ -26,7 +26,7 @@ export default function LoginPage() {
const redirect = searchParams.get('redirect') || '/';
const siteTitle = site?.title || site?.name || '项目管理系统';
const siteTitle = site?.title || site?.name || '部门任务管理';
const siteSubtitle =
site?.subtitle || '项目下发、成员协作、甘特排期与个人日历统一协同';
......@@ -141,7 +141,7 @@ export default function LoginPage() {
Sign In
</p>
<h2 className="mt-3 text-3xl font-semibold text-slate-900">
登录项目管理系统
登录部门任务管理
</h2>
<p className="mt-3 text-sm leading-6 text-slate-500">
参考 `streams-admin` 登录页改造,已接入验证码校验流程。
......
......@@ -14,7 +14,7 @@ import clsx from 'clsx';
import { useContext, useEffect, useMemo, useState } from 'react';
import { Link, Outlet, useLocation } from 'umi';
const APP_NAME = '项目管理系统';
const APP_NAME = '部门任务管理';
const menuGroups = [
{
......@@ -272,9 +272,6 @@ export default function ProjectManagerLayout() {
<h1 className="truncate text-sm font-semibold tracking-[0.02em] text-white/95">
{APP_NAME}
</h1>
<div className="mt-1 text-xs text-zinc-400">
项目资源管理工作台
</div>
</div>
{applicationSwitcherButton}
</>
......
......@@ -35,12 +35,12 @@
关键字段:
- `project_name`:项目名称
- `big_category`:大类
- `big_category`:大类,选择项为部门管理、纪检工作、审计工作
- `project`:项目归属或项目分类补充字段
- `project_owner`:项目负责人,直接关联系统用户,多值
- `project_collaborator`:项目协作人,直接关联系统用户,多值
- `task_source`:任务来源
- `task_nature`:任务性质
- `task_source`:任务来源,选择项由 Subject 字段配置 `settings.options` 提供
- `task_nature`:任务性质,选择项由 Subject 字段配置 `settings.options` 提供
- `occurrence_frequency`:发生频率
- `planned_start_date` / `planned_end_date`:计划时间
- `actual_start_date` / `actual_finish_date`:实际时间
......@@ -59,7 +59,7 @@
1. 领导创建一条 `pm_project_task`
2. 填写项目名称、计划开始日期、计划结束日期
3. 按需要补充大类、项目、任务来源、任务性质、发生频率
3. 按需要选择大类、任务来源、任务性质、发生频率,并补充项目
4. 按需勾选是否属于年度审计计划 `is_annual_audit_plan`
5. 选择一个或多个项目负责人 `project_owner`
6. 选择一个或多个项目协作人 `project_collaborator`
......@@ -128,28 +128,25 @@
- 结果状态分布柱状图
- 进度区间分布柱状图
- 成员负载 Top N 横向柱状图
- 维度概览区保留列表化统计:
- 发生频率
- 大类
- 任务来源
- “维度概览”区块当前意义不明确,资源统计看板暂不展示该模块;发生频率、大类、任务来源等维度待统计口径明确后再补充为独立图表或筛选项
- 风险清单区展示:
- 即将到期项目
- 超时项目清单
- 风险清单区位于成员负载明细下方,采用独立全宽区块展示,避免作为右侧纵列造成页面左右高度失衡
- 建议补充统计维度:
- 项目状态分布
- 结果状态分布(按时间规则实时派生)
- 发生频率分布
- 大类分布
- 任务来源分布
- 负责人/协作人负载分布
- 即将到期项目
- 超时项目清单
- 统计口径以 `pm_project_task.metadata` 的当前字段为准,优先展示:
- `project_status`
- `occurrence_frequency`
- `big_category`
- `task_source`
- `completion_percent`
- `project_owner`
- `project_collaborator`
- `planned_end_date`
- `actual_finish_date`
### 5.1 甘特图
......@@ -229,6 +226,8 @@ const result = await queryUsers(0, 20, {
## 6.3 Excel 批量导入口径
- Excel 模板采用业务列头:`大类`、`项目`、`是否属于年度审计计划`、`发生频率`、`任务名称`、`任务来源`、`任务性质`、`计划启动时间`、`计划完成时间`、`时间进度`、`任务进度`、`任务状态`、`主办人`、`协办人1`
- `大类` 导入到 `metadata.big_category`,当前仅支持 `部门管理`、`纪检工作`、`审计工作`
- `任务来源` / `任务性质` 导入到 `metadata.task_source` / `metadata.task_nature`,应填写对应 Subject 字段 `settings.options` 中配置的存储值
- `任务名称` 导入到 `metadata.project_name`
- `是否属于年度审计计划` 导入到 `metadata.is_annual_audit_plan`,支持 `是/否`、`true/false`、`1/0`
- `主办人` 导入到 `metadata.project_owner`,`协办人1` 导入到 `metadata.project_collaborator`;支持填写系统用户 `display_name` 或 `username`,前端会先按 `username/display_name` 的 `$regex` 条件调用 `queryUsers` 查询,并最终保存为用户 `_id` 数组
......
......@@ -28,6 +28,12 @@ export const OCCURRENCE_FREQUENCY_OPTIONS = [
{ label: '低', value: '1' },
];
export const BIG_CATEGORY_OPTIONS = [
{ label: '部门管理', value: '部门管理' },
{ label: '纪检工作', value: '纪检工作' },
{ label: '审计工作', value: '审计工作' },
];
export const PRIORITY_OPTIONS = [
{ label: '高', value: 'high' },
{ label: '中', value: 'medium' },
......@@ -331,11 +337,20 @@ function extractSubjectField(subject, fieldName) {
export async function loadProjectTaskFieldOptions() {
try {
const subject = await loadSubject(PROJECT_TASK_SUBJECT);
const bigCategoryField = extractSubjectField(subject, 'big_category');
const taskSourceField = extractSubjectField(subject, 'task_source');
const taskNatureField = extractSubjectField(subject, 'task_nature');
const projectStatusField = extractSubjectField(subject, 'project_status');
const priorityField = extractSubjectField(subject, 'priority');
const occurrenceFrequencyField = extractSubjectField(subject, 'occurrence_frequency');
return {
bigCategoryOptions:
normalizeSelectOptions(bigCategoryField?.settings?.options).length > 0
? normalizeSelectOptions(bigCategoryField?.settings?.options)
: BIG_CATEGORY_OPTIONS,
taskSourceOptions: normalizeSelectOptions(taskSourceField?.settings?.options),
taskNatureOptions: normalizeSelectOptions(taskNatureField?.settings?.options),
projectStatusOptions:
normalizeSelectOptions(projectStatusField?.settings?.options).length > 0
? normalizeSelectOptions(projectStatusField?.settings?.options)
......@@ -348,6 +363,9 @@ export async function loadProjectTaskFieldOptions() {
};
} catch (error) {
return {
bigCategoryOptions: BIG_CATEGORY_OPTIONS,
taskSourceOptions: [],
taskNatureOptions: [],
projectStatusOptions: PROJECT_STATUS_OPTIONS,
priorityOptions: [],
occurrenceFrequencyOptions: OCCURRENCE_FREQUENCY_OPTIONS,
......
......@@ -4,7 +4,6 @@ import { useContext, useEffect, useMemo, useState } from 'react';
import ReactECharts from 'echarts-for-react';
import {
formatDateLabel,
OCCURRENCE_FREQUENCY_LABEL_MAP,
PROJECT_STATUS_LABEL_MAP,
RESULT_STATUS_LABEL_MAP,
} from '../projectTaskService';
......@@ -297,28 +296,6 @@ function ChartPanel({ title, description, option, hasData }) {
);
}
function DimensionList({ items, emptyText }) {
if (items.length === 0) {
return <div className="text-sm text-zinc-400">{emptyText}</div>;
}
return (
<div className="space-y-3">
{items.map((item) => (
<div
key={item.key}
className="rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-4"
>
<div className="flex items-center justify-between gap-4">
<div className="min-w-0 text-sm font-medium text-zinc-800">{item.label}</div>
<div className="shrink-0 text-lg font-semibold text-zinc-950">{item.count}</div>
</div>
</div>
))}
</div>
);
}
export default function ResourceStatPage({ currentUsername = '' }) {
const userInfo = useContext(UserContext);
const effectiveCurrentUsername = currentUsername || userInfo?.username || userInfo?._id || '';
......@@ -400,48 +377,6 @@ export default function ResourceStatPage({ currentUsername = '' }) {
}))
.sort((left, right) => right.count - left.count);
const frequencyGroups = Object.entries(
tasks.reduce((accumulator, task) => {
const key = task.occurrenceFrequency || 'unset';
accumulator[key] = (accumulator[key] || 0) + 1;
return accumulator;
}, {}),
)
.map(([key, count]) => ({
key,
label: key === 'unset' ? '未设置' : OCCURRENCE_FREQUENCY_LABEL_MAP[key] || key,
count,
}))
.sort((left, right) => right.count - left.count);
const categoryGroups = Object.entries(
tasks.reduce((accumulator, task) => {
const key = task.bigCategory || 'unset';
accumulator[key] = (accumulator[key] || 0) + 1;
return accumulator;
}, {}),
)
.map(([key, count]) => ({
key,
label: key === 'unset' ? '未设置' : key,
count,
}))
.sort((left, right) => right.count - left.count);
const taskSourceGroups = Object.entries(
tasks.reduce((accumulator, task) => {
const key = task.taskSource || 'unset';
accumulator[key] = (accumulator[key] || 0) + 1;
return accumulator;
}, {}),
)
.map(([key, count]) => ({
key,
label: key === 'unset' ? '未设置' : key,
count,
}))
.sort((left, right) => right.count - left.count);
const progressBuckets = PROGRESS_BUCKETS.map((bucket) => ({
...bucket,
count: tasks.filter((task) => {
......@@ -495,9 +430,6 @@ export default function ResourceStatPage({ currentUsername = '' }) {
currentUserTaskCount,
statusGroups,
resultStatusGroups,
frequencyGroups,
categoryGroups,
taskSourceGroups,
progressBuckets,
memberLoad,
upcoming,
......@@ -635,7 +567,7 @@ export default function ResourceStatPage({ currentUsername = '' }) {
/>
</div>
<div className="grid gap-6 xl:grid-cols-[1fr_1fr_0.9fr]">
<div className="grid gap-6 xl:grid-cols-2">
<ChartPanel
title="进度区间分布"
description="按 `completion_percent` 分桶,快速查看项目推进层级。"
......@@ -648,29 +580,8 @@ export default function ResourceStatPage({ currentUsername = '' }) {
option={memberLoadOption}
hasData={dashboard.memberLoad.length > 0}
/>
<SectionCard
title="维度概览"
description="保留发生频率、大类、任务来源三类核心统计维度。"
bodyClassName="space-y-5"
>
<div>
<div className="mb-3 text-sm font-medium text-zinc-500">发生频率</div>
<DimensionList items={dashboard.frequencyGroups.slice(0, 3)} emptyText="暂无发生频率统计数据" />
</div>
<div>
<div className="mb-3 text-sm font-medium text-zinc-500">大类</div>
<DimensionList items={dashboard.categoryGroups.slice(0, 3)} emptyText="暂无大类统计数据" />
</div>
<div>
<div className="mb-3 text-sm font-medium text-zinc-500">任务来源</div>
<DimensionList items={dashboard.taskSourceGroups.slice(0, 3)} emptyText="暂无任务来源统计数据" />
</div>
</SectionCard>
</div>
<div className="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
<SectionCard
title="成员负载明细"
description="展示每位成员的关联项目、进行中项目和超时项目数量。"
......@@ -705,12 +616,11 @@ export default function ResourceStatPage({ currentUsername = '' }) {
</div>
</SectionCard>
<div className="space-y-6">
<SectionCard
title="即将到期"
description="按计划完成日期升序展示,便于提前提醒负责人。"
>
<div className="space-y-3">
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
{dashboard.upcoming.length > 0 ? (
dashboard.upcoming.map((task) => (
<div
......@@ -734,7 +644,7 @@ export default function ResourceStatPage({ currentUsername = '' }) {
title="超时清单"
description="按超时天数倒序展示,帮助工作台聚焦风险项目。"
>
<div className="space-y-3">
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
{dashboard.overdueList.length > 0 ? (
dashboard.overdueList.map((task) => (
<div
......@@ -754,7 +664,5 @@ export default function ResourceStatPage({ currentUsername = '' }) {
</div>
</SectionCard>
</div>
</div>
</div>
);
}
......@@ -89,12 +89,12 @@ type Record = {
| 字段标签 | 字段别名 | 字段类型 | 是否主键 | 是否多值 | 是否必填 |
| --- | --- | --- | --- | --- | --- |
| 项目名称 | `project_name` | text | 否 | 否 | 是 |
| 大类 | `big_category` | text | 否 | 否 | 否 |
| 大类 | `big_category` | select | 否 | 否 | 否 |
| 项目 | `project` | text | 否 | 否 | 否 |
| 项目负责人 | `project_owner` | user | 否 | 是 | 否 |
| 项目协作人 | `project_collaborator` | user | 否 | 是 | 否 |
| 任务来源 | `task_source` | text | 否 | 否 | 否 |
| 任务性质 | `task_nature` | text | 否 | 否 | 否 |
| 任务来源 | `task_source` | select | 否 | 否 | 否 |
| 任务性质 | `task_nature` | select | 否 | 否 | 否 |
| 发生频率 | `occurrence_frequency` | select | 否 | 否 | 否 |
| 计划开始日期 | `planned_start_date` | date | 否 | 否 | 是 |
| 计划结束日期 | `planned_end_date` | date | 否 | 否 | 是 |
......@@ -109,7 +109,22 @@ type Record = {
| 周进度记录 | `weekly_updates` | object | 否 | 否 | 否 |
| 是否属于年度审计计划 | `is_annual_audit_plan` | boolean | 否 | 否 | 否 |
### 3.1.1 `project_status` 选项
### 3.1.1 `big_category` 选项
| 显示值 | 存储值 |
| --- | --- |
| 部门管理 | `部门管理` |
| 纪检工作 | `纪检工作` |
| 审计工作 | `审计工作` |
### 3.1.2 `task_source` / `task_nature` 配置说明
- `task_source` 与 `task_nature` 为 `select` 类型
- 选项必须由 Subject 字段配置中的 `settings.options` 提供
- 前端表单只读取配置项生成下拉,不在代码中硬编码选项
- Excel 导入时应填写对应选项的存储值
### 3.1.3 `project_status` 选项
| 显示值 | 存储值 |
| --- | --- |
......@@ -119,7 +134,7 @@ type Record = {
| 受阻 | `blocked` |
| 已取消 | `cancelled` |
### 3.1.2 `occurrence_frequency` 选项
### 3.1.4 `occurrence_frequency` 选项
| 显示值 | 存储值 |
| --- | --- |
......@@ -127,13 +142,13 @@ type Record = {
| 中 | `2` |
| 低 | `1` |
### 3.1.3 `priority` 配置说明
### 3.1.5 `priority` 配置说明
- 当前最新结构中,`priority` 为 `select` 类型
- 但字段定义内暂未提供 `settings.options`
- 前端生成表单时不得硬编码选项,需等对象配置或接口返回的 `settings.options`
### 3.1.4 `weekly_updates` 子结构建议
### 3.1.6 `weekly_updates` 子结构建议
`weekly_updates` 当前字段类型为 `object`。如业务上继续沿用“周进度记录集合”方案,建议对象内维护 `items` 数组,每项至少包含以下字段:
......@@ -155,12 +170,12 @@ type Record = {
- 列表页或项目详情只展示最近一条摘要,不直接暴露底层 JSON
- 若后端最终确认 `weekly_updates` 直接存数组,可再将本节从“对象内 `items` 数组”调整为“字段直接存数组”
### 3.1.5 metadata 示例
### 3.1.7 metadata 示例
```json
{
"project_name": "某客户 2026 年专项项目",
"big_category": "审计",
"big_category": "审计工作",
"project": "2026 年专项项目",
"project_owner": [
"69b12a47e92b1de77c94a9f6"
......
......@@ -42,7 +42,7 @@ export default function SiteWrapper() {
setLoadError(error?.message || '加载站点配置失败');
setSiteInfo({
name: '项目管理系统',
name: '部门任务管理',
});
} finally {
if (!cancelled) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment