Commit 12c2303f authored by David Yang's avatar David Yang

feat: 应用切换

parent ca0cbf30
File added
......@@ -14,6 +14,8 @@ const ROUTE_PATHS = {
export default defineConfig({
layout: false,
fastRefresh: true,
base: '/project_manager_xy/',
publicPath: '/project_manager_xy/',
alias: {
react: resolvePackageRoot('react'),
'react-dom': resolvePackageRoot('react-dom'),
......
This diff is collapsed.
......@@ -8,6 +8,7 @@
"start": "npm run dev"
},
"dependencies": {
"@headlessui/react": "^2.2.10",
"@heroicons/react": "^2.2.0",
"@tailwindcss/postcss": "^4.2.0",
"@umijs/max": "^4.5.3",
......
{
"name": "project_manager_xy",
"version": "1.0.3",
"build": "abc",
"description": "merge master",
"date": "2025-07-14 19:27:20",
"author": "yang"
}
File added
This diff is collapsed.
import ApplicationSwitcherDialog from '@/pages/project_manager_xy/ApplicationSwitcherDialog';
import xyLogo from '@/assets/xy-logo.png';
import xyLogoSmall from '@/assets/xy-logo_small.png';
import { UserContext } from '@/wrapper/Auth';
import { SiteContext } from '@/wrapper/Site';
import {
Bars3Icon,
CalendarDaysIcon,
ChartBarSquareIcon,
Bars3Icon,
Squares2X2Icon,
XMarkIcon,
} from '@heroicons/react/24/outline';
import clsx from 'clsx';
......@@ -174,10 +177,12 @@ function MobileSidebar({ open, close, children }) {
export default function ProjectManagerLayout() {
const location = useLocation();
const userInfo = useContext(UserContext);
const siteInfo = useContext(SiteContext);
const [isMobile, setIsMobile] = useState(false);
const [isApplicationDialogOpen, setIsApplicationDialogOpen] = useState(false);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
const userInfo = useContext(UserContext);
useEffect(() => {
if (typeof window === 'undefined') {
......@@ -219,6 +224,23 @@ export default function ProjectManagerLayout() {
);
}, [location.pathname]);
const applicationSwitcherButton = (
<button
type="button"
onClick={() => setIsApplicationDialogOpen(true)}
aria-label="Open application switcher"
title="应用切换"
className={clsx(
'inline-flex items-center justify-center rounded-[20px] border border-[#f97316]/70 bg-[linear-gradient(180deg,rgba(39,39,42,0.92),rgba(24,24,27,0.96))] text-[#fb923c] shadow-[0_16px_30px_rgba(249,115,22,0.16)] transition duration-200 hover:-translate-y-0.5 hover:border-[#fb923c] hover:text-[#fdba74] hover:shadow-[0_18px_36px_rgba(249,115,22,0.26)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#fb923c]/50',
sidebarCollapsed ? 'size-11' : 'size-14',
)}
>
<Squares2X2Icon
className={clsx(sidebarCollapsed ? 'size-5' : 'size-7')}
/>
</button>
);
return (
<div className="flex h-screen overflow-hidden bg-[radial-gradient(circle_at_top_left,_rgba(251,191,188,0.22),_transparent_26%),radial-gradient(circle_at_bottom_right,_rgba(251,207,232,0.2),_transparent_30%),#f8fafc]">
<aside
......@@ -229,25 +251,34 @@ export default function ProjectManagerLayout() {
>
<div
className={clsx(
'mb-7 flex items-center',
sidebarCollapsed ? 'justify-center' : 'gap-3 px-1',
'mb-7',
sidebarCollapsed
? 'flex flex-col items-center gap-3'
: 'flex items-start justify-between gap-4 px-1',
)}
>
<img
src={sidebarCollapsed ? xyLogoSmall : xyLogo}
alt="XY Logo"
className={clsx(
'shrink-0 object-contain',
sidebarCollapsed
? 'h-8 w-auto max-w-8'
: 'h-8 w-auto max-w-[140px]',
)}
/>
{!sidebarCollapsed ? (
<h1 className="truncate text-sm font-semibold tracking-[0.02em] text-white/95">
{APP_NAME}
</h1>
) : null}
{sidebarCollapsed ? (
<>
<img
src={xyLogoSmall}
alt="XY Logo"
className="h-8 w-auto max-w-8 shrink-0 object-contain"
/>
{applicationSwitcherButton}
</>
) : (
<>
<div className="min-w-0 flex-1">
<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}
</>
)}
</div>
<nav className="flex-1 space-y-5 overflow-y-auto">
......@@ -344,12 +375,26 @@ export default function ProjectManagerLayout() {
<Bars3Icon className="size-5" />
</NavbarItem>
<div className="text-sm text-zinc-500">{currentGroupTitle}</div>
<NavbarItem
onClick={() => setIsApplicationDialogOpen(true)}
aria-label="Open application switcher"
className="border border-rose-200 bg-white text-rose-500 shadow-sm hover:bg-rose-50 hover:text-rose-600"
>
<Squares2X2Icon className="size-5" />
</NavbarItem>
</div>
<div className="flex-1 overflow-y-auto rounded-xl border border-zinc-200 bg-white/95 p-4 shadow-[0_20px_50px_rgba(15,23,42,0.06)] sm:p-6">
<Outlet />
</div>
</main>
<ApplicationSwitcherDialog
open={isApplicationDialogOpen}
onClose={() => setIsApplicationDialogOpen(false)}
userInfo={userInfo}
siteInfo={siteInfo}
/>
</div>
);
}
......@@ -23,6 +23,7 @@ import {
saveProjectTask,
searchSystemUsers,
sortWeeklyUpdates,
deleteProjectTask,
} from './projectTaskService';
function resolveCurrentDate() {
......@@ -1920,9 +1921,11 @@ function ProjectTaskDialog({
open,
task,
submitting,
deleting,
submitError,
onClose,
onSubmit,
onDelete,
fieldOptions,
}) {
const [formValues, setFormValues] = useState(buildTaskFormValues(null));
......@@ -1932,6 +1935,7 @@ function ProjectTaskDialog({
buildWeeklyUpdateFormValues(null),
);
const [weeklyUpdateError, setWeeklyUpdateError] = useState('');
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
useEffect(() => {
if (!open) {
......@@ -1942,6 +1946,7 @@ function ProjectTaskDialog({
setEditingWeeklyUpdateIndex(-1);
setWeeklyUpdateDraft(buildWeeklyUpdateFormValues(null));
setWeeklyUpdateError('');
setDeleteConfirmOpen(false);
}, [open, task]);
if (!open) {
......@@ -2086,6 +2091,7 @@ function ProjectTaskDialog({
? fieldOptions.occurrenceFrequencyOptions
: OCCURRENCE_FREQUENCY_OPTIONS;
const hasPersistedTask = Boolean(getTaskRecordId(task));
const isBusy = submitting || deleting;
return (
<>
......@@ -2102,7 +2108,7 @@ function ProjectTaskDialog({
<button
type="button"
onClick={handleStartProject}
disabled={submitting}
disabled={isBusy}
className="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-2 text-sm font-medium text-emerald-700 transition hover:bg-emerald-100"
>
{submitting ? '启动中...' : '启动项目'}
......@@ -2112,7 +2118,7 @@ function ProjectTaskDialog({
<button
type="button"
onClick={handleCompleteProject}
disabled={submitting}
disabled={isBusy}
className="rounded-xl border border-rose-200 bg-rose-50 px-4 py-2 text-sm font-medium text-rose-700 transition hover:bg-rose-100"
>
{submitting ? '完成中...' : '完成项目'}
......@@ -2121,7 +2127,7 @@ function ProjectTaskDialog({
<button
type="button"
onClick={onClose}
disabled={submitting}
disabled={isBusy}
className="rounded-full border border-zinc-200 px-3 py-2 text-sm text-zinc-500 transition hover:bg-zinc-100"
>
关闭
......@@ -2437,23 +2443,66 @@ function ProjectTaskDialog({
</div>
) : null}
<div className="mt-6 flex flex-wrap items-center justify-between gap-4 border-t border-zinc-100 pt-5">
<div className="text-sm text-zinc-500">
当前状态:{PROJECT_STATUS_LABEL_MAP[formValues.project_status] || '-'}
{' · '}
{PRIORITY_LABEL_MAP[formValues.priority] || '未设置优先级'}
<div className="mt-6 flex flex-wrap items-end justify-between gap-4 border-t border-zinc-100 pt-5">
<div className="flex-1">
<div className="text-sm text-zinc-500">
当前状态:{PROJECT_STATUS_LABEL_MAP[formValues.project_status] || '-'}
{' · '}
{PRIORITY_LABEL_MAP[formValues.priority] || '未设置优先级'}
</div>
{hasPersistedTask ? (
deleteConfirmOpen ? (
<div className="mt-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3">
<div className="text-sm font-medium text-rose-700">
确认删除当前任务?
</div>
<div className="mt-1 text-xs text-rose-600">
删除后项目会从甘特图、日历图和项目清单中移除。
</div>
<div className="mt-3 flex flex-wrap items-center gap-2">
<button
type="button"
onClick={() => setDeleteConfirmOpen(false)}
disabled={isBusy}
className="rounded-xl border border-rose-200 bg-white px-4 py-2 text-sm font-medium text-rose-600 transition hover:bg-rose-100 disabled:cursor-not-allowed disabled:opacity-70"
>
取消删除
</button>
<button
type="button"
onClick={() => onDelete?.(task)}
disabled={isBusy}
className="rounded-xl bg-rose-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-rose-700 disabled:cursor-not-allowed disabled:opacity-70"
>
{deleting ? '删除中...' : '确认删除'}
</button>
</div>
</div>
) : (
<button
type="button"
onClick={() => setDeleteConfirmOpen(true)}
disabled={isBusy}
className="mt-3 rounded-xl border border-rose-200 bg-white px-4 py-2 text-sm font-medium text-rose-600 transition hover:bg-rose-50 disabled:cursor-not-allowed disabled:opacity-70"
>
删除当前任务
</button>
)
) : null}
</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={onClose}
disabled={isBusy}
className="rounded-xl border border-zinc-200 bg-white px-5 py-2.5 text-sm font-medium text-zinc-600 transition hover:bg-zinc-100"
>
取消
</button>
<button
type="submit"
disabled={submitting}
disabled={isBusy}
className="rounded-xl bg-[var(--pm-brand)] px-5 py-2.5 text-sm font-medium text-white transition hover:bg-[var(--pm-brand-strong)] disabled:cursor-not-allowed disabled:opacity-70"
>
{submitting ? '保存中...' : '保存项目'}
......@@ -2494,6 +2543,7 @@ export default function ProjectBookingWorkspace({ currentUsername }) {
const [dialogOpen, setDialogOpen] = useState(false);
const [editingTask, setEditingTask] = useState(null);
const [dialogSubmitting, setDialogSubmitting] = useState(false);
const [dialogDeleting, setDialogDeleting] = useState(false);
const [dialogError, setDialogError] = useState('');
const [importDialogOpen, setImportDialogOpen] = useState(false);
const [importPreviewRows, setImportPreviewRows] = useState([]);
......@@ -2731,12 +2781,14 @@ export default function ProjectBookingWorkspace({ currentUsername }) {
const openCreateDialog = () => {
setEditingTask(null);
setDialogDeleting(false);
setDialogError('');
setDialogOpen(true);
};
const openEditDialog = (task) => {
setEditingTask(task || selectedTask || null);
setDialogDeleting(false);
setDialogError('');
setDialogOpen(true);
};
......@@ -2756,6 +2808,31 @@ export default function ProjectBookingWorkspace({ currentUsername }) {
}
};
const handleDialogDelete = async (taskToDelete) => {
if (!taskToDelete) {
setDialogError('未找到可删除的项目');
return;
}
const deletingTaskId = getTaskRecordId(taskToDelete);
setDialogDeleting(true);
setDialogError('');
try {
await deleteProjectTask(taskToDelete);
await loadWorkspace();
if (selectedTaskId && deletingTaskId === selectedTaskId) {
setSelectedTaskId('');
}
setDialogOpen(false);
setEditingTask(null);
} catch (requestError) {
setDialogError(requestError?.message || '删除失败,请稍后重试');
} finally {
setDialogDeleting(false);
}
};
const handleOpenTaskFromCalendar = (task) => {
setSelectedTaskId(getTaskRecordId(task));
setActiveTab('gantt');
......@@ -2934,17 +3011,20 @@ export default function ProjectBookingWorkspace({ currentUsername }) {
open={dialogOpen}
task={editingTask}
submitting={dialogSubmitting}
deleting={dialogDeleting}
submitError={dialogError}
fieldOptions={fieldOptions}
onClose={() => {
if (dialogSubmitting) {
if (dialogSubmitting || dialogDeleting) {
return;
}
setDialogOpen(false);
setEditingTask(null);
setDialogDeleting(false);
setDialogError('');
}}
onSubmit={handleDialogSubmit}
onDelete={handleDialogDelete}
/>
<ProjectImportDialog
......
......@@ -14,6 +14,8 @@
## 2. 功能入口
- 侧栏顶部:`应用切换` 按钮,打开应用切换弹框,展示当前账号可访问的全部应用;点击应用卡片后进入对应工作区
- 移动端顶部:`应用切换` 图标按钮,打开同一应用切换弹框
- 左侧菜单:`资源统计`
- 左侧菜单:`资源预定`
......@@ -80,6 +82,8 @@
6. 项目完成时,如当前完成进度小于 100,可默认补到 100
7. 在项目详情弹窗中通过“新增本周进度”录入一条周进度记录;如填写了周进度里的累计项目进度,保存时自动同步覆盖主记录的 `completion_percent`
8. 若已超过计划结束日期仍未完成,补充 `delay_reason`
9. 对于已落库项目,可在编辑弹框内点击“删除当前任务”
10. 删除操作必须二次确认;确认后前端调用删除接口,按软删除口径将记录移出甘特图、日历图与项目清单
### 4.4 状态结果判定
......
......@@ -5,6 +5,7 @@ import {
loadUser,
queryRecords,
queryUsers,
removeRecord,
updateRecord,
} from '@/services/DataService';
import * as XLSX from 'xlsx';
......@@ -1179,6 +1180,17 @@ export async function saveProjectTask(values, currentTask) {
});
}
export async function deleteProjectTask(currentTask) {
const recordName =
currentTask?.name || currentTask?._id || currentTask?.id || '';
if (!recordName) {
throw new Error('未找到可删除的项目记录');
}
return removeRecord(recordName);
}
export function buildDemoTasks() {
return [
{
......
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