feat(projects): 1、增加空白页占位;2、调试已开发功能;

This commit is contained in:
2026-05-14 09:05:08 +08:00
parent f634d21d2a
commit ddd05f8c02
58 changed files with 7392 additions and 1325 deletions

View File

@@ -0,0 +1,347 @@
import dayjs from 'dayjs';
export type WorkbenchTrend = 'up' | 'down' | 'flat';
export type WorkbenchTodoCategory = 'review' | 'task' | 'ticket' | 'mention';
export type WorkbenchTodoTimeBucket = 'all' | 'today' | 'week' | 'overdue';
export type WorkbenchProjectStatus = 'active' | 'preview' | 'paused';
export interface WorkbenchBannerSummarySource {
/** 今日待办数 */
todoCount: number;
/** 即将到期数(今日/明日内截止) */
upcomingCount: number;
/** 本周已完成 */
weekDone: number;
/** 本周总量 */
weekTotal: number;
/** 本周进行中 */
weekInProgress: number;
/** 本周逾期 */
weekOverdue: number;
}
export interface WorkbenchBannerSummary {
todoCount: number;
upcomingCount: number;
weekDone: number;
weekTotal: number;
weekInProgress: number;
weekOverdue: number;
/** 完成率0-100 整数) */
weekCompletionRate: number;
}
export interface WorkbenchKpiSource {
/** 待办 */
todo: {
count: number;
diffFromYesterday: number;
};
/** 我负责的任务 */
task: {
count: number;
diffFromYesterday: number;
};
/** 我参与的项目 */
project: {
count: number;
activeCount: number;
};
/** 我负责的需求 */
requirement: {
count: number;
pendingReview: number;
};
}
export interface WorkbenchKpiCard {
key: 'todo' | 'task' | 'project' | 'requirement';
label: string;
value: number;
trend: WorkbenchTrend;
trendText: string;
hint: string;
icon: string;
tone: 'sky' | 'emerald' | 'amber' | 'rose';
}
export interface WorkbenchTodoItemSource {
id: string;
category: WorkbenchTodoCategory;
title: string;
/** 截止时间ISO 字符串 */
deadline: string | null;
/** 来源(提交人/项目名/工单号) */
source: string;
/** 是否逾期 */
overdue?: boolean;
/** 是否高优先级 */
highPriority?: boolean;
/** 跳转路由 key可选未配则不跳转 */
routeKey?: string;
}
export interface WorkbenchTodoItem extends Omit<WorkbenchTodoItemSource, 'deadline'> {
deadlineLabel: string;
/** 相对截止天数:负数表示已逾期 */
remainingDays: number | null;
categoryLabel: string;
categoryTone: 'sky' | 'emerald' | 'amber' | 'rose' | 'violet';
}
export interface WorkbenchActivityItemSource {
id: string;
actor: string;
action: string;
/** 目标对象(需求/任务/工单/项目名等) */
target: string;
/** 目标对象的种类,用于颜色 */
targetKind: 'requirement' | 'task' | 'ticket' | 'project' | 'product';
/** 时间ISO */
time: string;
/** 是否 @ 了当前用户 */
mentioned?: boolean;
}
export interface WorkbenchActivityItem extends Omit<WorkbenchActivityItemSource, 'time'> {
timeLabel: string;
relativeLabel: string;
tone: 'sky' | 'emerald' | 'amber' | 'rose' | 'violet';
}
export interface WorkbenchProjectItemSource {
id: string;
name: string;
code: string;
status: WorkbenchProjectStatus;
/** 我的角色 */
myRole: string;
/** 进度百分比 0-100 */
progress: number;
/** 我负责的任务数 */
myTaskCount: number;
/** 我负责的待处理任务数 */
myPendingTaskCount: number;
/** 最近活动时间ISO */
lastActiveTime: string;
}
export interface WorkbenchProjectItem extends Omit<WorkbenchProjectItemSource, 'lastActiveTime'> {
statusLabel: string;
statusTone: 'sky' | 'emerald' | 'amber';
progress: number;
lastActiveLabel: string;
}
const todoCategoryMeta: Record<
WorkbenchTodoCategory,
{ label: string; tone: WorkbenchTodoItem['categoryTone']; icon: string }
> = {
review: { label: '需求评审', tone: 'sky', icon: 'mdi:file-search-outline' },
task: { label: '任务', tone: 'emerald', icon: 'mdi:checkbox-marked-circle-outline' },
ticket: { label: '工单', tone: 'amber', icon: 'mdi:ticket-confirmation-outline' },
mention: { label: '@ 提及', tone: 'violet', icon: 'mdi:at' }
};
const activityToneMap: Record<WorkbenchActivityItemSource['targetKind'], WorkbenchActivityItem['tone']> = {
requirement: 'sky',
task: 'emerald',
ticket: 'amber',
project: 'violet',
product: 'rose'
};
const projectStatusMeta: Record<WorkbenchProjectStatus, { label: string; tone: WorkbenchProjectItem['statusTone'] }> = {
active: { label: '进行中', tone: 'emerald' },
preview: { label: '试运行', tone: 'sky' },
paused: { label: '已暂停', tone: 'amber' }
};
function clampPercent(value: number) {
if (!Number.isFinite(value)) return 0;
return Math.min(100, Math.max(0, Math.round(value)));
}
function formatRelative(value: string) {
const target = dayjs(value);
if (!target.isValid()) return '--';
const now = dayjs();
const diffMinutes = now.diff(target, 'minute');
if (diffMinutes < 1) return '刚刚';
if (diffMinutes < 60) return `${diffMinutes} 分钟前`;
const diffHours = now.diff(target, 'hour');
if (diffHours < 24) return `${diffHours} 小时前`;
const diffDays = now.diff(target, 'day');
if (diffDays < 7) return `${diffDays} 天前`;
return target.format('MM-DD HH:mm');
}
function formatDeadline(value: string | null) {
if (!value) return '不限';
const target = dayjs(value);
if (!target.isValid()) return '不限';
const now = dayjs().startOf('day');
const start = target.startOf('day');
const days = start.diff(now, 'day');
if (days === 0) return `今日 ${target.format('HH:mm')}`;
if (days === 1) return `明日 ${target.format('HH:mm')}`;
if (days === -1) return `昨日 ${target.format('HH:mm')}`;
if (days > 0 && days <= 7) return `${days} 天后 ${target.format('MM-DD')}`;
if (days < 0) return `逾期 ${Math.abs(days)}`;
return target.format('YYYY-MM-DD');
}
function getRemainingDays(value: string | null) {
if (!value) return null;
const target = dayjs(value);
if (!target.isValid()) return null;
return target.startOf('day').diff(dayjs().startOf('day'), 'day');
}
export function buildWorkbenchBannerSummary(source: WorkbenchBannerSummarySource): WorkbenchBannerSummary {
const total = Math.max(0, source.weekTotal);
const done = Math.max(0, source.weekDone);
const rate = total === 0 ? 0 : clampPercent((done / total) * 100);
return {
todoCount: Math.max(0, source.todoCount),
upcomingCount: Math.max(0, source.upcomingCount),
weekDone: done,
weekTotal: total,
weekInProgress: Math.max(0, source.weekInProgress),
weekOverdue: Math.max(0, source.weekOverdue),
weekCompletionRate: rate
};
}
export function buildWorkbenchKpiCards(source: WorkbenchKpiSource): WorkbenchKpiCard[] {
function diffTrend(diff: number): { trend: WorkbenchTrend; text: string } {
if (diff > 0) return { trend: 'up', text: `较昨日 +${diff}` };
if (diff < 0) return { trend: 'down', text: `较昨日 ${diff}` };
return { trend: 'flat', text: '与昨日持平' };
}
const todoTrend = diffTrend(source.todo.diffFromYesterday);
const taskTrend = diffTrend(source.task.diffFromYesterday);
return [
{
key: 'todo',
label: '我的待办',
value: source.todo.count,
trend: todoTrend.trend,
trendText: todoTrend.text,
hint: '需要我处理的评审、任务、工单与 @ 提醒合计',
icon: 'mdi:clipboard-text-clock-outline',
tone: 'sky'
},
{
key: 'task',
label: '我负责的任务',
value: source.task.count,
trend: taskTrend.trend,
trendText: taskTrend.text,
hint: '当前由我负责的进行中任务',
icon: 'mdi:checkbox-marked-circle-outline',
tone: 'emerald'
},
{
key: 'project',
label: '我参与的项目',
value: source.project.count,
trend: 'flat',
trendText: `进行中 ${source.project.activeCount}`,
hint: '我作为成员参与的项目总数',
icon: 'mdi:briefcase-outline',
tone: 'amber'
},
{
key: 'requirement',
label: '我负责的需求',
value: source.requirement.count,
trend: 'flat',
trendText: `待评审 ${source.requirement.pendingReview}`,
hint: '由我担任产品经理或负责人的需求',
icon: 'mdi:file-document-multiple-outline',
tone: 'rose'
}
];
}
export function buildWorkbenchTodoItems(source: readonly WorkbenchTodoItemSource[]): WorkbenchTodoItem[] {
return [...source]
.sort((left, right) => {
const leftValue = left.deadline ? dayjs(left.deadline).valueOf() : Number.POSITIVE_INFINITY;
const rightValue = right.deadline ? dayjs(right.deadline).valueOf() : Number.POSITIVE_INFINITY;
return leftValue - rightValue;
})
.map(item => {
const meta = todoCategoryMeta[item.category];
return {
...item,
deadlineLabel: formatDeadline(item.deadline),
remainingDays: getRemainingDays(item.deadline),
categoryLabel: meta.label,
categoryTone: meta.tone
} satisfies WorkbenchTodoItem;
});
}
export function filterWorkbenchTodoItems(items: readonly WorkbenchTodoItem[], bucket: WorkbenchTodoTimeBucket) {
if (bucket === 'all') return [...items];
if (bucket === 'overdue') return items.filter(item => item.remainingDays !== null && item.remainingDays < 0);
if (bucket === 'today') return items.filter(item => item.remainingDays === 0);
// bucket === 'week'
return items.filter(item => item.remainingDays !== null && item.remainingDays >= 0 && item.remainingDays <= 7);
}
export function buildWorkbenchActivityItems(source: readonly WorkbenchActivityItemSource[]): WorkbenchActivityItem[] {
return [...source]
.filter(item => dayjs(item.time).isValid())
.sort((left, right) => dayjs(right.time).valueOf() - dayjs(left.time).valueOf())
.map(item => ({
...item,
timeLabel: dayjs(item.time).format('YYYY-MM-DD HH:mm'),
relativeLabel: formatRelative(item.time),
tone: activityToneMap[item.targetKind]
}));
}
export function buildWorkbenchProjectItems(source: readonly WorkbenchProjectItemSource[]): WorkbenchProjectItem[] {
return source.map(item => {
const meta = projectStatusMeta[item.status];
return {
...item,
statusLabel: meta.label,
statusTone: meta.tone,
progress: clampPercent(item.progress),
lastActiveLabel: formatRelative(item.lastActiveTime)
} satisfies WorkbenchProjectItem;
});
}
export function getGreeting(hour: number = dayjs().hour()) {
if (hour < 6) return '凌晨好';
if (hour < 11) return '早上好';
if (hour < 13) return '中午好';
if (hour < 18) return '下午好';
if (hour < 22) return '晚上好';
return '夜深了';
}
export function getTodayLabel() {
const today = dayjs();
const weekdayMap = ['日', '一', '二', '三', '四', '五', '六'];
return `今天 ${today.format('YYYY-MM-DD')} 星期${weekdayMap[today.day()]}`;
}

