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

feat: 应用切换

parent ca0cbf30
File added
...@@ -14,6 +14,8 @@ const ROUTE_PATHS = { ...@@ -14,6 +14,8 @@ const ROUTE_PATHS = {
export default defineConfig({ export default defineConfig({
layout: false, layout: false,
fastRefresh: true, fastRefresh: true,
base: '/project_manager_xy/',
publicPath: '/project_manager_xy/',
alias: { alias: {
react: resolvePackageRoot('react'), react: resolvePackageRoot('react'),
'react-dom': resolvePackageRoot('react-dom'), 'react-dom': resolvePackageRoot('react-dom'),
......
This diff is collapsed.
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
"start": "npm run dev" "start": "npm run dev"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.10",
"@heroicons/react": "^2.2.0", "@heroicons/react": "^2.2.0",
"@tailwindcss/postcss": "^4.2.0", "@tailwindcss/postcss": "^4.2.0",
"@umijs/max": "^4.5.3", "@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 xyLogo from '@/assets/xy-logo.png';
import xyLogoSmall from '@/assets/xy-logo_small.png'; import xyLogoSmall from '@/assets/xy-logo_small.png';
import { UserContext } from '@/wrapper/Auth'; import { UserContext } from '@/wrapper/Auth';
import { SiteContext } from '@/wrapper/Site';
import { import {
Bars3Icon,
CalendarDaysIcon, CalendarDaysIcon,
ChartBarSquareIcon, ChartBarSquareIcon,
Bars3Icon, Squares2X2Icon,
XMarkIcon, XMarkIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import clsx from 'clsx'; import clsx from 'clsx';
...@@ -174,10 +177,12 @@ function MobileSidebar({ open, close, children }) { ...@@ -174,10 +177,12 @@ function MobileSidebar({ open, close, children }) {
export default function ProjectManagerLayout() { export default function ProjectManagerLayout() {
const location = useLocation(); const location = useLocation();
const userInfo = useContext(UserContext);
const siteInfo = useContext(SiteContext);
const [isMobile, setIsMobile] = useState(false); const [isMobile, setIsMobile] = useState(false);
const [isApplicationDialogOpen, setIsApplicationDialogOpen] = useState(false);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false); const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
const userInfo = useContext(UserContext);
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
...@@ -219,6 +224,23 @@ export default function ProjectManagerLayout() { ...@@ -219,6 +224,23 @@ export default function ProjectManagerLayout() {
); );
}, [location.pathname]); }, [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 ( 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]"> <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 <aside
...@@ -229,25 +251,34 @@ export default function ProjectManagerLayout() { ...@@ -229,25 +251,34 @@ export default function ProjectManagerLayout() {
> >
<div <div
className={clsx( className={clsx(
'mb-7 flex items-center', 'mb-7',
sidebarCollapsed ? 'justify-center' : 'gap-3 px-1', sidebarCollapsed
? 'flex flex-col items-center gap-3'
: 'flex items-start justify-between gap-4 px-1',
)} )}
> >
<img {sidebarCollapsed ? (
src={sidebarCollapsed ? xyLogoSmall : xyLogo} <>
alt="XY Logo" <img
className={clsx( src={xyLogoSmall}
'shrink-0 object-contain', alt="XY Logo"
sidebarCollapsed className="h-8 w-auto max-w-8 shrink-0 object-contain"
? 'h-8 w-auto max-w-8' />
: 'h-8 w-auto max-w-[140px]', {applicationSwitcherButton}
)} </>
/> ) : (
{!sidebarCollapsed ? ( <>
<h1 className="truncate text-sm font-semibold tracking-[0.02em] text-white/95"> <div className="min-w-0 flex-1">
{APP_NAME} <h1 className="truncate text-sm font-semibold tracking-[0.02em] text-white/95">
</h1> {APP_NAME}
) : null} </h1>
<div className="mt-1 text-xs text-zinc-400">
项目资源管理工作台
</div>
</div>
{applicationSwitcherButton}
</>
)}
</div> </div>
<nav className="flex-1 space-y-5 overflow-y-auto"> <nav className="flex-1 space-y-5 overflow-y-auto">
...@@ -344,12 +375,26 @@ export default function ProjectManagerLayout() { ...@@ -344,12 +375,26 @@ export default function ProjectManagerLayout() {
<Bars3Icon className="size-5" /> <Bars3Icon className="size-5" />
</NavbarItem> </NavbarItem>
<div className="text-sm text-zinc-500">{currentGroupTitle}</div> <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>
<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"> <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 /> <Outlet />
</div> </div>
</main> </main>
<ApplicationSwitcherDialog
open={isApplicationDialogOpen}
onClose={() => setIsApplicationDialogOpen(false)}
userInfo={userInfo}
siteInfo={siteInfo}
/>
</div> </div>
); );
} }
...@@ -23,6 +23,7 @@ import { ...@@ -23,6 +23,7 @@ import {
saveProjectTask, saveProjectTask,
searchSystemUsers, searchSystemUsers,
sortWeeklyUpdates, sortWeeklyUpdates,
deleteProjectTask,
} from './projectTaskService'; } from './projectTaskService';
function resolveCurrentDate() { function resolveCurrentDate() {
...@@ -1920,9 +1921,11 @@ function ProjectTaskDialog({ ...@@ -1920,9 +1921,11 @@ function ProjectTaskDialog({
open, open,
task, task,
submitting, submitting,
deleting,
submitError, submitError,
onClose, onClose,
onSubmit, onSubmit,
onDelete,
fieldOptions, fieldOptions,
}) { }) {
const [formValues, setFormValues] = useState(buildTaskFormValues(null)); const [formValues, setFormValues] = useState(buildTaskFormValues(null));
...@@ -1932,6 +1935,7 @@ function ProjectTaskDialog({ ...@@ -1932,6 +1935,7 @@ function ProjectTaskDialog({
buildWeeklyUpdateFormValues(null), buildWeeklyUpdateFormValues(null),
); );
const [weeklyUpdateError, setWeeklyUpdateError] = useState(''); const [weeklyUpdateError, setWeeklyUpdateError] = useState('');
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
useEffect(() => { useEffect(() => {
if (!open) { if (!open) {
...@@ -1942,6 +1946,7 @@ function ProjectTaskDialog({ ...@@ -1942,6 +1946,7 @@ function ProjectTaskDialog({
setEditingWeeklyUpdateIndex(-1); setEditingWeeklyUpdateIndex(-1);
setWeeklyUpdateDraft(buildWeeklyUpdateFormValues(null)); setWeeklyUpdateDraft(buildWeeklyUpdateFormValues(null));
setWeeklyUpdateError(''); setWeeklyUpdateError('');
setDeleteConfirmOpen(false);
}, [open, task]); }, [open, task]);
if (!open) { if (!open) {
...@@ -2086,6 +2091,7 @@ function ProjectTaskDialog({ ...@@ -2086,6 +2091,7 @@ function ProjectTaskDialog({
? fieldOptions.occurrenceFrequencyOptions ? fieldOptions.occurrenceFrequencyOptions
: OCCURRENCE_FREQUENCY_OPTIONS; : OCCURRENCE_FREQUENCY_OPTIONS;
const hasPersistedTask = Boolean(getTaskRecordId(task)); const hasPersistedTask = Boolean(getTaskRecordId(task));
const isBusy = submitting || deleting;
return ( return (
<> <>
...@@ -2102,7 +2108,7 @@ function ProjectTaskDialog({ ...@@ -2102,7 +2108,7 @@ function ProjectTaskDialog({
<button <button
type="button" type="button"
onClick={handleStartProject} 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" 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 ? '启动中...' : '启动项目'} {submitting ? '启动中...' : '启动项目'}
...@@ -2112,7 +2118,7 @@ function ProjectTaskDialog({ ...@@ -2112,7 +2118,7 @@ function ProjectTaskDialog({
<button <button
type="button" type="button"
onClick={handleCompleteProject} 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" 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 ? '完成中...' : '完成项目'} {submitting ? '完成中...' : '完成项目'}
...@@ -2121,7 +2127,7 @@ function ProjectTaskDialog({ ...@@ -2121,7 +2127,7 @@ function ProjectTaskDialog({
<button <button
type="button" type="button"
onClick={onClose} 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" 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({ ...@@ -2437,23 +2443,66 @@ function ProjectTaskDialog({
</div> </div>
) : null} ) : null}
<div className="mt-6 flex flex-wrap items-center justify-between gap-4 border-t border-zinc-100 pt-5"> <div className="mt-6 flex flex-wrap items-end justify-between gap-4 border-t border-zinc-100 pt-5">
<div className="text-sm text-zinc-500"> <div className="flex-1">
当前状态:{PROJECT_STATUS_LABEL_MAP[formValues.project_status] || '-'} <div className="text-sm text-zinc-500">
{' · '} 当前状态:{PROJECT_STATUS_LABEL_MAP[formValues.project_status] || '-'}
{PRIORITY_LABEL_MAP[formValues.priority] || '未设置优先级'} {' · '}
{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>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button <button
type="button" type="button"
onClick={onClose} 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" 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>
<button <button
type="submit" 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" 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 ? '保存中...' : '保存项目'} {submitting ? '保存中...' : '保存项目'}
...@@ -2494,6 +2543,7 @@ export default function ProjectBookingWorkspace({ currentUsername }) { ...@@ -2494,6 +2543,7 @@ export default function ProjectBookingWorkspace({ currentUsername }) {
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [editingTask, setEditingTask] = useState(null); const [editingTask, setEditingTask] = useState(null);
const [dialogSubmitting, setDialogSubmitting] = useState(false); const [dialogSubmitting, setDialogSubmitting] = useState(false);
const [dialogDeleting, setDialogDeleting] = useState(false);
const [dialogError, setDialogError] = useState(''); const [dialogError, setDialogError] = useState('');
const [importDialogOpen, setImportDialogOpen] = useState(false); const [importDialogOpen, setImportDialogOpen] = useState(false);
const [importPreviewRows, setImportPreviewRows] = useState([]); const [importPreviewRows, setImportPreviewRows] = useState([]);
...@@ -2731,12 +2781,14 @@ export default function ProjectBookingWorkspace({ currentUsername }) { ...@@ -2731,12 +2781,14 @@ export default function ProjectBookingWorkspace({ currentUsername }) {
const openCreateDialog = () => { const openCreateDialog = () => {
setEditingTask(null); setEditingTask(null);
setDialogDeleting(false);
setDialogError(''); setDialogError('');
setDialogOpen(true); setDialogOpen(true);
}; };
const openEditDialog = (task) => { const openEditDialog = (task) => {
setEditingTask(task || selectedTask || null); setEditingTask(task || selectedTask || null);
setDialogDeleting(false);
setDialogError(''); setDialogError('');
setDialogOpen(true); setDialogOpen(true);
}; };
...@@ -2756,6 +2808,31 @@ export default function ProjectBookingWorkspace({ currentUsername }) { ...@@ -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) => { const handleOpenTaskFromCalendar = (task) => {
setSelectedTaskId(getTaskRecordId(task)); setSelectedTaskId(getTaskRecordId(task));
setActiveTab('gantt'); setActiveTab('gantt');
...@@ -2934,17 +3011,20 @@ export default function ProjectBookingWorkspace({ currentUsername }) { ...@@ -2934,17 +3011,20 @@ export default function ProjectBookingWorkspace({ currentUsername }) {
open={dialogOpen} open={dialogOpen}
task={editingTask} task={editingTask}
submitting={dialogSubmitting} submitting={dialogSubmitting}
deleting={dialogDeleting}
submitError={dialogError} submitError={dialogError}
fieldOptions={fieldOptions} fieldOptions={fieldOptions}
onClose={() => { onClose={() => {
if (dialogSubmitting) { if (dialogSubmitting || dialogDeleting) {
return; return;
} }
setDialogOpen(false); setDialogOpen(false);
setEditingTask(null); setEditingTask(null);
setDialogDeleting(false);
setDialogError(''); setDialogError('');
}} }}
onSubmit={handleDialogSubmit} onSubmit={handleDialogSubmit}
onDelete={handleDialogDelete}
/> />
<ProjectImportDialog <ProjectImportDialog
......
...@@ -14,6 +14,8 @@ ...@@ -14,6 +14,8 @@
## 2. 功能入口 ## 2. 功能入口
- 侧栏顶部:`应用切换` 按钮,打开应用切换弹框,展示当前账号可访问的全部应用;点击应用卡片后进入对应工作区
- 移动端顶部:`应用切换` 图标按钮,打开同一应用切换弹框
- 左侧菜单:`资源统计` - 左侧菜单:`资源统计`
- 左侧菜单:`资源预定` - 左侧菜单:`资源预定`
...@@ -80,6 +82,8 @@ ...@@ -80,6 +82,8 @@
6. 项目完成时,如当前完成进度小于 100,可默认补到 100 6. 项目完成时,如当前完成进度小于 100,可默认补到 100
7. 在项目详情弹窗中通过“新增本周进度”录入一条周进度记录;如填写了周进度里的累计项目进度,保存时自动同步覆盖主记录的 `completion_percent` 7. 在项目详情弹窗中通过“新增本周进度”录入一条周进度记录;如填写了周进度里的累计项目进度,保存时自动同步覆盖主记录的 `completion_percent`
8. 若已超过计划结束日期仍未完成,补充 `delay_reason` 8. 若已超过计划结束日期仍未完成,补充 `delay_reason`
9. 对于已落库项目,可在编辑弹框内点击“删除当前任务”
10. 删除操作必须二次确认;确认后前端调用删除接口,按软删除口径将记录移出甘特图、日历图与项目清单
### 4.4 状态结果判定 ### 4.4 状态结果判定
......
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
loadUser, loadUser,
queryRecords, queryRecords,
queryUsers, queryUsers,
removeRecord,
updateRecord, updateRecord,
} from '@/services/DataService'; } from '@/services/DataService';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
...@@ -1179,6 +1180,17 @@ export async function saveProjectTask(values, currentTask) { ...@@ -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() { export function buildDemoTasks() {
return [ 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