feat(projects): 1、增加空白页占位;2、调试已开发功能;
This commit is contained in:
347
src/views/workbench/homepage.ts
Normal file
347
src/views/workbench/homepage.ts
Normal 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()]}`;
|
||||
}
|
||||
65
src/views/workbench/index.vue
Normal file
65
src/views/workbench/index.vue
Normal 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
194
src/views/workbench/mock.ts
Normal 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[];
|
||||
169
src/views/workbench/modules/workbench-activity-panel.vue
Normal file
169
src/views/workbench/modules/workbench-activity-panel.vue
Normal 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>
|
||||
318
src/views/workbench/modules/workbench-banner.vue
Normal file
318
src/views/workbench/modules/workbench-banner.vue
Normal 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>
|
||||
192
src/views/workbench/modules/workbench-kpi.vue
Normal file
192
src/views/workbench/modules/workbench-kpi.vue
Normal 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>
|
||||
311
src/views/workbench/modules/workbench-project-grid.vue
Normal file
311
src/views/workbench/modules/workbench-project-grid.vue
Normal 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>
|
||||
347
src/views/workbench/modules/workbench-todo-panel.vue
Normal file
347
src/views/workbench/modules/workbench-todo-panel.vue
Normal 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>
|
||||
Reference in New Issue
Block a user