View File

@@ -0,0 +1,65 @@
<script setup lang="ts">
import { computed } from 'vue';
import {
buildWorkbenchActivityItems,
buildWorkbenchBannerSummary,
buildWorkbenchKpiCards,
buildWorkbenchProjectItems,
buildWorkbenchTodoItems
} from './homepage';
import {
workbenchActivityMock,
workbenchBannerSummaryMock,
workbenchKpiMock,
workbenchProjectMock,
workbenchTodoMock
} from './mock';
import WorkbenchBanner from './modules/workbench-banner.vue';
import WorkbenchKpi from './modules/workbench-kpi.vue';
import WorkbenchTodoPanel from './modules/workbench-todo-panel.vue';
import WorkbenchActivityPanel from './modules/workbench-activity-panel.vue';
import WorkbenchProjectGrid from './modules/workbench-project-grid.vue';
defineOptions({ name: 'Workbench' });
const bannerSummary = computed(() => buildWorkbenchBannerSummary(workbenchBannerSummaryMock));
const kpiCards = computed(() => buildWorkbenchKpiCards(workbenchKpiMock));
const todoItems = computed(() => buildWorkbenchTodoItems(workbenchTodoMock));
const activityItems = computed(() => buildWorkbenchActivityItems(workbenchActivityMock));
const projectItems = computed(() => buildWorkbenchProjectItems(workbenchProjectMock));
</script>
<template>
<div class="workbench">
<WorkbenchBanner :summary="bannerSummary" />
<WorkbenchKpi :cards="kpiCards" />
<section class="workbench__main">
<WorkbenchTodoPanel :items="todoItems" />
<WorkbenchActivityPanel :items="activityItems" />
</section>
<WorkbenchProjectGrid :items="projectItems" />
</div>
</template>
<style scoped>
.workbench {
display: flex;
flex-direction: column;
gap: 16px;
}
.workbench__main {
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
gap: 16px;
}
@media (width <= 1280px) {
.workbench__main {
grid-template-columns: 1fr;
}
}
</style>

