Commit c1615014 authored by David Yang's avatar David Yang

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

parent 12c2303f
...@@ -26,7 +26,7 @@ export default function LoginPage() { ...@@ -26,7 +26,7 @@ export default function LoginPage() {
const redirect = searchParams.get('redirect') || '/'; const redirect = searchParams.get('redirect') || '/';
const siteTitle = site?.title || site?.name || '项目管理系统'; const siteTitle = site?.title || site?.name || '部门任务管理';
const siteSubtitle = const siteSubtitle =
site?.subtitle || '项目下发、成员协作、甘特排期与个人日历统一协同'; site?.subtitle || '项目下发、成员协作、甘特排期与个人日历统一协同';
...@@ -141,7 +141,7 @@ export default function LoginPage() { ...@@ -141,7 +141,7 @@ export default function LoginPage() {
Sign In Sign In
</p> </p>
<h2 className="mt-3 text-3xl font-semibold text-slate-900"> <h2 className="mt-3 text-3xl font-semibold text-slate-900">
登录项目管理系统 登录部门任务管理
</h2> </h2>
<p className="mt-3 text-sm leading-6 text-slate-500"> <p className="mt-3 text-sm leading-6 text-slate-500">
参考 `streams-admin` 登录页改造,已接入验证码校验流程。 参考 `streams-admin` 登录页改造,已接入验证码校验流程。
......
...@@ -14,7 +14,7 @@ import clsx from 'clsx'; ...@@ -14,7 +14,7 @@ import clsx from 'clsx';
import { useContext, useEffect, useMemo, useState } from 'react'; import { useContext, useEffect, useMemo, useState } from 'react';
import { Link, Outlet, useLocation } from 'umi'; import { Link, Outlet, useLocation } from 'umi';
const APP_NAME = '项目管理系统'; const APP_NAME = '部门任务管理';
const menuGroups = [ const menuGroups = [
{ {
...@@ -272,9 +272,6 @@ export default function ProjectManagerLayout() { ...@@ -272,9 +272,6 @@ export default function ProjectManagerLayout() {
<h1 className="truncate text-sm font-semibold tracking-[0.02em] text-white/95"> <h1 className="truncate text-sm font-semibold tracking-[0.02em] text-white/95">
{APP_NAME} {APP_NAME}
</h1> </h1>
<div className="mt-1 text-xs text-zinc-400">
项目资源管理工作台
</div>
</div> </div>
{applicationSwitcherButton} {applicationSwitcherButton}
</> </>
......
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { import {
BIG_CATEGORY_OPTIONS,
buildImportTaskPayload, buildImportTaskPayload,
buildTaskPayload, buildTaskPayload,
OCCURRENCE_FREQUENCY_OPTIONS, OCCURRENCE_FREQUENCY_OPTIONS,
...@@ -1679,8 +1680,8 @@ function WeeklyUpdateEditorDialog({ ...@@ -1679,8 +1680,8 @@ function WeeklyUpdateEditorDialog({
</div> </div>
<div className="grid gap-4 px-6 py-5 md:grid-cols-2"> <div className="grid gap-4 px-6 py-5 md:grid-cols-2">
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">本周开始日期</span> <span className="block font-medium leading-5 text-zinc-700">本周开始日期</span>
<input <input
type="date" type="date"
value={value.week_start_date} value={value.week_start_date}
...@@ -1689,8 +1690,8 @@ function WeeklyUpdateEditorDialog({ ...@@ -1689,8 +1690,8 @@ function WeeklyUpdateEditorDialog({
/> />
</label> </label>
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">本周结束日期</span> <span className="block font-medium leading-5 text-zinc-700">本周结束日期</span>
<input <input
type="date" type="date"
value={value.week_end_date} value={value.week_end_date}
...@@ -1699,8 +1700,8 @@ function WeeklyUpdateEditorDialog({ ...@@ -1699,8 +1700,8 @@ function WeeklyUpdateEditorDialog({
/> />
</label> </label>
<label className="space-y-2 text-sm text-zinc-600 md:col-span-2"> <label className="block space-y-2.5 text-sm text-zinc-600 md:col-span-2">
<span className="font-medium">本周完成内容</span> <span className="block font-medium leading-5 text-zinc-700">本周完成内容</span>
<textarea <textarea
value={value.weekly_summary} value={value.weekly_summary}
onChange={(event) => onChange('weekly_summary', event.target.value)} onChange={(event) => onChange('weekly_summary', event.target.value)}
...@@ -1708,8 +1709,8 @@ function WeeklyUpdateEditorDialog({ ...@@ -1708,8 +1709,8 @@ function WeeklyUpdateEditorDialog({
/> />
</label> </label>
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">更新后累计进度</span> <span className="block font-medium leading-5 text-zinc-700">更新后累计进度</span>
<input <input
type="number" type="number"
min="0" min="0"
...@@ -1722,8 +1723,8 @@ function WeeklyUpdateEditorDialog({ ...@@ -1722,8 +1723,8 @@ function WeeklyUpdateEditorDialog({
/> />
</label> </label>
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">记录日期</span> <span className="block font-medium leading-5 text-zinc-700">记录日期</span>
<input <input
type="date" type="date"
value={value.updated_at} value={value.updated_at}
...@@ -1732,8 +1733,8 @@ function WeeklyUpdateEditorDialog({ ...@@ -1732,8 +1733,8 @@ function WeeklyUpdateEditorDialog({
/> />
</label> </label>
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">当前阻塞事项</span> <span className="block font-medium leading-5 text-zinc-700">当前阻塞事项</span>
<textarea <textarea
value={value.blockers} value={value.blockers}
onChange={(event) => onChange('blockers', event.target.value)} onChange={(event) => onChange('blockers', event.target.value)}
...@@ -1741,8 +1742,8 @@ function WeeklyUpdateEditorDialog({ ...@@ -1741,8 +1742,8 @@ function WeeklyUpdateEditorDialog({
/> />
</label> </label>
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">下周计划</span> <span className="block font-medium leading-5 text-zinc-700">下周计划</span>
<textarea <textarea
value={value.next_week_plan} value={value.next_week_plan}
onChange={(event) => onChange('next_week_plan', event.target.value)} onChange={(event) => onChange('next_week_plan', event.target.value)}
...@@ -2082,6 +2083,12 @@ function ProjectTaskDialog({ ...@@ -2082,6 +2083,12 @@ function ProjectTaskDialog({
? formValues.weekly_updates ? formValues.weekly_updates
: []; : [];
const priorityOptions = fieldOptions?.priorityOptions || []; const priorityOptions = fieldOptions?.priorityOptions || [];
const bigCategoryOptions =
fieldOptions?.bigCategoryOptions?.length > 0
? fieldOptions.bigCategoryOptions
: BIG_CATEGORY_OPTIONS;
const taskSourceOptions = fieldOptions?.taskSourceOptions || [];
const taskNatureOptions = fieldOptions?.taskNatureOptions || [];
const projectStatusOptions = const projectStatusOptions =
fieldOptions?.projectStatusOptions?.length > 0 fieldOptions?.projectStatusOptions?.length > 0
? fieldOptions.projectStatusOptions ? fieldOptions.projectStatusOptions
...@@ -2097,7 +2104,7 @@ function ProjectTaskDialog({ ...@@ -2097,7 +2104,7 @@ function ProjectTaskDialog({
<> <>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/45 px-4 py-6"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/45 px-4 py-6">
<div className="max-h-[92vh] w-full max-w-5xl overflow-hidden rounded-[28px] bg-white shadow-[0_32px_100px_rgba(15,23,42,0.28)]"> <div className="max-h-[92vh] w-full max-w-5xl overflow-hidden rounded-[28px] bg-white shadow-[0_32px_100px_rgba(15,23,42,0.28)]">
<div className="flex items-center justify-between border-b border-zinc-200 px-6 py-5"> <div className="flex items-center justify-between border-b border-zinc-200 px-7 py-6">
<div> <div>
<div className="text-xl font-semibold text-zinc-900"> <div className="text-xl font-semibold text-zinc-900">
{task ? '编辑项目' : '新建项目'} {task ? '编辑项目' : '新建项目'}
...@@ -2140,11 +2147,11 @@ function ProjectTaskDialog({ ...@@ -2140,11 +2147,11 @@ function ProjectTaskDialog({
event.preventDefault(); event.preventDefault();
onSubmit(formValues); onSubmit(formValues);
}} }}
className="max-h-[calc(92vh-84px)] overflow-auto px-6 py-5" className="max-h-[calc(92vh-92px)] overflow-auto px-7 pb-6 pt-7"
> >
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-x-4 gap-y-5 md:grid-cols-2">
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">项目名称</span> <span className="block font-medium leading-5 text-zinc-700">项目名称</span>
<input <input
value={formValues.project_name} value={formValues.project_name}
onChange={(event) => updateField('project_name', event.target.value)} onChange={(event) => updateField('project_name', event.target.value)}
...@@ -2153,17 +2160,24 @@ function ProjectTaskDialog({ ...@@ -2153,17 +2160,24 @@ function ProjectTaskDialog({
/> />
</label> </label>
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">大类</span> <span className="block font-medium leading-5 text-zinc-700">大类</span>
<input <select
value={formValues.big_category} value={formValues.big_category}
onChange={(event) => updateField('big_category', event.target.value)} onChange={(event) => updateField('big_category', event.target.value)}
className="h-11 w-full rounded-xl border border-zinc-200 px-3 outline-none transition focus:border-rose-300" className="h-11 w-full rounded-xl border border-zinc-200 bg-white px-3 outline-none transition focus:border-rose-300"
/> >
<option value="">请选择大类</option>
{bigCategoryOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label> </label>
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">项目</span> <span className="block font-medium leading-5 text-zinc-700">项目</span>
<input <input
value={formValues.project} value={formValues.project}
onChange={(event) => updateField('project', event.target.value)} onChange={(event) => updateField('project', event.target.value)}
...@@ -2171,8 +2185,8 @@ function ProjectTaskDialog({ ...@@ -2171,8 +2185,8 @@ function ProjectTaskDialog({
/> />
</label> </label>
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">项目负责人</span> <span className="block font-medium leading-5 text-zinc-700">项目负责人</span>
<UserMultiSearchSelect <UserMultiSearchSelect
value={formValues.project_owner} value={formValues.project_owner}
onChange={(value) => updateField('project_owner', value)} onChange={(value) => updateField('project_owner', value)}
...@@ -2180,8 +2194,8 @@ function ProjectTaskDialog({ ...@@ -2180,8 +2194,8 @@ function ProjectTaskDialog({
/> />
</label> </label>
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">项目协作人</span> <span className="block font-medium leading-5 text-zinc-700">项目协作人</span>
<UserMultiSearchSelect <UserMultiSearchSelect
value={formValues.project_collaborator} value={formValues.project_collaborator}
onChange={(value) => updateField('project_collaborator', value)} onChange={(value) => updateField('project_collaborator', value)}
...@@ -2189,26 +2203,44 @@ function ProjectTaskDialog({ ...@@ -2189,26 +2203,44 @@ function ProjectTaskDialog({
/> />
</label> </label>
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">任务来源</span> <span className="block font-medium leading-5 text-zinc-700">任务来源</span>
<input <select
value={formValues.task_source} value={formValues.task_source}
onChange={(event) => updateField('task_source', event.target.value)} onChange={(event) => updateField('task_source', event.target.value)}
className="h-11 w-full rounded-xl border border-zinc-200 px-3 outline-none transition focus:border-rose-300" className="h-11 w-full rounded-xl border border-zinc-200 bg-white px-3 outline-none transition focus:border-rose-300"
/> >
<option value="">
{taskSourceOptions.length > 0 ? '请选择任务来源' : '请先配置任务来源选项'}
</option>
{taskSourceOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label> </label>
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">任务性质</span> <span className="block font-medium leading-5 text-zinc-700">任务性质</span>
<input <select
value={formValues.task_nature} value={formValues.task_nature}
onChange={(event) => updateField('task_nature', event.target.value)} onChange={(event) => updateField('task_nature', event.target.value)}
className="h-11 w-full rounded-xl border border-zinc-200 px-3 outline-none transition focus:border-rose-300" className="h-11 w-full rounded-xl border border-zinc-200 bg-white px-3 outline-none transition focus:border-rose-300"
/> >
<option value="">
{taskNatureOptions.length > 0 ? '请选择任务性质' : '请先配置任务性质选项'}
</option>
{taskNatureOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label> </label>
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">发生频率</span> <span className="block font-medium leading-5 text-zinc-700">发生频率</span>
<select <select
value={formValues.occurrence_frequency} value={formValues.occurrence_frequency}
onChange={(event) => updateField('occurrence_frequency', event.target.value)} onChange={(event) => updateField('occurrence_frequency', event.target.value)}
...@@ -2223,8 +2255,8 @@ function ProjectTaskDialog({ ...@@ -2223,8 +2255,8 @@ function ProjectTaskDialog({
</select> </select>
</label> </label>
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">优先级</span> <span className="block font-medium leading-5 text-zinc-700">优先级</span>
<select <select
value={formValues.priority} value={formValues.priority}
onChange={(event) => updateField('priority', event.target.value)} onChange={(event) => updateField('priority', event.target.value)}
...@@ -2239,8 +2271,8 @@ function ProjectTaskDialog({ ...@@ -2239,8 +2271,8 @@ function ProjectTaskDialog({
</select> </select>
</label> </label>
<div className="space-y-2 text-sm text-zinc-600"> <div className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">是否属于年度审计计划</span> <span className="block font-medium leading-5 text-zinc-700">是否属于年度审计计划</span>
<div className="rounded-2xl border border-zinc-200 bg-white px-3 py-3"> <div className="rounded-2xl border border-zinc-200 bg-white px-3 py-3">
<label className="flex items-start gap-3"> <label className="flex items-start gap-3">
<input <input
...@@ -2258,8 +2290,8 @@ function ProjectTaskDialog({ ...@@ -2258,8 +2290,8 @@ function ProjectTaskDialog({
</div> </div>
</div> </div>
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">计划开始日期</span> <span className="block font-medium leading-5 text-zinc-700">计划开始日期</span>
<input <input
type="date" type="date"
value={formValues.planned_start_date} value={formValues.planned_start_date}
...@@ -2269,8 +2301,8 @@ function ProjectTaskDialog({ ...@@ -2269,8 +2301,8 @@ function ProjectTaskDialog({
/> />
</label> </label>
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">计划结束日期</span> <span className="block font-medium leading-5 text-zinc-700">计划结束日期</span>
<input <input
type="date" type="date"
value={formValues.planned_end_date} value={formValues.planned_end_date}
...@@ -2280,8 +2312,8 @@ function ProjectTaskDialog({ ...@@ -2280,8 +2312,8 @@ function ProjectTaskDialog({
/> />
</label> </label>
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">实际开始日期</span> <span className="block font-medium leading-5 text-zinc-700">实际开始日期</span>
<input <input
type="date" type="date"
value={formValues.actual_start_date} value={formValues.actual_start_date}
...@@ -2290,8 +2322,8 @@ function ProjectTaskDialog({ ...@@ -2290,8 +2322,8 @@ function ProjectTaskDialog({
/> />
</label> </label>
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">实际完成日期</span> <span className="block font-medium leading-5 text-zinc-700">实际完成日期</span>
<input <input
type="date" type="date"
value={formValues.actual_finish_date} value={formValues.actual_finish_date}
...@@ -2301,9 +2333,9 @@ function ProjectTaskDialog({ ...@@ -2301,9 +2333,9 @@ function ProjectTaskDialog({
</label> </label>
</div> </div>
<div className="mt-5 grid gap-4 md:grid-cols-2"> <div className="mt-6 grid gap-x-4 gap-y-5 md:grid-cols-2">
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">项目状态</span> <span className="block font-medium leading-5 text-zinc-700">项目状态</span>
<select <select
value={formValues.project_status} value={formValues.project_status}
onChange={(event) => updateField('project_status', event.target.value)} onChange={(event) => updateField('project_status', event.target.value)}
...@@ -2321,8 +2353,8 @@ function ProjectTaskDialog({ ...@@ -2321,8 +2353,8 @@ function ProjectTaskDialog({
</div> </div>
</label> </label>
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">完成进度</span> <span className="block font-medium leading-5 text-zinc-700">完成进度</span>
<input <input
type="number" type="number"
min="0" min="0"
...@@ -2334,9 +2366,9 @@ function ProjectTaskDialog({ ...@@ -2334,9 +2366,9 @@ function ProjectTaskDialog({
</label> </label>
</div> </div>
<div className="mt-5 grid gap-4 md:grid-cols-2"> <div className="mt-6 grid gap-x-4 gap-y-5 md:grid-cols-2">
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">项目说明</span> <span className="block font-medium leading-5 text-zinc-700">项目说明</span>
<textarea <textarea
value={formValues.description} value={formValues.description}
onChange={(event) => updateField('description', event.target.value)} onChange={(event) => updateField('description', event.target.value)}
...@@ -2344,8 +2376,8 @@ function ProjectTaskDialog({ ...@@ -2344,8 +2376,8 @@ function ProjectTaskDialog({
/> />
</label> </label>
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">备注</span> <span className="block font-medium leading-5 text-zinc-700">备注</span>
<textarea <textarea
value={formValues.remark} value={formValues.remark}
onChange={(event) => updateField('remark', event.target.value)} onChange={(event) => updateField('remark', event.target.value)}
...@@ -2354,9 +2386,9 @@ function ProjectTaskDialog({ ...@@ -2354,9 +2386,9 @@ function ProjectTaskDialog({
</label> </label>
</div> </div>
<div className="mt-5 grid gap-4 md:grid-cols-2"> <div className="mt-6 grid gap-x-4 gap-y-5 md:grid-cols-2">
<label className="space-y-2 text-sm text-zinc-600"> <label className="block space-y-2.5 text-sm text-zinc-600">
<span className="font-medium">延期原因</span> <span className="block font-medium leading-5 text-zinc-700">延期原因</span>
<textarea <textarea
value={formValues.delay_reason} value={formValues.delay_reason}
onChange={(event) => updateField('delay_reason', event.target.value)} onChange={(event) => updateField('delay_reason', event.target.value)}
...@@ -2365,7 +2397,7 @@ function ProjectTaskDialog({ ...@@ -2365,7 +2397,7 @@ function ProjectTaskDialog({
</label> </label>
</div> </div>
<div className="mt-5 rounded-2xl border border-zinc-200/90 bg-zinc-50/60 p-4"> <div className="mt-6 rounded-2xl border border-zinc-200/90 bg-zinc-50/60 p-4">
<div className="flex flex-wrap items-start justify-between gap-3"> <div className="flex flex-wrap items-start justify-between gap-3">
<div className="text-sm font-medium text-zinc-900">周进度记录</div> <div className="text-sm font-medium text-zinc-900">周进度记录</div>
<button <button
...@@ -2551,6 +2583,9 @@ export default function ProjectBookingWorkspace({ currentUsername }) { ...@@ -2551,6 +2583,9 @@ export default function ProjectBookingWorkspace({ currentUsername }) {
const [importLoading, setImportLoading] = useState(false); const [importLoading, setImportLoading] = useState(false);
const [importFileName, setImportFileName] = useState(''); const [importFileName, setImportFileName] = useState('');
const [fieldOptions, setFieldOptions] = useState({ const [fieldOptions, setFieldOptions] = useState({
bigCategoryOptions: BIG_CATEGORY_OPTIONS,
taskSourceOptions: [],
taskNatureOptions: [],
projectStatusOptions: PROJECT_STATUS_OPTIONS, projectStatusOptions: PROJECT_STATUS_OPTIONS,
priorityOptions: [], priorityOptions: [],
occurrenceFrequencyOptions: OCCURRENCE_FREQUENCY_OPTIONS, occurrenceFrequencyOptions: OCCURRENCE_FREQUENCY_OPTIONS,
......
...@@ -35,12 +35,12 @@ ...@@ -35,12 +35,12 @@
关键字段: 关键字段:
- `project_name`:项目名称 - `project_name`:项目名称
- `big_category`:大类 - `big_category`:大类,选择项为部门管理、纪检工作、审计工作
- `project`:项目归属或项目分类补充字段 - `project`:项目归属或项目分类补充字段
- `project_owner`:项目负责人,直接关联系统用户,多值 - `project_owner`:项目负责人,直接关联系统用户,多值
- `project_collaborator`:项目协作人,直接关联系统用户,多值 - `project_collaborator`:项目协作人,直接关联系统用户,多值
- `task_source`:任务来源 - `task_source`:任务来源,选择项由 Subject 字段配置 `settings.options` 提供
- `task_nature`:任务性质 - `task_nature`:任务性质,选择项由 Subject 字段配置 `settings.options` 提供
- `occurrence_frequency`:发生频率 - `occurrence_frequency`:发生频率
- `planned_start_date` / `planned_end_date`:计划时间 - `planned_start_date` / `planned_end_date`:计划时间
- `actual_start_date` / `actual_finish_date`:实际时间 - `actual_start_date` / `actual_finish_date`:实际时间
...@@ -59,7 +59,7 @@ ...@@ -59,7 +59,7 @@
1. 领导创建一条 `pm_project_task` 1. 领导创建一条 `pm_project_task`
2. 填写项目名称、计划开始日期、计划结束日期 2. 填写项目名称、计划开始日期、计划结束日期
3. 按需要补充大类、项目、任务来源、任务性质、发生频率 3. 按需要选择大类、任务来源、任务性质、发生频率,并补充项目
4. 按需勾选是否属于年度审计计划 `is_annual_audit_plan` 4. 按需勾选是否属于年度审计计划 `is_annual_audit_plan`
5. 选择一个或多个项目负责人 `project_owner` 5. 选择一个或多个项目负责人 `project_owner`
6. 选择一个或多个项目协作人 `project_collaborator` 6. 选择一个或多个项目协作人 `project_collaborator`
...@@ -128,28 +128,25 @@ ...@@ -128,28 +128,25 @@
- 结果状态分布柱状图 - 结果状态分布柱状图
- 进度区间分布柱状图 - 进度区间分布柱状图
- 成员负载 Top N 横向柱状图 - 成员负载 Top N 横向柱状图
- 维度概览区保留列表化统计: - “维度概览”区块当前意义不明确,资源统计看板暂不展示该模块;发生频率、大类、任务来源等维度待统计口径明确后再补充为独立图表或筛选项
- 发生频率
- 大类
- 任务来源
- 风险清单区展示: - 风险清单区展示:
- 即将到期项目 - 即将到期项目
- 超时项目清单 - 超时项目清单
- 风险清单区位于成员负载明细下方,采用独立全宽区块展示,避免作为右侧纵列造成页面左右高度失衡
- 建议补充统计维度: - 建议补充统计维度:
- 项目状态分布 - 项目状态分布
- 结果状态分布(按时间规则实时派生) - 结果状态分布(按时间规则实时派生)
- 发生频率分布
- 大类分布
- 任务来源分布
- 负责人/协作人负载分布 - 负责人/协作人负载分布
- 即将到期项目 - 即将到期项目
- 超时项目清单 - 超时项目清单
- 统计口径以 `pm_project_task.metadata` 的当前字段为准,优先展示: - 统计口径以 `pm_project_task.metadata` 的当前字段为准,优先展示:
- `project_status` - `project_status`
- `occurrence_frequency` - `completion_percent`
- `big_category` - `project_owner`
- `task_source` - `project_collaborator`
- `planned_end_date`
- `actual_finish_date`
### 5.1 甘特图 ### 5.1 甘特图
...@@ -229,6 +226,8 @@ const result = await queryUsers(0, 20, { ...@@ -229,6 +226,8 @@ const result = await queryUsers(0, 20, {
## 6.3 Excel 批量导入口径 ## 6.3 Excel 批量导入口径
- Excel 模板采用业务列头:`大类`、`项目`、`是否属于年度审计计划`、`发生频率`、`任务名称`、`任务来源`、`任务性质`、`计划启动时间`、`计划完成时间`、`时间进度`、`任务进度`、`任务状态`、`主办人`、`协办人1` - Excel 模板采用业务列头:`大类`、`项目`、`是否属于年度审计计划`、`发生频率`、`任务名称`、`任务来源`、`任务性质`、`计划启动时间`、`计划完成时间`、`时间进度`、`任务进度`、`任务状态`、`主办人`、`协办人1`
- `大类` 导入到 `metadata.big_category`,当前仅支持 `部门管理`、`纪检工作`、`审计工作`
- `任务来源` / `任务性质` 导入到 `metadata.task_source` / `metadata.task_nature`,应填写对应 Subject 字段 `settings.options` 中配置的存储值
- `任务名称` 导入到 `metadata.project_name` - `任务名称` 导入到 `metadata.project_name`
- `是否属于年度审计计划` 导入到 `metadata.is_annual_audit_plan`,支持 `是/否`、`true/false`、`1/0` - `是否属于年度审计计划` 导入到 `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` 数组 - `主办人` 导入到 `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 = [ ...@@ -28,6 +28,12 @@ export const OCCURRENCE_FREQUENCY_OPTIONS = [
{ label: '低', value: '1' }, { label: '低', value: '1' },
]; ];
export const BIG_CATEGORY_OPTIONS = [
{ label: '部门管理', value: '部门管理' },
{ label: '纪检工作', value: '纪检工作' },
{ label: '审计工作', value: '审计工作' },
];
export const PRIORITY_OPTIONS = [ export const PRIORITY_OPTIONS = [
{ label: '高', value: 'high' }, { label: '高', value: 'high' },
{ label: '中', value: 'medium' }, { label: '中', value: 'medium' },
...@@ -331,11 +337,20 @@ function extractSubjectField(subject, fieldName) { ...@@ -331,11 +337,20 @@ function extractSubjectField(subject, fieldName) {
export async function loadProjectTaskFieldOptions() { export async function loadProjectTaskFieldOptions() {
try { try {
const subject = await loadSubject(PROJECT_TASK_SUBJECT); 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 projectStatusField = extractSubjectField(subject, 'project_status');
const priorityField = extractSubjectField(subject, 'priority'); const priorityField = extractSubjectField(subject, 'priority');
const occurrenceFrequencyField = extractSubjectField(subject, 'occurrence_frequency'); const occurrenceFrequencyField = extractSubjectField(subject, 'occurrence_frequency');
return { 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: projectStatusOptions:
normalizeSelectOptions(projectStatusField?.settings?.options).length > 0 normalizeSelectOptions(projectStatusField?.settings?.options).length > 0
? normalizeSelectOptions(projectStatusField?.settings?.options) ? normalizeSelectOptions(projectStatusField?.settings?.options)
...@@ -348,6 +363,9 @@ export async function loadProjectTaskFieldOptions() { ...@@ -348,6 +363,9 @@ export async function loadProjectTaskFieldOptions() {
}; };
} catch (error) { } catch (error) {
return { return {
bigCategoryOptions: BIG_CATEGORY_OPTIONS,
taskSourceOptions: [],
taskNatureOptions: [],
projectStatusOptions: PROJECT_STATUS_OPTIONS, projectStatusOptions: PROJECT_STATUS_OPTIONS,
priorityOptions: [], priorityOptions: [],
occurrenceFrequencyOptions: OCCURRENCE_FREQUENCY_OPTIONS, occurrenceFrequencyOptions: OCCURRENCE_FREQUENCY_OPTIONS,
......
...@@ -4,7 +4,6 @@ import { useContext, useEffect, useMemo, useState } from 'react'; ...@@ -4,7 +4,6 @@ import { useContext, useEffect, useMemo, useState } from 'react';
import ReactECharts from 'echarts-for-react'; import ReactECharts from 'echarts-for-react';
import { import {
formatDateLabel, formatDateLabel,
OCCURRENCE_FREQUENCY_LABEL_MAP,
PROJECT_STATUS_LABEL_MAP, PROJECT_STATUS_LABEL_MAP,
RESULT_STATUS_LABEL_MAP, RESULT_STATUS_LABEL_MAP,
} from '../projectTaskService'; } from '../projectTaskService';
...@@ -297,28 +296,6 @@ function ChartPanel({ title, description, option, hasData }) { ...@@ -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 = '' }) { export default function ResourceStatPage({ currentUsername = '' }) {
const userInfo = useContext(UserContext); const userInfo = useContext(UserContext);
const effectiveCurrentUsername = currentUsername || userInfo?.username || userInfo?._id || ''; const effectiveCurrentUsername = currentUsername || userInfo?.username || userInfo?._id || '';
...@@ -400,48 +377,6 @@ export default function ResourceStatPage({ currentUsername = '' }) { ...@@ -400,48 +377,6 @@ export default function ResourceStatPage({ currentUsername = '' }) {
})) }))
.sort((left, right) => right.count - left.count); .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) => ({ const progressBuckets = PROGRESS_BUCKETS.map((bucket) => ({
...bucket, ...bucket,
count: tasks.filter((task) => { count: tasks.filter((task) => {
...@@ -495,9 +430,6 @@ export default function ResourceStatPage({ currentUsername = '' }) { ...@@ -495,9 +430,6 @@ export default function ResourceStatPage({ currentUsername = '' }) {
currentUserTaskCount, currentUserTaskCount,
statusGroups, statusGroups,
resultStatusGroups, resultStatusGroups,
frequencyGroups,
categoryGroups,
taskSourceGroups,
progressBuckets, progressBuckets,
memberLoad, memberLoad,
upcoming, upcoming,
...@@ -635,7 +567,7 @@ export default function ResourceStatPage({ currentUsername = '' }) { ...@@ -635,7 +567,7 @@ export default function ResourceStatPage({ currentUsername = '' }) {
/> />
</div> </div>
<div className="grid gap-6 xl:grid-cols-[1fr_1fr_0.9fr]"> <div className="grid gap-6 xl:grid-cols-2">
<ChartPanel <ChartPanel
title="进度区间分布" title="进度区间分布"
description="按 `completion_percent` 分桶,快速查看项目推进层级。" description="按 `completion_percent` 分桶,快速查看项目推进层级。"
...@@ -648,29 +580,8 @@ export default function ResourceStatPage({ currentUsername = '' }) { ...@@ -648,29 +580,8 @@ export default function ResourceStatPage({ currentUsername = '' }) {
option={memberLoadOption} option={memberLoadOption}
hasData={dashboard.memberLoad.length > 0} 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>
<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 <SectionCard
title="成员负载明细" title="成员负载明细"
description="展示每位成员的关联项目、进行中项目和超时项目数量。" description="展示每位成员的关联项目、进行中项目和超时项目数量。"
...@@ -705,12 +616,11 @@ export default function ResourceStatPage({ currentUsername = '' }) { ...@@ -705,12 +616,11 @@ export default function ResourceStatPage({ currentUsername = '' }) {
</div> </div>
</SectionCard> </SectionCard>
<div className="space-y-6">
<SectionCard <SectionCard
title="即将到期" title="即将到期"
description="按计划完成日期升序展示,便于提前提醒负责人。" 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.length > 0 ? (
dashboard.upcoming.map((task) => ( dashboard.upcoming.map((task) => (
<div <div
...@@ -734,7 +644,7 @@ export default function ResourceStatPage({ currentUsername = '' }) { ...@@ -734,7 +644,7 @@ export default function ResourceStatPage({ currentUsername = '' }) {
title="超时清单" title="超时清单"
description="按超时天数倒序展示,帮助工作台聚焦风险项目。" 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.length > 0 ? (
dashboard.overdueList.map((task) => ( dashboard.overdueList.map((task) => (
<div <div
...@@ -754,7 +664,5 @@ export default function ResourceStatPage({ currentUsername = '' }) { ...@@ -754,7 +664,5 @@ export default function ResourceStatPage({ currentUsername = '' }) {
</div> </div>
</SectionCard> </SectionCard>
</div> </div>
</div>
</div>
); );
} }
...@@ -89,12 +89,12 @@ type Record = { ...@@ -89,12 +89,12 @@ type Record = {
| 字段标签 | 字段别名 | 字段类型 | 是否主键 | 是否多值 | 是否必填 | | 字段标签 | 字段别名 | 字段类型 | 是否主键 | 是否多值 | 是否必填 |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 项目名称 | `project_name` | text | 否 | 否 | 是 | | 项目名称 | `project_name` | text | 否 | 否 | 是 |
| 大类 | `big_category` | text | 否 | 否 | 否 | | 大类 | `big_category` | select | 否 | 否 | 否 |
| 项目 | `project` | text | 否 | 否 | 否 | | 项目 | `project` | text | 否 | 否 | 否 |
| 项目负责人 | `project_owner` | user | 否 | 是 | 否 | | 项目负责人 | `project_owner` | user | 否 | 是 | 否 |
| 项目协作人 | `project_collaborator` | user | 否 | 是 | 否 | | 项目协作人 | `project_collaborator` | user | 否 | 是 | 否 |
| 任务来源 | `task_source` | text | 否 | 否 | 否 | | 任务来源 | `task_source` | select | 否 | 否 | 否 |
| 任务性质 | `task_nature` | text | 否 | 否 | 否 | | 任务性质 | `task_nature` | select | 否 | 否 | 否 |
| 发生频率 | `occurrence_frequency` | select | 否 | 否 | 否 | | 发生频率 | `occurrence_frequency` | select | 否 | 否 | 否 |
| 计划开始日期 | `planned_start_date` | date | 否 | 否 | 是 | | 计划开始日期 | `planned_start_date` | date | 否 | 否 | 是 |
| 计划结束日期 | `planned_end_date` | date | 否 | 否 | 是 | | 计划结束日期 | `planned_end_date` | date | 否 | 否 | 是 |
...@@ -109,7 +109,22 @@ type Record = { ...@@ -109,7 +109,22 @@ type Record = {
| 周进度记录 | `weekly_updates` | object | 否 | 否 | 否 | | 周进度记录 | `weekly_updates` | object | 否 | 否 | 否 |
| 是否属于年度审计计划 | `is_annual_audit_plan` | boolean | 否 | 否 | 否 | | 是否属于年度审计计划 | `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 = { ...@@ -119,7 +134,7 @@ type Record = {
| 受阻 | `blocked` | | 受阻 | `blocked` |
| 已取消 | `cancelled` | | 已取消 | `cancelled` |
### 3.1.2 `occurrence_frequency` 选项 ### 3.1.4 `occurrence_frequency` 选项
| 显示值 | 存储值 | | 显示值 | 存储值 |
| --- | --- | | --- | --- |
...@@ -127,13 +142,13 @@ type Record = { ...@@ -127,13 +142,13 @@ type Record = {
| 中 | `2` | | 中 | `2` |
| 低 | `1` | | 低 | `1` |
### 3.1.3 `priority` 配置说明 ### 3.1.5 `priority` 配置说明
- 当前最新结构中,`priority` 为 `select` 类型 - 当前最新结构中,`priority` 为 `select` 类型
- 但字段定义内暂未提供 `settings.options` - 但字段定义内暂未提供 `settings.options`
- 前端生成表单时不得硬编码选项,需等对象配置或接口返回的 `settings.options` - 前端生成表单时不得硬编码选项,需等对象配置或接口返回的 `settings.options`
### 3.1.4 `weekly_updates` 子结构建议 ### 3.1.6 `weekly_updates` 子结构建议
`weekly_updates` 当前字段类型为 `object`。如业务上继续沿用“周进度记录集合”方案,建议对象内维护 `items` 数组,每项至少包含以下字段: `weekly_updates` 当前字段类型为 `object`。如业务上继续沿用“周进度记录集合”方案,建议对象内维护 `items` 数组,每项至少包含以下字段:
...@@ -155,12 +170,12 @@ type Record = { ...@@ -155,12 +170,12 @@ type Record = {
- 列表页或项目详情只展示最近一条摘要,不直接暴露底层 JSON - 列表页或项目详情只展示最近一条摘要,不直接暴露底层 JSON
- 若后端最终确认 `weekly_updates` 直接存数组,可再将本节从“对象内 `items` 数组”调整为“字段直接存数组” - 若后端最终确认 `weekly_updates` 直接存数组,可再将本节从“对象内 `items` 数组”调整为“字段直接存数组”
### 3.1.5 metadata 示例 ### 3.1.7 metadata 示例
```json ```json
{ {
"project_name": "某客户 2026 年专项项目", "project_name": "某客户 2026 年专项项目",
"big_category": "审计", "big_category": "审计工作",
"project": "2026 年专项项目", "project": "2026 年专项项目",
"project_owner": [ "project_owner": [
"69b12a47e92b1de77c94a9f6" "69b12a47e92b1de77c94a9f6"
......
...@@ -42,7 +42,7 @@ export default function SiteWrapper() { ...@@ -42,7 +42,7 @@ export default function SiteWrapper() {
setLoadError(error?.message || '加载站点配置失败'); setLoadError(error?.message || '加载站点配置失败');
setSiteInfo({ setSiteInfo({
name: '项目管理系统', name: '部门任务管理',
}); });
} finally { } finally {
if (!cancelled) { 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