diff --git a/apps/senlinai-acro-react/scripts/structure-check.mjs b/apps/senlinai-acro-react/scripts/structure-check.mjs index 0a80fac..11b5fa6 100644 --- a/apps/senlinai-acro-react/scripts/structure-check.mjs +++ b/apps/senlinai-acro-react/scripts/structure-check.mjs @@ -5,13 +5,14 @@ const requiredFiles = [ 'src/pages/login.tsx', 'src/pages/workspace-body.tsx', 'src/pages/workspace-home.tsx', - 'src/pages/workspace-inbox.tsx', + 'src/pages/workspace-explore.tsx', 'src/pages/projects/project-overview.tsx', 'src/pages/projects/project-channel-page.tsx', 'src/pages/projects/project-tasks.tsx', 'src/pages/projects/project-ai.tsx', 'src/pages/projects/project-notes.tsx', 'src/pages/projects/project-cron.tsx', + 'src/pages/projects/project-new-channel.tsx', 'src/pages/projects/project-rail.tsx', 'src/pages/projects/project-sidebar.tsx', 'src/pages/projects/project-topbar.tsx', diff --git a/apps/senlinai-acro-react/scripts/visual-check.mjs b/apps/senlinai-acro-react/scripts/visual-check.mjs index c8156db..dd5e8bd 100644 --- a/apps/senlinai-acro-react/scripts/visual-check.mjs +++ b/apps/senlinai-acro-react/scripts/visual-check.mjs @@ -38,7 +38,7 @@ const collectMetrics = async () => page.evaluate(() => { const workspaceActions = [...document.querySelectorAll('.workspace-page .overview-head .arco-btn')] .map((button) => button.textContent?.trim()) const dashboard = document.querySelector('.dashboard-button') - const workspaceInbox = document.querySelector('.dashboard-button[title="Inbox"]') + const workspaceExplore = document.querySelector('.dashboard-button[title="探索"]') const project = document.querySelector('.project-button') const create = document.querySelector('.create-project') const firstBadge = document.querySelector('.project-badge .arco-badge-number') @@ -84,8 +84,8 @@ const collectMetrics = async () => page.evaluate(() => { workspaceActions, dashboardButton: rect(dashboard), dashboardButtonActive: dashboard?.classList.contains('active') ?? false, - workspaceInboxButton: rect(workspaceInbox), - workspaceInboxButtonActive: workspaceInbox?.classList.contains('active') ?? false, + workspaceExploreButton: rect(workspaceExplore), + workspaceExploreButtonActive: workspaceExplore?.classList.contains('active') ?? false, projectButton: rect(project), projectButtonActive: project?.classList.contains('active') ?? false, createProjectButton: rect(create), @@ -111,10 +111,10 @@ await page.waitForTimeout(700) await page.screenshot({ path: 'test-results/workbench-react-acro-light.png', fullPage: true }) const workspaceMetrics = await collectMetrics() -await page.locator('.dashboard-button[title="Inbox"]').click() +await page.locator('.dashboard-button[title="探索"]').click() await page.waitForTimeout(500) -await page.screenshot({ path: 'test-results/workspace-inbox-react-acro-light.png', fullPage: true }) -const workspaceInboxMetrics = await collectMetrics() +await page.screenshot({ path: 'test-results/workspace-explore-react-acro-light.png', fullPage: true }) +const workspaceExploreMetrics = await collectMetrics() await page.locator('.project-button').first().click() await page.waitForTimeout(500) @@ -133,7 +133,8 @@ for (const channel of [ { label: '工作计划', pageClass: 'project-tasks-page', expectedHeading: '工作计划' }, { label: 'AI 会话', pageClass: 'project-ai-page', expectedHeading: 'AI 会话' }, { label: '笔记资料', pageClass: 'project-notes-page', expectedHeading: '笔记资料' }, - { label: 'Cron 计划任务', pageClass: 'project-cron-page', expectedHeading: 'Cron 计划任务' }, + { label: '计划任务', pageClass: 'project-cron-page', expectedHeading: '计划任务' }, + { label: '新建频道', pageClass: 'project-new-channel-page', expectedHeading: '新建频道' }, ]) { await page.locator('.channel-button', { hasText: channel.label }).click() await page.waitForTimeout(250) @@ -158,7 +159,7 @@ await server.close() console.log(JSON.stringify({ workspaceMetrics, - workspaceInboxMetrics, + workspaceExploreMetrics, projectMetrics, channelSidebarHoverMetrics, stageHoverMetrics, @@ -180,8 +181,8 @@ if (metrics.overflowX) failures.push('expected no horizontal overflow') if (!metrics.workbenchMain) failures.push('missing workbench main') if (!metrics.hasWorkspacePage) failures.push('expected login to land on workspace page') if (!metrics.dashboardButtonActive) failures.push('expected dashboard button to be active after login') -if (!metrics.workspaceInboxButton) failures.push('expected fixed workspace inbox button to render in project rail') -if (metrics.workspaceInboxButtonActive) failures.push('expected fixed workspace inbox button not to be active after login') +if (!metrics.workspaceExploreButton) failures.push('expected fixed workspace explore button to render in project rail') +if (metrics.workspaceExploreButtonActive) failures.push('expected fixed workspace explore button not to be active after login') if (metrics.projectButtonActive) failures.push('expected first project button not to be active on workspace landing page') if (metrics.channelSidebar) failures.push(`expected workspace to hide channel sidebar, got ${JSON.stringify(metrics.channelSidebar)}`) if (metrics.inspector) failures.push(`expected workspace to hide inspector, got ${JSON.stringify(metrics.inspector)}`) @@ -231,11 +232,11 @@ if ( failures.push(`dashboard button size ${JSON.stringify(metrics.dashboardButton)} does not match project button ${JSON.stringify(metrics.projectButton)}`) } if ( - !metrics.workspaceInboxButton || - metrics.workspaceInboxButton.width !== metrics.projectButton?.width || - metrics.workspaceInboxButton.height !== metrics.projectButton?.height + !metrics.workspaceExploreButton || + metrics.workspaceExploreButton.width !== metrics.projectButton?.width || + metrics.workspaceExploreButton.height !== metrics.projectButton?.height ) { - failures.push(`workspace inbox button size ${JSON.stringify(metrics.workspaceInboxButton)} does not match project button ${JSON.stringify(metrics.projectButton)}`) + failures.push(`workspace explore button size ${JSON.stringify(metrics.workspaceExploreButton)} does not match project button ${JSON.stringify(metrics.projectButton)}`) } if ( !metrics.createProjectButton || @@ -245,14 +246,14 @@ if ( failures.push(`create project button size ${JSON.stringify(metrics.createProjectButton)} does not match project button ${JSON.stringify(metrics.projectButton)}`) } -if (!workspaceInboxMetrics.workspaceInboxButtonActive) failures.push('expected fixed workspace inbox button to become active when selected') -if (workspaceInboxMetrics.dashboardButtonActive) failures.push('expected dashboard button not to be active on workspace inbox page') -if (workspaceInboxMetrics.channelSidebar) failures.push(`expected workspace inbox to hide channel sidebar, got ${JSON.stringify(workspaceInboxMetrics.channelSidebar)}`) -if (workspaceInboxMetrics.inspector) failures.push(`expected workspace inbox to hide inspector, got ${JSON.stringify(workspaceInboxMetrics.inspector)}`) -if (!workspaceInboxMetrics.stage || !workspaceInboxMetrics.projectRail) { - failures.push(`missing workspace inbox layout regions: rail=${JSON.stringify(workspaceInboxMetrics.projectRail)}, stage=${JSON.stringify(workspaceInboxMetrics.stage)}`) -} else if (Math.abs(workspaceInboxMetrics.stage.left - workspaceInboxMetrics.projectRail.right) > 1) { - failures.push(`expected workspace inbox stage to start after project rail, got stage.left=${workspaceInboxMetrics.stage.left}, rail.right=${workspaceInboxMetrics.projectRail.right}`) +if (!workspaceExploreMetrics.workspaceExploreButtonActive) failures.push('expected fixed workspace explore button to become active when selected') +if (workspaceExploreMetrics.dashboardButtonActive) failures.push('expected dashboard button not to be active on workspace explore page') +if (workspaceExploreMetrics.channelSidebar) failures.push(`expected workspace explore to hide channel sidebar, got ${JSON.stringify(workspaceExploreMetrics.channelSidebar)}`) +if (workspaceExploreMetrics.inspector) failures.push(`expected workspace explore to hide inspector, got ${JSON.stringify(workspaceExploreMetrics.inspector)}`) +if (!workspaceExploreMetrics.stage || !workspaceExploreMetrics.projectRail) { + failures.push(`missing workspace explore layout regions: rail=${JSON.stringify(workspaceExploreMetrics.projectRail)}, stage=${JSON.stringify(workspaceExploreMetrics.stage)}`) +} else if (Math.abs(workspaceExploreMetrics.stage.left - workspaceExploreMetrics.projectRail.right) > 1) { + failures.push(`expected workspace explore stage to start after project rail, got stage.left=${workspaceExploreMetrics.stage.left}, rail.right=${workspaceExploreMetrics.projectRail.right}`) } if (!projectMetrics.hasProjectOverview) failures.push('expected first project click to show project overview page') diff --git a/apps/senlinai-acro-react/src/App.css b/apps/senlinai-acro-react/src/App.css index 7433a7e..6d814d2 100644 --- a/apps/senlinai-acro-react/src/App.css +++ b/apps/senlinai-acro-react/src/App.css @@ -347,6 +347,11 @@ color: white; } +.dashboard-button .arco-icon, +.create-project .arco-icon { + font-size: 24px; +} + .create-project { font-size: 11px; white-space: normal; @@ -399,6 +404,18 @@ margin: 0; } +.project-title-icon { + width: 24px; + height: 24px; + display: inline-grid; + place-items: center; + border-radius: 6px; + background: var(--senlin-soft-blue); + color: var(--senlin-primary); + font-size: 12px; + font-weight: 700; +} + .channel-list { display: grid; gap: 4px; @@ -429,6 +446,11 @@ box-shadow: inset 3px 0 0 var(--senlin-primary); } +.channel-badge-muted .arco-badge-number { + background: #c9cdd4; + color: #4e5969; +} + .channel-left { min-width: 0; display: flex; @@ -452,6 +474,17 @@ font-weight: 700; } +.tag-create-button.arco-btn { + height: 24px; + padding: 0 8px; + border-radius: 12px; + color: rgb(var(--arcoblue-6)); +} + +.tag-create-button .arco-icon { + font-size: 12px; +} + .recent-sessions button { display: grid; gap: 2px; @@ -495,6 +528,85 @@ margin: 3px 0 0; } +.project-overview-hero { + min-height: 112px; + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 14px; + border: 1px solid var(--senlin-border); + border-radius: 8px; + background-position: center; + background-size: cover; + color: #fff; + padding: 16px 18px; + overflow: hidden; +} + +.project-overview-identity { + min-width: 0; + display: flex; + align-items: center; + gap: 14px; +} + +.project-overview-icon { + width: 44px; + height: 44px; + flex: 0 0 auto; + display: inline-grid; + place-items: center; + border: 1px solid rgb(255 255 255 / 42%); + border-radius: 8px; + background: rgb(255 255 255 / 18%); + color: #fff; + font-size: 16px; + font-weight: 700; + backdrop-filter: blur(10px); +} + +.project-overview-identity h3, +.project-overview-identity .arco-typography { + color: #fff; +} + +.project-overview-identity h3 { + margin: 0 0 3px; +} + +.project-overview-identity .arco-typography { + max-width: 680px; + margin: 0; + opacity: 0.88; +} + +.project-overview-share { + max-width: min(520px, 42vw); + display: grid; + justify-items: end; + gap: 4px; +} + +.project-overview-share .arco-typography { + color: rgb(255 255 255 / 82%); +} + +.project-overview-share .arco-btn { + height: 30px; + max-width: 100%; + border-color: rgb(255 255 255 / 36%); + background: rgb(255 255 255 / 18%); + color: #fff; + padding: 0 10px; + backdrop-filter: blur(10px); +} + +.project-overview-share .arco-btn > span:last-child { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .metric-row { min-width: 0; } @@ -579,6 +691,61 @@ grid-template-columns: minmax(260px, 1fr) 82px 100px 100px 130px; } +.overview-task-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 12px; + padding-top: 6px; +} + +.overview-task-card { + min-width: 0; + min-height: 132px; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 10px; + border: 1px solid var(--senlin-border); + border-radius: 6px; + background: var(--senlin-surface); + color: var(--senlin-text); + padding: 14px; + text-align: left; + cursor: pointer; + transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease; +} + +.overview-task-card:hover { + border-color: color-mix(in srgb, var(--senlin-primary) 42%, var(--senlin-border)); + box-shadow: 0 8px 22px color-mix(in srgb, var(--senlin-primary) 12%, transparent); + transform: translateY(-1px); +} + +.overview-task-card.completed { + background: color-mix(in srgb, var(--senlin-surface) 92%, #00b42a); +} + +.overview-task-title { + min-width: 0; + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; +} + +.overview-task-title svg { + color: var(--senlin-primary); +} + +.overview-task-meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 10px; + color: var(--senlin-muted); + font-size: 12px; +} + .workspace-task-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); @@ -646,6 +813,109 @@ grid-template-columns: minmax(260px, 1fr) 80px 160px 80px; } +.project-file-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 8px 18px; +} + +.project-file-grid.compact { + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); +} + +.project-file-tile { + min-width: 0; + height: 76px; + display: grid; + grid-template-columns: 56px minmax(0, 1fr); + align-items: center; + gap: 12px; + border: 1px solid transparent; + border-radius: 3px; + background: transparent; + color: var(--senlin-text); + padding: 8px 10px; + text-align: left; + cursor: pointer; +} + +.project-file-tile:hover, +.project-file-tile:focus-visible { + border-color: #bfdbff; + background: #e8f3ff; + outline: none; +} + +.theme-dark .project-file-tile:hover, +.theme-dark .project-file-tile:focus-visible { + border-color: color-mix(in srgb, var(--senlin-primary) 56%, var(--senlin-border)); + background: color-mix(in srgb, var(--senlin-primary) 18%, var(--senlin-surface)); +} + +.project-file-icon { + width: 54px; + height: 54px; + display: inline-grid; + place-items: center; + border-radius: 6px; + background: #eef2f7; + color: #4e5969; + font-size: 30px; +} + +.project-file-icon.folder { + background: #fff3cf; + color: #f0a020; +} + +.project-file-icon.pdf { + background: #fff1f0; + color: #f53f3f; +} + +.project-file-icon.sheet { + background: #e8ffea; + color: #00b42a; +} + +.project-file-icon.doc, +.project-file-icon.note { + background: #e8f3ff; + color: #165dff; +} + +.project-file-icon.image { + background: #f2f3f5; + color: #86909c; +} + +.project-file-copy { + min-width: 0; + display: grid; + gap: 2px; +} + +.project-file-name, +.project-file-type, +.project-file-meta { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.project-file-name { + color: var(--senlin-text); + font-size: 14px; + font-weight: 500; +} + +.project-file-type, +.project-file-meta { + color: var(--senlin-muted); + font-size: 12px; +} + .project-channel-page { min-width: 0; } @@ -815,6 +1085,10 @@ padding: 12px; } +.channel-editor-card .arco-card-body { + padding: 14px; +} + .action-modal .arco-modal-content { border-radius: 8px; } diff --git a/apps/senlinai-acro-react/src/api/mappers.tsx b/apps/senlinai-acro-react/src/api/mappers.tsx index 1b8ee08..7af92eb 100644 --- a/apps/senlinai-acro-react/src/api/mappers.tsx +++ b/apps/senlinai-acro-react/src/api/mappers.tsx @@ -16,10 +16,14 @@ export function mapWorkspace(payload: BackendProjectWorkspace, index = 0): Proje const project: Project = { id: String(payload.project.id), name: payload.project.name, - short: payload.project.initials, + identifier: payload.project.identifier ?? '', + icon: payload.project.icon ?? '', + background: payload.project.background ?? '', + description: payload.project.description, + short: projectIconLabel(payload.project.icon ?? '', payload.project.initials), badge: payload.project.unreadCount, urgent: payload.project.unreadCount > 20, - color: projectColors[index % projectColors.length], + color: projectColor(payload.project.background ?? '', index), } return { @@ -35,6 +39,22 @@ export function mapWorkspace(payload: BackendProjectWorkspace, index = 0): Proje } } +function projectIconLabel(icon: string, fallback: string) { + const trimmed = icon.trim() + if (!trimmed) return fallback + const chars = Array.from(trimmed) + if (chars.length <= 2) return trimmed + return chars.slice(0, 2).join('').toUpperCase() +} + +function projectColor(background: string, index: number) { + const trimmed = background.trim() + if (trimmed.startsWith('#') || trimmed.startsWith('rgb') || trimmed.startsWith('hsl')) { + return trimmed + } + return projectColors[index % projectColors.length] +} + function mapChannel(channel: BackendChannel): ProjectChannel { return { id: channel.id, @@ -79,7 +99,7 @@ function channelLabel(channel: BackendChannel) { case 'notes_sources': return '笔记资料' case 'cron': - return 'Cron 计划任务' + return '计划任务' default: return channel.title } diff --git a/apps/senlinai-acro-react/src/api/projects.ts b/apps/senlinai-acro-react/src/api/projects.ts index d6e20ba..0ddab47 100644 --- a/apps/senlinai-acro-react/src/api/projects.ts +++ b/apps/senlinai-acro-react/src/api/projects.ts @@ -5,6 +5,12 @@ export type BackendProject = { id?: number Name?: string name?: string + Identifier?: string + identifier?: string + Icon?: string + icon?: string + Background?: string + background?: string Description?: string description?: string } @@ -12,6 +18,9 @@ export type BackendProject = { export type BackendWorkspaceProject = { id: number name: string + identifier?: string + icon?: string + background?: string description: string initials: string unreadCount: number @@ -88,6 +97,13 @@ export type BackendProjectWorkspace = { cronPlans: BackendCronPlan[] } +export type BackendTag = { + ID?: number + id?: number + Name?: string + name?: string +} + export async function fetchProjects(session: ApiSession) { return apiRequest('/api/projects', { token: session.token }) } @@ -96,8 +112,15 @@ export async function fetchProjectWorkspace(session: ApiSession, projectId: stri return apiRequest(`/api/projects/${projectId}/workspace`, { token: session.token }) } +export async function fetchProjectTags(session: ApiSession, projectId: string | number) { + return apiRequest(`/api/projects/${projectId}/tags`, { token: session.token }) +} + export type CreateProjectInput = { name: string + identifier?: string + icon?: string + background?: string description?: string } @@ -106,6 +129,15 @@ export type CreateTaskInput = { description?: string status?: string dueAt?: string + tag?: string +} + +export type UpdateTaskInput = { + title: string + description?: string + completed: boolean + nextProjectId?: number + tag?: string } export type UploadSourceInput = { @@ -120,6 +152,10 @@ export type CreateCronPlanInput = { nextRunAt?: string } +export type CreateProjectTagInput = { + name: string +} + export async function createProject(session: ApiSession, input: CreateProjectInput) { return apiRequest('/api/projects', { method: 'POST', @@ -136,6 +172,14 @@ export async function createTask(session: ApiSession, projectId: string | number }) } +export async function updateTask(session: ApiSession, projectId: string | number, taskId: string | number, input: UpdateTaskInput) { + return apiRequest(`/api/projects/${projectId}/tasks/${taskId}`, { + method: 'PATCH', + token: session.token, + body: input, + }) +} + export async function uploadSource(session: ApiSession, projectId: string | number, input: UploadSourceInput) { const body = new FormData() body.append('file', input.file) @@ -154,3 +198,11 @@ export async function createCronPlan(session: ApiSession, projectId: string | nu body: input, }) } + +export async function createProjectTag(session: ApiSession, projectId: string | number, input: CreateProjectTagInput) { + return apiRequest(`/api/projects/${projectId}/tags`, { + method: 'POST', + token: session.token, + body: input, + }) +} diff --git a/apps/senlinai-acro-react/src/app/App.tsx b/apps/senlinai-acro-react/src/app/App.tsx index 42ccd91..92fe876 100644 --- a/apps/senlinai-acro-react/src/app/App.tsx +++ b/apps/senlinai-acro-react/src/app/App.tsx @@ -7,14 +7,18 @@ import { mapWorkspace } from '../api/mappers' import { createCronPlan, createProject, + createProjectTag, createTask, fetchProjectWorkspace, fetchProjects, + updateTask, uploadSource, } from '../api/projects' import { LoginPage } from '../pages/login' import { ProjectActionModals, type CronDraft, type ProjectActionModal, type ProjectDraft, type SourceDraft, type TaskDraft } from '../pages/projects/project-action-modals' +import type { ProjectSettingsUpdate } from '../pages/projects/project-sidebar' import { ProjectPage } from '../pages/workspace-home' +import type { WorkspaceTaskUpdate } from '../pages/workspace-body' import type { ChannelKey, Project, ProjectWorkspace, Screen, Theme, WorkbenchView } from '../pages/projects/project-types' function App() { @@ -26,13 +30,14 @@ function App() { const [activeProjectID, setActiveProjectID] = useState('') const [activeChannel, setActiveChannel] = useState('overview') const [activeTaskID, setActiveTaskID] = useState(null) - const [, setSelectedItem] = useState('Inbox 待处理(36)') + const [, setSelectedItem] = useState('探索采集') const [loading, setLoading] = useState(false) const [actionLoading, setActionLoading] = useState(false) const [activeModal, setActiveModal] = useState(null) const dark = theme === 'dark' const activeWorkspace = workspaces.find((workspace) => workspace.project.id === activeProjectID) ?? workspaces[0] + const activeTagOptions = activeWorkspace?.tags.filter((tag) => tag !== 'all' && tag !== '全部') ?? [] async function loadWorkspaces(nextSession: ApiSession, preferredProjectID = activeProjectID) { const backendProjects = await fetchProjects(nextSession) @@ -102,7 +107,13 @@ function App() { return } void runAction(async () => { - const created = await createProject(requireSession(), { name: draft.name.trim(), description: draft.description.trim() }) + const created = await createProject(requireSession(), { + name: draft.name.trim(), + identifier: draft.identifier.trim(), + icon: draft.icon.trim(), + background: draft.background.trim(), + description: draft.description.trim(), + }) const nextProjectID = String(created.ID ?? created.id ?? '') await refreshAfterAction(nextProjectID) if (nextProjectID) { @@ -124,6 +135,7 @@ function App() { description: draft.description.trim(), status: 'open', dueAt: normalizeOptionalTime(draft.dueAt), + tag: draft.tag.trim(), }) await refreshAfterAction() setActiveChannel('tasks') @@ -167,6 +179,18 @@ function App() { }, '计划任务已创建') } + function handleCreateProjectTag(name: string) { + const trimmedName = name.trim() + if (!trimmedName) { + Message.warning('请输入标签名称') + return + } + void runAction(async () => { + await createProjectTag(requireSession(), requireActiveProject(), { name: trimmedName }) + await refreshAfterAction() + }, '标签已创建') + } + function openTask(project: Project, taskID: string) { setActiveProjectID(project.id) setActiveView('project') @@ -174,6 +198,38 @@ function App() { setActiveTaskID(taskID) } + function handleUpdateWorkspaceTask(update: WorkspaceTaskUpdate) { + void runAction(async () => { + await updateTask(requireSession(), update.originalProjectId, update.taskId, { + title: update.title, + description: update.summary, + completed: update.completed, + nextProjectId: Number(update.nextProjectId), + tag: update.tag, + }) + await refreshAfterAction(update.nextProjectId) + }, '任务已更新') + } + + function handleUpdateProject(update: ProjectSettingsUpdate) { + setWorkspaces((current) => + current.map((workspace) => { + if (workspace.project.id !== update.projectId) return workspace + const nextProject = { + ...workspace.project, + name: update.name, + identifier: update.identifier, + icon: update.icon, + background: update.background, + description: update.description, + short: projectIconLabel(update.icon, update.identifier || update.name.slice(0, 2)), + color: projectColor(update.background, workspace.project.color), + } + return { ...workspace, project: nextProject } + }), + ) + } + return (
@@ -190,7 +246,7 @@ function App() { activeTaskID={activeTaskID} theme={theme} onSelectWorkspace={() => setActiveView('workspace')} - onSelectWorkspaceInbox={() => setActiveView('workspace-inbox')} + onSelectWorkspaceExplore={() => setActiveView('workspace-explore')} onSelectProject={(project) => { setActiveProjectID(project.id) setActiveView('project') @@ -209,6 +265,9 @@ function App() { onCreateTask={() => setActiveModal('task')} onUploadSource={() => setActiveModal('source')} onCreateCronPlan={() => setActiveModal('cron')} + onUpdateWorkspaceTask={handleUpdateWorkspaceTask} + onUpdateProject={handleUpdateProject} + onCreateProjectTag={handleCreateProjectTag} /> ) : ( @@ -221,10 +280,25 @@ function App() { onCreateTask={handleCreateTask} onUploadSource={handleUploadSource} onCreateCronPlan={handleCreateCronPlan} + tagOptions={activeTagOptions} />
) } +function projectIconLabel(icon: string, fallback: string) { + const trimmed = icon.trim() + if (!trimmed) return fallback + const chars = Array.from(trimmed) + if (chars.length <= 2) return trimmed + return chars.slice(0, 2).join('').toUpperCase() +} + +function projectColor(background: string, fallback: string) { + const trimmed = background.trim() + if (trimmed.startsWith('#') || trimmed.startsWith('rgb') || trimmed.startsWith('hsl')) return trimmed + return fallback +} + export default App diff --git a/apps/senlinai-acro-react/src/pages/projects/project-action-modals.tsx b/apps/senlinai-acro-react/src/pages/projects/project-action-modals.tsx index de61e1e..d2ce6a7 100644 --- a/apps/senlinai-acro-react/src/pages/projects/project-action-modals.tsx +++ b/apps/senlinai-acro-react/src/pages/projects/project-action-modals.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { Input, Modal, Space, Switch, Typography } from '@arco-design/web-react' +import { Input, Modal, Select, Space, Switch, Typography } from '@arco-design/web-react' const { Text } = Typography const { TextArea } = Input @@ -8,6 +8,9 @@ export type ProjectActionModal = 'project' | 'task' | 'source' | 'cron' | null export type ProjectDraft = { name: string + identifier: string + icon: string + background: string description: string } @@ -15,6 +18,7 @@ export type TaskDraft = { title: string description: string dueAt: string + tag: string } export type SourceDraft = { @@ -37,6 +41,7 @@ export function ProjectActionModals({ onCreateTask, onUploadSource, onCreateCronPlan, + tagOptions, }: { activeModal: ProjectActionModal loading: boolean @@ -45,16 +50,17 @@ export function ProjectActionModals({ onCreateTask: (draft: TaskDraft) => void onUploadSource: (draft: SourceDraft) => void onCreateCronPlan: (draft: CronDraft) => void + tagOptions: string[] }) { - const [project, setProject] = useState({ name: '', description: '' }) - const [task, setTask] = useState({ title: '', description: '', dueAt: '' }) + const [project, setProject] = useState({ name: '', identifier: '', icon: '', background: '', description: '' }) + const [task, setTask] = useState({ title: '', description: '', dueAt: '', tag: '' }) const [source, setSource] = useState({ title: '', file: null }) const [cron, setCron] = useState({ title: '', schedule: '0 9 * * *', enabled: true, nextRunAt: '' }) useEffect(() => { if (activeModal === null) { - setProject({ name: '', description: '' }) - setTask({ title: '', description: '', dueAt: '' }) + setProject({ name: '', identifier: '', icon: '', background: '', description: '' }) + setTask({ title: '', description: '', dueAt: '', tag: '' }) setSource({ title: '', file: null }) setCron({ title: '', schedule: '0 9 * * *', enabled: true, nextRunAt: '' }) } @@ -76,7 +82,19 @@ export function ProjectActionModals({ setProject((draft) => ({ ...draft, name }))} /> + + +