194
src/views/workbench/mock.ts Normal file
View File

@@ -0,0 +1,194 @@
import dayjs from 'dayjs';
import type {
WorkbenchActivityItemSource,
WorkbenchBannerSummarySource,
WorkbenchKpiSource,
WorkbenchProjectItemSource,
WorkbenchTodoItemSource
} from './homepage';
const now = dayjs();
const iso = (date: dayjs.Dayjs) => date.format('YYYY-MM-DD HH:mm:ss');
export const workbenchBannerSummaryMock = {
todoCount: 8,
upcomingCount: 2,
weekDone: 12,
weekTotal: 18,
weekInProgress: 5,
weekOverdue: 1
} satisfies WorkbenchBannerSummarySource;
export const workbenchKpiMock = {
todo: { count: 8, diffFromYesterday: 1 },
task: { count: 14, diffFromYesterday: -1 },
project: { count: 3, activeCount: 2 },
requirement: { count: 6, pendingReview: 2 }
} satisfies WorkbenchKpiSource;
export const workbenchTodoMock = [
{
id: 'todo-1',
category: 'review',
title: '订单导出 V2 需求评审',
deadline: iso(now.add(2, 'day').hour(18).minute(0)),
source: '张三 · 收银台 V3',
highPriority: true,
routeKey: 'product_list'
},
{
id: 'todo-2',
category: 'task',
title: '支付回调接口联调',
deadline: iso(now.add(1, 'day').hour(17).minute(30)),
source: '收银台 V3 · 后端联调',
routeKey: 'project_list'
},
{
id: 'todo-3',
category: 'ticket',
title: '工单 #1024 等待我处理',
deadline: iso(now.hour(20).minute(0)),
source: '王五 · 客户反馈',
routeKey: 'ticket'
},
{
id: 'todo-4',
category: 'mention',
title: '需求「会员等级」中 @ 了你',
deadline: null,
source: '李四 · 会员中心',
routeKey: 'product_list'
},
{
id: 'todo-5',
category: 'task',
title: '订单中心结算文档评审',
deadline: iso(now.add(4, 'day').hour(15).minute(0)),
source: '订单中心 · 文档',
routeKey: 'project_list'
},
{
id: 'todo-6',
category: 'review',
title: 'API 返回结构调整评审',
deadline: iso(now.subtract(1, 'day').hour(18).minute(0)),
source: '赵六 · 收银台 V3',
overdue: true,
highPriority: true,
routeKey: 'product_list'
},
{
id: 'todo-7',
category: 'task',
title: '会员等级提示文案校对',
deadline: iso(now.add(3, 'day').hour(11).minute(0)),
source: '会员中心 · 文案',
routeKey: 'project_list'
},
{
id: 'todo-8',
category: 'ticket',
title: '工单 #1018 用户登录异常',
deadline: iso(now.add(1, 'day').hour(10).minute(0)),
source: '客户支持 · 紧急',
highPriority: true,
routeKey: 'ticket'
}
] satisfies WorkbenchTodoItemSource[];
export const workbenchActivityMock = [
{
id: 'act-1',
actor: '张三',
action: '提交了需求评审申请',
target: '订单导出 V2',
targetKind: 'requirement',
time: iso(now.subtract(10, 'minute'))
},
{
id: 'act-2',
actor: '李四',
action: '完成了任务',
target: '支付回调接口联调',
targetKind: 'task',
time: iso(now.subtract(2, 'hour'))
},
{
id: 'act-3',
actor: '李四',
action: '在需求中 @ 了你',
target: '会员等级',
targetKind: 'requirement',
time: iso(now.subtract(1, 'day').hour(17).minute(23)),
mentioned: true
},
{
id: 'act-4',
actor: '王五',
action: '提交了工单',
target: '#1024 · 客户反馈',
targetKind: 'ticket',
time: iso(now.subtract(1, 'day').hour(15).minute(8))
},
{
id: 'act-5',
actor: '赵六',
action: '把项目状态调整为',
target: '试运行(订单中心)',
targetKind: 'project',
time: iso(now.subtract(2, 'day').hour(10).minute(0))
},
{
id: 'act-6',
actor: '系统',
action: '提醒:你有 1 项任务即将逾期',
target: 'API 返回结构调整评审',
targetKind: 'requirement',
time: iso(now.subtract(3, 'day').hour(9).minute(30))
},
{
id: 'act-7',
actor: '钱七',
action: '更新了产品资料',
target: '收银台 V3 · 定位说明',
targetKind: 'product',
time: iso(now.subtract(4, 'day').hour(16).minute(45))
}
] satisfies WorkbenchActivityItemSource[];
export const workbenchProjectMock = [
{
id: 'prj-1',
name: '收银台 V3',
code: 'CASHIER-V3',
status: 'active',
myRole: '项目负责人',
progress: 72,
myTaskCount: 6,
myPendingTaskCount: 2,
lastActiveTime: iso(now.subtract(35, 'minute'))
},
{
id: 'prj-2',
name: '会员中心',
code: 'MEMBER',
status: 'active',
myRole: '后端负责人',
progress: 58,
myTaskCount: 4,
myPendingTaskCount: 1,
lastActiveTime: iso(now.subtract(3, 'hour'))
},
{
id: 'prj-3',
name: '订单中心',
code: 'ORDER-CENTER',
status: 'preview',
myRole: '产品经理',
progress: 95,
myTaskCount: 4,
myPendingTaskCount: 0,
lastActiveTime: iso(now.subtract(2, 'day').hour(10))
}
] satisfies WorkbenchProjectItemSource[];

View File

@@ -0,0 +1,169 @@
<script setup lang="ts">
import type { WorkbenchActivityItem } from '../homepage';
defineOptions({ name: 'WorkbenchActivityPanel' });
interface Props {
items: WorkbenchActivityItem[];
}
defineProps<Props>();
</script>
<template>
<ElCard class="workbench-activity card-wrapper" shadow="never">
<template #header>
<div>
<h3 class="workbench-activity__title">最近动态</h3>
<p class="workbench-activity__desc">关注与我相关的需求任务工单变化与 @ 提醒</p>
</div>
</template>
<div v-if="items.length" class="workbench-activity__list">
<article v-for="item in items" :key="item.id" class="workbench-activity__item">
<div class="workbench-activity__rail">
<span class="workbench-activity__dot" :class="`workbench-activity__dot--${item.tone}`" />
<span class="workbench-activity__line" />
</div>
<div class="workbench-activity__body">
<div class="workbench-activity__meta">
<span class="workbench-activity__time" :title="item.timeLabel">{{ item.relativeLabel }}</span>
<span v-if="item.mentioned" class="workbench-activity__mention">@ 提醒</span>
</div>
<p class="workbench-activity__sentence">
<strong class="workbench-activity__actor">{{ item.actor }}</strong>
<span>{{ item.action }}</span>
<strong class="workbench-activity__target">{{ item.target }}</strong>
</p>
</div>
</article>
</div>
<ElEmpty v-else description="暂无动态" :image-size="72" />
</ElCard>
</template>
<style scoped>
.workbench-activity {
overflow: hidden;
}
:deep(.el-card__header) {
padding: 16px 18px;
border-bottom: 1px solid rgb(226 232 240 / 80%);
}
.workbench-activity__title {
margin: 0;
color: rgb(15 23 42 / 98%);
font-size: 16px;
font-weight: 700;
}
.workbench-activity__desc {
margin: 4px 0 0;
color: rgb(100 116 139 / 92%);
font-size: 13px;
line-height: 1.6;
}
.workbench-activity__list {
display: flex;
flex-direction: column;
gap: 4px;
}
.workbench-activity__item {
display: grid;
grid-template-columns: 20px minmax(0, 1fr);
gap: 12px;
}
.workbench-activity__rail {
display: flex;
flex-direction: column;
align-items: center;
}
.workbench-activity__dot {
width: 12px;
height: 12px;
border-radius: 999px;
margin-top: 8px;
box-shadow: 0 0 0 4px rgb(255 255 255 / 96%);
}
.workbench-activity__dot--sky {
background-color: rgb(14 165 233 / 92%);
}
.workbench-activity__dot--emerald {
background-color: rgb(5 150 105 / 92%);
}
.workbench-activity__dot--amber {
background-color: rgb(217 119 6 / 92%);
}
.workbench-activity__dot--rose {
background-color: rgb(225 29 72 / 92%);
}
.workbench-activity__dot--violet {
background-color: rgb(124 58 237 / 92%);
}
.workbench-activity__line {
flex: 1;
width: 2px;
min-height: 28px;
margin-top: 4px;
background: linear-gradient(180deg, rgb(203 213 225 / 96%), rgb(226 232 240 / 24%));
}
.workbench-activity__item:last-child .workbench-activity__line {
opacity: 0;
}
.workbench-activity__body {
padding: 6px 14px 14px;
min-width: 0;
}
.workbench-activity__meta {
display: flex;
align-items: center;
gap: 8px;
}
.workbench-activity__time {
color: rgb(100 116 139 / 92%);
font-size: 12px;
}
.workbench-activity__mention {
padding: 1px 8px;
border-radius: 999px;
background-color: rgb(237 233 254 / 96%);
color: rgb(109 40 217 / 96%);
font-size: 11px;
font-weight: 600;
}
.workbench-activity__sentence {
margin: 6px 0 0;
color: rgb(71 85 105 / 94%);
font-size: 13px;
line-height: 1.7;
}
.workbench-activity__actor {
color: rgb(15 23 42 / 98%);
font-weight: 600;
}
.workbench-activity__target {
color: rgb(14 116 144 / 96%);
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,318 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useAuthStore } from '@/store/modules/auth';
import { useRouterPush } from '@/hooks/common/router';
import { getGreeting, getTodayLabel } from '../homepage';
import type { WorkbenchBannerSummary } from '../homepage';
defineOptions({ name: 'WorkbenchBanner' });
interface Props {
summary: WorkbenchBannerSummary;
}
const props = defineProps<Props>();
const { routerPushByKey } = useRouterPush();
const authStore = useAuthStore();
const displayName = computed(() => authStore.userInfo.nickname || authStore.userInfo.userName || '同学');
const greeting = computed(() => getGreeting());
const todayLabel = computed(() => getTodayLabel());
const rhythmItems = computed(() => [
{ label: '本周完成', value: `${props.summary.weekDone} / ${props.summary.weekTotal}`, tone: 'emerald' as const },
{ label: '进行中', value: String(props.summary.weekInProgress), tone: 'sky' as const },
{ label: '逾期', value: String(props.summary.weekOverdue), tone: 'rose' as const }
]);
function handleCreateRequirement() {
routerPushByKey('product_list');
}
function handleCreateTask() {
routerPushByKey('project_list');
}
</script>
<template>
<section class="workbench-banner">
<div class="workbench-banner__identity">
<div class="workbench-banner__title-group">
<h1 class="workbench-banner__title">{{ greeting }}{{ displayName }}</h1>
<span class="workbench-banner__decor-word">RDMS</span>
</div>
<p class="workbench-banner__subtitle">{{ todayLabel }}</p>
<div class="workbench-banner__digest">
<div class="workbench-banner__digest-item">
<span class="workbench-banner__digest-label">今日待办</span>
<strong class="workbench-banner__digest-value">{{ summary.todoCount }}</strong>
<span class="workbench-banner__digest-unit"></span>
</div>
<span class="workbench-banner__digest-sep">·</span>
<div class="workbench-banner__digest-item">
<span class="workbench-banner__digest-label">即将到期</span>
<strong class="workbench-banner__digest-value workbench-banner__digest-value--warn">
{{ summary.upcomingCount }}
</strong>
<span class="workbench-banner__digest-unit"></span>
</div>
</div>
<div class="workbench-banner__actions">
<ElButton type="primary" @click="handleCreateRequirement">
<SvgIcon icon="mdi:plus" class="workbench-banner__btn-icon" />
<span>新建需求</span>
</ElButton>
<ElButton @click="handleCreateTask">
<SvgIcon icon="mdi:plus" class="workbench-banner__btn-icon" />
<span>新建任务</span>
</ElButton>
</div>
</div>
<div class="workbench-banner__rhythm">
<div class="workbench-banner__rhythm-header">
<h2 class="workbench-banner__rhythm-title">本周节奏</h2>
<span class="workbench-banner__rhythm-rate">完成率 {{ summary.weekCompletionRate }}%</span>
</div>
<div class="workbench-banner__rhythm-bar">
<div class="workbench-banner__rhythm-bar-inner" :style="{ width: `${summary.weekCompletionRate}%` }" />
</div>
<ul class="workbench-banner__rhythm-list">
<li
v-for="item in rhythmItems"
:key="item.label"
class="workbench-banner__rhythm-item"
:class="`workbench-banner__rhythm-item--${item.tone}`"
>
<span class="workbench-banner__rhythm-item-label">{{ item.label }}</span>
<strong class="workbench-banner__rhythm-item-value">{{ item.value }}</strong>
</li>
</ul>
</div>
</section>
</template>
<style scoped>
.workbench-banner {
display: grid;
grid-template-columns: minmax(0, 1.55fr) minmax(280px, 1fr);
gap: 16px;
padding: 24px;
border: 1px solid rgb(226 232 240 / 92%);
border-radius: 24px;
background:
radial-gradient(circle at top left, rgb(14 116 144 / 14%), transparent 34%),
radial-gradient(circle at bottom right, rgb(15 118 110 / 10%), transparent 28%),
linear-gradient(135deg, rgb(255 255 255 / 99%), rgb(248 250 252 / 98%));
}
.workbench-banner__identity {
display: flex;
flex-direction: column;
gap: 14px;
min-width: 0;
}
.workbench-banner__title-group {
display: flex;
align-items: baseline;
gap: 14px;
flex-wrap: wrap;
}
.workbench-banner__title {
margin: 0;
color: rgb(15 23 42 / 98%);
font-size: 32px;
line-height: 1.15;
letter-spacing: -0.02em;
}
.workbench-banner__decor-word {
color: transparent;
background: linear-gradient(180deg, rgb(14 116 144 / 92%), rgb(13 148 136 / 60%));
background-clip: text;
-webkit-text-fill-color: transparent;
font-size: 22px;
font-weight: 800;
letter-spacing: 0.32em;
text-shadow: 0 10px 24px rgb(14 116 144 / 14%);
user-select: none;
}
.workbench-banner__subtitle {
margin: 0;
color: rgb(100 116 139 / 92%);
font-size: 14px;
}
.workbench-banner__digest {
display: flex;
align-items: baseline;
gap: 12px;
padding: 14px 16px;
border: 1px solid rgb(226 232 240 / 88%);
border-radius: 18px;
background-color: rgb(255 255 255 / 78%);
}
.workbench-banner__digest-item {
display: flex;
align-items: baseline;
gap: 8px;
}
.workbench-banner__digest-label {
color: rgb(100 116 139 / 92%);
font-size: 13px;
}
.workbench-banner__digest-value {
color: rgb(15 23 42 / 98%);
font-size: 24px;
line-height: 1;
}
.workbench-banner__digest-value--warn {
color: rgb(217 119 6 / 94%);
}
.workbench-banner__digest-unit {
color: rgb(100 116 139 / 90%);
font-size: 12px;
}
.workbench-banner__digest-sep {
color: rgb(203 213 225 / 96%);
font-size: 18px;
user-select: none;
}
.workbench-banner__actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.workbench-banner__btn-icon {
margin-right: 4px;
font-size: 16px;
}
.workbench-banner__rhythm {
display: flex;
flex-direction: column;
gap: 14px;
padding: 18px 20px;
border: 1px solid rgb(226 232 240 / 88%);
border-radius: 20px;
background: linear-gradient(180deg, rgb(255 255 255 / 99%), rgb(241 245 249 / 98%));
}
.workbench-banner__rhythm-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
}
.workbench-banner__rhythm-title {
margin: 0;
color: rgb(15 23 42 / 98%);
font-size: 15px;
font-weight: 700;
}
.workbench-banner__rhythm-rate {
color: rgb(5 150 105 / 94%);
font-size: 13px;
font-weight: 700;
}
.workbench-banner__rhythm-bar {
position: relative;
height: 8px;
border-radius: 999px;
background-color: rgb(226 232 240 / 80%);
overflow: hidden;
}
.workbench-banner__rhythm-bar-inner {
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, rgb(14 116 144 / 92%), rgb(16 185 129 / 88%));
transition: width 240ms ease;
}
.workbench-banner__rhythm-list {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
margin: 0;
padding: 0;
list-style: none;
}
.workbench-banner__rhythm-item {
display: flex;
flex-direction: column;
gap: 6px;
padding: 12px 12px;
border-radius: 14px;
background-color: rgb(248 250 252 / 96%);
}
.workbench-banner__rhythm-item--emerald {
background-color: rgb(236 253 245 / 88%);
}
.workbench-banner__rhythm-item--sky {
background-color: rgb(240 249 255 / 88%);
}
.workbench-banner__rhythm-item--rose {
background-color: rgb(255 241 242 / 88%);
}
.workbench-banner__rhythm-item-label {
color: rgb(100 116 139 / 92%);
font-size: 12px;
}
.workbench-banner__rhythm-item-value {
color: rgb(15 23 42 / 98%);
font-size: 20px;
line-height: 1.1;
}
.workbench-banner__rhythm-item--emerald .workbench-banner__rhythm-item-value {
color: rgb(5 150 105 / 96%);
}
.workbench-banner__rhythm-item--sky .workbench-banner__rhythm-item-value {
color: rgb(14 116 144 / 96%);
}
.workbench-banner__rhythm-item--rose .workbench-banner__rhythm-item-value {
color: rgb(225 29 72 / 94%);
}
@media (width <= 1280px) {
.workbench-banner {
grid-template-columns: 1fr;
}
}
@media (width <= 768px) {
.workbench-banner {
padding: 18px;
}
.workbench-banner__title {
font-size: 26px;
}
}
</style>

View File

@@ -0,0 +1,192 @@
<script setup lang="ts">
import type { WorkbenchKpiCard } from '../homepage';
defineOptions({ name: 'WorkbenchKpi' });
interface Props {
cards: WorkbenchKpiCard[];
}
defineProps<Props>();
function getTrendIcon(trend: WorkbenchKpiCard['trend']) {
if (trend === 'up') return 'mdi:arrow-top-right-thin';
if (trend === 'down') return 'mdi:arrow-bottom-right-thin';
return 'mdi:minus';
}
</script>
<template>
<section class="workbench-kpi">
<article
v-for="card in cards"
:key="card.key"
class="workbench-kpi__card"
:class="`workbench-kpi__card--${card.tone}`"
>
<div class="workbench-kpi__card-header">
<span class="workbench-kpi__card-label">{{ card.label }}</span>
<span class="workbench-kpi__card-icon">
<SvgIcon :icon="card.icon" />
</span>
</div>
<strong class="workbench-kpi__card-value">{{ card.value }}</strong>
<div class="workbench-kpi__card-trend" :class="`workbench-kpi__card-trend--${card.trend}`">
<SvgIcon :icon="getTrendIcon(card.trend)" class="workbench-kpi__card-trend-icon" />
<span>{{ card.trendText }}</span>
</div>
<p class="workbench-kpi__card-hint">{{ card.hint }}</p>
</article>
</section>
</template>
<style scoped>
.workbench-kpi {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px;
}
.workbench-kpi__card {
position: relative;
display: flex;
flex-direction: column;
gap: 12px;
padding: 20px;
min-height: 148px;
border: 1px solid rgb(226 232 240 / 92%);
border-radius: 20px;
background: linear-gradient(180deg, rgb(255 255 255 / 99%), rgb(248 250 252 / 96%));
overflow: hidden;
}
.workbench-kpi__card::after {
content: '';
position: absolute;
inset: -40% -30% auto auto;
width: 160px;
height: 160px;
border-radius: 50%;
opacity: 0.55;
pointer-events: none;
}
.workbench-kpi__card--sky::after {
background: radial-gradient(circle, rgb(14 165 233 / 22%), transparent 70%);
}
.workbench-kpi__card--emerald::after {
background: radial-gradient(circle, rgb(16 185 129 / 22%), transparent 70%);
}
.workbench-kpi__card--amber::after {
background: radial-gradient(circle, rgb(245 158 11 / 22%), transparent 70%);
}
.workbench-kpi__card--rose::after {
background: radial-gradient(circle, rgb(244 63 94 / 22%), transparent 70%);
}
.workbench-kpi__card-header {
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
z-index: 1;
}
.workbench-kpi__card-label {
color: rgb(100 116 139 / 94%);
font-size: 13px;
font-weight: 600;
}
.workbench-kpi__card-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 10px;
background-color: rgb(255 255 255 / 88%);
font-size: 18px;
}
.workbench-kpi__card--sky .workbench-kpi__card-icon {
color: rgb(14 116 144 / 94%);
}
.workbench-kpi__card--emerald .workbench-kpi__card-icon {
color: rgb(5 150 105 / 94%);
}
.workbench-kpi__card--amber .workbench-kpi__card-icon {
color: rgb(217 119 6 / 94%);
}
.workbench-kpi__card--rose .workbench-kpi__card-icon {
color: rgb(225 29 72 / 94%);
}
.workbench-kpi__card-value {
position: relative;
z-index: 1;
color: rgb(15 23 42 / 98%);
font-size: 32px;
line-height: 1.05;
letter-spacing: -0.02em;
}
.workbench-kpi__card-trend {
display: inline-flex;
align-items: center;
gap: 4px;
position: relative;
z-index: 1;
width: fit-content;
padding: 3px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
}
.workbench-kpi__card-trend--up {
color: rgb(5 150 105 / 96%);
background-color: rgb(236 253 245 / 96%);
}
.workbench-kpi__card-trend--down {
color: rgb(225 29 72 / 96%);
background-color: rgb(255 241 242 / 96%);
}
.workbench-kpi__card-trend--flat {
color: rgb(100 116 139 / 94%);
background-color: rgb(241 245 249 / 96%);
}
.workbench-kpi__card-trend-icon {
font-size: 14px;
}
.workbench-kpi__card-hint {
margin: 0;
position: relative;
z-index: 1;
color: rgb(100 116 139 / 88%);
font-size: 12px;
line-height: 1.6;
}
@media (width <= 1280px) {
.workbench-kpi {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (width <= 600px) {
.workbench-kpi {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,311 @@
<script setup lang="ts">
import { useRouterPush } from '@/hooks/common/router';
import type { WorkbenchProjectItem } from '../homepage';
defineOptions({ name: 'WorkbenchProjectGrid' });
interface Props {
items: WorkbenchProjectItem[];
}
defineProps<Props>();
const { routerPushByKey } = useRouterPush();
function handleEnterProjectList() {
routerPushByKey('project_list');
}
</script>
<template>
<ElCard class="workbench-project card-wrapper" shadow="never">
<template #header>
<div class="workbench-project__header">
<div>
<h3 class="workbench-project__title">我参与的项目</h3>
<p class="workbench-project__desc">直接看每个项目的当前进度我的角色与未完成任务</p>
</div>
<ElButton type="primary" link @click="handleEnterProjectList">
<span>进入项目列表</span>
<SvgIcon icon="mdi:arrow-right-thin" class="workbench-project__more-icon" />
</ElButton>
</div>
</template>
<div v-if="items.length" class="workbench-project__grid">
<article v-for="item in items" :key="item.id" class="workbench-project__card">
<div class="workbench-project__card-header">
<div class="workbench-project__card-title-group">
<h4 class="workbench-project__card-title">{{ item.name }}</h4>
<span class="workbench-project__card-code">{{ item.code }}</span>
</div>
<span class="workbench-project__card-status" :class="`workbench-project__card-status--${item.statusTone}`">
{{ item.statusLabel }}
</span>
</div>
<div class="workbench-project__card-role">
<span class="workbench-project__card-role-label">我的角色</span>
<strong class="workbench-project__card-role-value">{{ item.myRole }}</strong>
</div>
<div class="workbench-project__progress">
<div class="workbench-project__progress-header">
<span class="workbench-project__progress-label">进度</span>
<strong class="workbench-project__progress-value">{{ item.progress }}%</strong>
</div>
<div class="workbench-project__progress-bar">
<div
class="workbench-project__progress-bar-inner"
:class="`workbench-project__progress-bar-inner--${item.statusTone}`"
:style="{ width: `${item.progress}%` }"
/>
</div>
</div>
<div class="workbench-project__footer">
<div class="workbench-project__footer-block">
<span class="workbench-project__footer-label">我负责的任务</span>
<strong class="workbench-project__footer-value">
{{ item.myTaskCount }}
<span v-if="item.myPendingTaskCount > 0" class="workbench-project__footer-sub">
待处理 {{ item.myPendingTaskCount }}
</span>
</strong>
</div>
<div class="workbench-project__footer-block workbench-project__footer-block--right">
<span class="workbench-project__footer-label">最近活动</span>
<strong class="workbench-project__footer-value">{{ item.lastActiveLabel }}</strong>
</div>
</div>
</article>
</div>
<ElEmpty v-else description="暂未参与任何项目" :image-size="72" />
</ElCard>
</template>
<style scoped>
.workbench-project {
overflow: hidden;
}
:deep(.el-card__header) {
padding: 16px 18px;
border-bottom: 1px solid rgb(226 232 240 / 80%);
}
.workbench-project__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.workbench-project__title {
margin: 0;
color: rgb(15 23 42 / 98%);
font-size: 16px;
font-weight: 700;
}
.workbench-project__desc {
margin: 4px 0 0;
color: rgb(100 116 139 / 92%);
font-size: 13px;
line-height: 1.6;
}
.workbench-project__more-icon {
margin-left: 4px;
font-size: 16px;
}
.workbench-project__grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
.workbench-project__card {
display: flex;
flex-direction: column;
gap: 14px;
padding: 18px;
border: 1px solid rgb(226 232 240 / 92%);
border-radius: 18px;
background: linear-gradient(180deg, rgb(255 255 255 / 99%), rgb(248 250 252 / 96%));
transition:
border-color 160ms ease,
transform 160ms ease;
}
.workbench-project__card:hover {
border-color: rgb(14 116 144 / 60%);
}
.workbench-project__card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.workbench-project__card-title-group {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.workbench-project__card-title {
margin: 0;
color: rgb(15 23 42 / 98%);
font-size: 16px;
font-weight: 700;
line-height: 1.4;
}
.workbench-project__card-code {
color: rgb(100 116 139 / 92%);
font-size: 12px;
letter-spacing: 0.04em;
}
.workbench-project__card-status {
flex-shrink: 0;
padding: 3px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
}
.workbench-project__card-status--emerald {
background-color: rgb(220 252 231 / 96%);
color: rgb(5 150 105 / 96%);
}
.workbench-project__card-status--sky {
background-color: rgb(224 242 254 / 96%);
color: rgb(14 116 144 / 96%);
}
.workbench-project__card-status--amber {
background-color: rgb(254 243 199 / 96%);
color: rgb(180 83 9 / 96%);
}
.workbench-project__card-role {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border-radius: 12px;
background-color: rgb(241 245 249 / 80%);
}
.workbench-project__card-role-label {
color: rgb(100 116 139 / 92%);
font-size: 12px;
}
.workbench-project__card-role-value {
color: rgb(15 23 42 / 98%);
font-size: 13px;
font-weight: 600;
}
.workbench-project__progress {
display: flex;
flex-direction: column;
gap: 8px;
}
.workbench-project__progress-header {
display: flex;
align-items: baseline;
justify-content: space-between;
}
.workbench-project__progress-label {
color: rgb(100 116 139 / 92%);
font-size: 12px;
}
.workbench-project__progress-value {
color: rgb(15 23 42 / 98%);
font-size: 18px;
line-height: 1;
letter-spacing: -0.02em;
}
.workbench-project__progress-bar {
height: 6px;
border-radius: 999px;
background-color: rgb(226 232 240 / 88%);
overflow: hidden;
}
.workbench-project__progress-bar-inner {
height: 100%;
border-radius: 999px;
transition: width 240ms ease;
}
.workbench-project__progress-bar-inner--emerald {
background: linear-gradient(90deg, rgb(5 150 105 / 92%), rgb(16 185 129 / 86%));
}
.workbench-project__progress-bar-inner--sky {
background: linear-gradient(90deg, rgb(14 116 144 / 92%), rgb(14 165 233 / 86%));
}
.workbench-project__progress-bar-inner--amber {
background: linear-gradient(90deg, rgb(217 119 6 / 92%), rgb(245 158 11 / 86%));
}
.workbench-project__footer {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
}
.workbench-project__footer-block {
display: flex;
flex-direction: column;
gap: 4px;
}
.workbench-project__footer-block--right {
align-items: flex-end;
}
.workbench-project__footer-label {
color: rgb(100 116 139 / 92%);
font-size: 12px;
}
.workbench-project__footer-value {
color: rgb(15 23 42 / 98%);
font-size: 14px;
font-weight: 600;
}
.workbench-project__footer-sub {
color: rgb(190 18 60 / 94%);
font-size: 12px;
font-weight: 600;
}
@media (width <= 1280px) {
.workbench-project__grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (width <= 600px) {
.workbench-project__grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,347 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import type { RouteKey } from '@elegant-router/types';
import { useRouterPush } from '@/hooks/common/router';
import { filterWorkbenchTodoItems } from '../homepage';
import type { WorkbenchTodoItem, WorkbenchTodoTimeBucket } from '../homepage';
defineOptions({ name: 'WorkbenchTodoPanel' });
interface Props {
items: WorkbenchTodoItem[];
}
const props = defineProps<Props>();
const { routerPushByKey } = useRouterPush();
const activeBucket = ref<WorkbenchTodoTimeBucket>('all');
const buckets: Array<{ key: WorkbenchTodoTimeBucket; label: string }> = [
{ key: 'all', label: '全部' },
{ key: 'today', label: '今日' },
{ key: 'week', label: '本周' },
{ key: 'overdue', label: '逾期' }
];
const bucketCounts = computed(() => ({
all: props.items.length,
today: filterWorkbenchTodoItems(props.items, 'today').length,
week: filterWorkbenchTodoItems(props.items, 'week').length,
overdue: filterWorkbenchTodoItems(props.items, 'overdue').length
}));
const filteredItems = computed(() => filterWorkbenchTodoItems(props.items, activeBucket.value));
function handleClickItem(item: WorkbenchTodoItem) {
if (!item.routeKey) return;
routerPushByKey(item.routeKey as RouteKey);
}
function getDeadlineToneClass(item: WorkbenchTodoItem) {
if (item.overdue || (item.remainingDays !== null && item.remainingDays < 0)) {
return 'workbench-todo__deadline--rose';
}
if (item.remainingDays === 0) return 'workbench-todo__deadline--amber';
return 'workbench-todo__deadline--slate';
}
</script>
<template>
<ElCard class="workbench-todo card-wrapper" shadow="never">
<template #header>
<div class="workbench-todo__header">
<div class="workbench-todo__title-group">
<h3 class="workbench-todo__title">我的待办</h3>
<p class="workbench-todo__desc">需要我处理的需求评审任务工单与 @ 提醒</p>
</div>
<div class="workbench-todo__tabs">
<button
v-for="bucket in buckets"
:key="bucket.key"
type="button"
class="workbench-todo__tab"
:class="{ 'workbench-todo__tab--active': activeBucket === bucket.key }"
@click="activeBucket = bucket.key"
>
<span>{{ bucket.label }}</span>
<span class="workbench-todo__tab-count">{{ bucketCounts[bucket.key] }}</span>
</button>
</div>
</div>
</template>
<div v-if="filteredItems.length" class="workbench-todo__list">
<article
v-for="item in filteredItems"
:key="item.id"
class="workbench-todo__item"
:class="{ 'workbench-todo__item--clickable': Boolean(item.routeKey) }"
@click="handleClickItem(item)"
>
<div class="workbench-todo__leading">
<span class="workbench-todo__category" :class="`workbench-todo__category--${item.categoryTone}`">
{{ item.categoryLabel }}
</span>
<span v-if="item.highPriority" class="workbench-todo__priority"></span>
</div>
<div class="workbench-todo__body">
<h4 class="workbench-todo__item-title">{{ item.title }}</h4>
<div class="workbench-todo__meta">
<span class="workbench-todo__source">{{ item.source }}</span>
</div>
</div>
<div class="workbench-todo__trailing">
<span class="workbench-todo__deadline" :class="getDeadlineToneClass(item)">
{{ item.deadlineLabel }}
</span>
</div>
</article>
</div>
<ElEmpty v-else description="当前筛选下暂无待办" :image-size="72" />
</ElCard>
</template>
<style scoped>
.workbench-todo {
overflow: hidden;
}
:deep(.el-card__header) {
padding: 16px 18px;
border-bottom: 1px solid rgb(226 232 240 / 80%);
}
.workbench-todo__header {
display: flex;
flex-direction: column;
gap: 14px;
}
.workbench-todo__title-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.workbench-todo__title {
margin: 0;
color: rgb(15 23 42 / 98%);
font-size: 16px;
font-weight: 700;
}
.workbench-todo__desc {
margin: 0;
color: rgb(100 116 139 / 92%);
font-size: 13px;
line-height: 1.6;
}
.workbench-todo__tabs {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.workbench-todo__tab {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border: 1px solid rgb(226 232 240 / 92%);
border-radius: 999px;
background-color: rgb(255 255 255 / 96%);
color: rgb(71 85 105 / 94%);
font-size: 13px;
cursor: pointer;
transition: all 160ms ease;
}
.workbench-todo__tab:hover {
border-color: rgb(14 116 144 / 64%);
color: rgb(14 116 144 / 96%);
}
.workbench-todo__tab--active {
border-color: rgb(14 116 144 / 92%);
background-color: rgb(14 116 144 / 96%);
color: white;
}
.workbench-todo__tab--active:hover {
color: white;
}
.workbench-todo__tab-count {
padding: 1px 6px;
border-radius: 999px;
background-color: rgb(241 245 249 / 96%);
color: rgb(71 85 105 / 94%);
font-size: 11px;
font-weight: 600;
}
.workbench-todo__tab--active .workbench-todo__tab-count {
background-color: rgb(255 255 255 / 22%);
color: white;
}
.workbench-todo__list {
display: flex;
flex-direction: column;
gap: 10px;
}
.workbench-todo__item {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 14px;
padding: 14px 16px;
border: 1px solid rgb(226 232 240 / 90%);
border-radius: 16px;
background-color: rgb(255 255 255 / 98%);
transition:
border-color 160ms ease,
background-color 160ms ease;
}
.workbench-todo__item--clickable {
cursor: pointer;
}
.workbench-todo__item--clickable:hover {
border-color: rgb(14 116 144 / 60%);
background-color: rgb(240 253 250 / 84%);
}
.workbench-todo__leading {
display: flex;
align-items: center;
gap: 8px;
}
.workbench-todo__category {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
}
.workbench-todo__category--sky {
background-color: rgb(224 242 254 / 96%);
color: rgb(14 116 144 / 96%);
}
.workbench-todo__category--emerald {
background-color: rgb(220 252 231 / 96%);
color: rgb(5 150 105 / 96%);
}
.workbench-todo__category--amber {
background-color: rgb(254 243 199 / 96%);
color: rgb(180 83 9 / 96%);
}
.workbench-todo__category--rose {
background-color: rgb(255 228 230 / 96%);
color: rgb(190 18 60 / 96%);
}
.workbench-todo__category--violet {
background-color: rgb(237 233 254 / 96%);
color: rgb(109 40 217 / 96%);
}
.workbench-todo__priority {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 6px;
background-color: rgb(254 226 226 / 96%);
color: rgb(220 38 38 / 96%);
font-size: 11px;
font-weight: 800;
}
.workbench-todo__body {
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.workbench-todo__item-title {
margin: 0;
color: rgb(15 23 42 / 98%);
font-size: 14px;
font-weight: 600;
line-height: 1.5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.workbench-todo__meta {
display: flex;
align-items: center;
gap: 8px;
}
.workbench-todo__source {
color: rgb(100 116 139 / 92%);
font-size: 12px;
line-height: 1.5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.workbench-todo__trailing {
display: flex;
align-items: center;
}
.workbench-todo__deadline {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
}
.workbench-todo__deadline--slate {
background-color: rgb(241 245 249 / 96%);
color: rgb(71 85 105 / 94%);
}
.workbench-todo__deadline--amber {
background-color: rgb(254 243 199 / 96%);
color: rgb(180 83 9 / 96%);
}
.workbench-todo__deadline--rose {
background-color: rgb(255 228 230 / 96%);
color: rgb(190 18 60 / 96%);
}
@media (width <= 600px) {
.workbench-todo__item {
grid-template-columns: auto minmax(0, 1fr);
}
.workbench-todo__trailing {
grid-column: 1 / -1;
justify-content: flex-end;
}
}
</style>