refactor(projects): 删除废弃代码

This commit is contained in:
2026-06-05 16:29:35 +08:00
parent b72ad00912
commit 2e369b23a9
57 changed files with 37 additions and 9318 deletions

View File

@@ -1,5 +1,5 @@
{
"generatedAt": "2026-06-01T01:55:51.875Z",
"generatedAt": "2026-06-05T03:08:01.803Z",
"description": "Frontend visible page resource whitelist for backend route/menu configuration.",
"rules": {
"directoryComponent": "layout.base",
@@ -437,39 +437,6 @@
"pageType": "leaf",
"source": "generated"
},
{
"name": "personal-center_pending-approval",
"path": "/personal-center/pending-approval",
"component": "view.personal-center_pending-approval",
"title": "待我审批",
"routeTitle": "personal-center_pending-approval",
"i18nKey": "route.personal-center_pending-approval",
"icon": "mdi:check-decagram-outline",
"localIcon": null,
"order": 5,
"hideInMenu": false,
"keepAlive": true,
"activeMenu": null,
"multiTab": false,
"fixedIndexInTab": null,
"redirect": null,
"props": null,
"meta": {
"title": "待我审批",
"i18nKey": "route.personal-center_pending-approval",
"icon": "mdi:check-decagram-outline",
"localIcon": null,
"order": 5,
"keepAlive": true,
"hideInMenu": false,
"activeMenu": null,
"multiTab": false,
"fixedIndexInTab": null
},
"parentName": "personal-center",
"pageType": "leaf",
"source": "generated"
},
{
"name": "personal-center_overtime-application",
"path": "/personal-center/overtime-application",
@@ -503,6 +470,39 @@
"pageType": "leaf",
"source": "generated"
},
{
"name": "personal-center_pending-approval",
"path": "/personal-center/pending-approval",
"component": "view.personal-center_pending-approval",
"title": "待我审批",
"routeTitle": "personal-center_pending-approval",
"i18nKey": "route.personal-center_pending-approval",
"icon": "mdi:check-decagram-outline",
"localIcon": null,
"order": 7,
"hideInMenu": false,
"keepAlive": true,
"activeMenu": null,
"multiTab": false,
"fixedIndexInTab": null,
"redirect": null,
"props": null,
"meta": {
"title": "待我审批",
"i18nKey": "route.personal-center_pending-approval",
"icon": "mdi:check-decagram-outline",
"localIcon": null,
"order": 7,
"keepAlive": true,
"hideInMenu": false,
"activeMenu": null,
"multiTab": false,
"fixedIndexInTab": null
},
"parentName": "personal-center",
"pageType": "leaf",
"source": "generated"
},
{
"name": "system_user",
"path": "/system/user",

View File

@@ -37,61 +37,39 @@
"update-pkg": "sa update-pkg"
},
"dependencies": {
"@antv/data-set": "0.11.8",
"@antv/g2": "5.4.0",
"@antv/g6": "5.0.49",
"@better-scroll/core": "2.5.1",
"@iconify-vue/mingcute": "^1.0.5",
"@iconify/vue": "5.0.0",
"@sa/axios": "workspace:*",
"@sa/color": "workspace:*",
"@sa/hooks": "workspace:*",
"@sa/materials": "workspace:*",
"@sa/utils": "workspace:*",
"@visactor/vchart": "2.0.4",
"@visactor/vchart-theme": "1.12.2",
"@visactor/vtable-editors": "1.19.8",
"@visactor/vtable-gantt": "1.19.8",
"@visactor/vue-vtable": "1.19.8",
"@vueuse/components": "13.9.0",
"@vueuse/core": "13.9.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"clipboard": "2.0.11",
"dayjs": "1.11.18",
"defu": "^6.1.4",
"dhtmlx-gantt": "9.0.14",
"dompurify": "3.2.6",
"echarts": "6.0.0",
"element-plus": "^2.11.1",
"grid-layout-plus": "^1.1.1",
"jsbarcode": "3.12.1",
"jsencrypt": "^3.5.4",
"json5": "2.2.3",
"nprogress": "0.2.0",
"pinia": "3.0.3",
"pinyin-pro": "3.27.0",
"print-js": "1.6.0",
"swiper": "11.2.10",
"tailwind-merge": "3.3.1",
"typeit": "8.8.7",
"vditor": "3.11.2",
"vue": "3.5.20",
"vue-draggable-plus": "0.6.0",
"vue-i18n": "11.1.11",
"vue-pdf-embed": "2.1.3",
"vue-router": "4.5.1",
"xgplayer": "3.0.23",
"xlsx": "0.18.5"
"vue-router": "4.5.1"
},
"devDependencies": {
"@amap/amap-jsapi-types": "0.0.15",
"@elegant-router/vue": "0.3.8",
"@iconify/json": "2.2.380",
"@sa/scripts": "workspace:*",
"@sa/uno-preset": "workspace:*",
"@soybeanjs/eslint-config": "1.7.1",
"@types/bmapgl": "0.0.7",
"@types/node": "24.3.0",
"@types/nprogress": "0.2.3",
"@unocss/eslint-config": "66.5.0",

View File

@@ -1,61 +0,0 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { getPaletteColorByNumber } from '@sa/color';
defineOptions({ name: 'WaveBg' });
interface Props {
/** Theme color */
themeColor: string;
}
const props = defineProps<Props>();
const lightColor = computed(() => getPaletteColorByNumber(props.themeColor, 200));
const darkColor = computed(() => getPaletteColorByNumber(props.themeColor, 500));
</script>
<template>
<div class="absolute-lt z-1 size-full overflow-hidden">
<div class="absolute -right-300px -top-900px lt-sm:(-right-100px -top-1170px)">
<svg height="1337" width="1337">
<defs>
<path
id="path-1"
opacity="1"
fill-rule="evenodd"
d="M1337,668.5 C1337,1037.455193874239 1037.455193874239,1337 668.5,1337 C523.6725684305388,1337 337,1236 370.50000000000006,1094 C434.03835568300906,824.6732385973953 6.906089672974592e-14,892.6277623047779 0,668.5000000000001 C0,299.5448061257611 299.5448061257609,1.1368683772161603e-13 668.4999999999999,0 C1037.455193874239,0 1337,299.544806125761 1337,668.5Z"
/>
<linearGradient id="linearGradient-2" x1="0.79" y1="0.62" x2="0.21" y2="0.86">
<stop offset="0" :stop-color="lightColor" stop-opacity="1" />
<stop offset="1" :stop-color="darkColor" stop-opacity="1" />
</linearGradient>
</defs>
<g opacity="1">
<use xlink:href="#path-1" fill="url(#linearGradient-2)" fill-opacity="1" />
</g>
</svg>
</div>
<div class="absolute -bottom-400px -left-200px lt-sm:(-bottom-760px -left-100px)">
<svg height="896" width="967.8852157128662">
<defs>
<path
id="path-2"
opacity="1"
fill-rule="evenodd"
d="M896,448 C1142.6325445712241,465.5747656464056 695.2579309733121,896 448,896 C200.74206902668806,896 5.684341886080802e-14,695.2579309733121 0,448.0000000000001 C0,200.74206902668806 200.74206902668791,5.684341886080802e-14 447.99999999999994,0 C695.2579309733121,0 475,418 896,448Z"
/>
<linearGradient id="linearGradient-3" x1="0.5" y1="0" x2="0.5" y2="1">
<stop offset="0" :stop-color="darkColor" stop-opacity="1" />
<stop offset="1" :stop-color="lightColor" stop-opacity="1" />
</linearGradient>
</defs>
<g opacity="1">
<use xlink:href="#path-2" fill="url(#linearGradient-3)" fill-opacity="1" />
</g>
</svg>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -1,8 +0,0 @@
/** baidu map sdk url */
export const BAIDU_MAP_SDK_URL = `https://api.map.baidu.com/getscript?v=3.0&ak=KSezYymXPth1DIGILRX3oYN9PxbOQQmU&services=&t=20210201100830&s=1`;
/** Amap sdk url */
export const AMAP_SDK_URL = 'https://webapi.amap.com/maps?v=2.0&key=e7bd02bd504062087e6563daf4d6721d';
/** tencent sdk url */
export const TENCENT_MAP_SDK_URL = 'https://map.qq.com/api/gljs?v=1.exp&key=A6DBZ-KXPLW-JKSRY-ONZF4-CPHY3-K6BL7';

View File

@@ -1,158 +0,0 @@
import { computed, effectScope, onScopeDispose, ref, watch } from 'vue';
import { useElementSize } from '@vueuse/core';
import VChart, { registerLiquidChart } from '@visactor/vchart';
import type { ISpec, ITheme } from '@visactor/vchart';
import light from '@visactor/vchart-theme/public/light.json';
import dark from '@visactor/vchart-theme/public/dark.json';
import { useThemeStore } from '@/store/modules/theme';
registerLiquidChart();
// register the theme
VChart.ThemeManager.registerTheme('light', light as ITheme);
VChart.ThemeManager.registerTheme('dark', dark as ITheme);
interface ChartHooks {
onRender?: (chart: VChart) => void | Promise<void>;
onUpdated?: (chart: VChart) => void | Promise<void>;
onDestroy?: (chart: VChart) => void | Promise<void>;
}
export function useVChart<T extends ISpec>(specFactory: () => T, hooks: ChartHooks = {}) {
const scope = effectScope();
const themeStore = useThemeStore();
const darkMode = computed(() => themeStore.darkMode);
const domRef = ref<HTMLElement | null>(null);
const initialSize = { width: 0, height: 0 };
const { width, height } = useElementSize(domRef, initialSize);
let chart: VChart | null = null;
const spec: T = specFactory();
const { onRender, onUpdated, onDestroy } = hooks;
/**
* whether can render chart
*
* when domRef is ready and initialSize is valid
*/
function canRender() {
return domRef.value && initialSize.width > 0 && initialSize.height > 0;
}
/** is chart rendered */
function isRendered() {
return Boolean(domRef.value && chart);
}
/**
* update chart spec
*
* @param callback callback function
*/
async function updateSpec(callback: (opts: T, optsFactory: () => T) => ISpec = () => spec) {
if (!isRendered()) return;
const updatedOpts = callback(spec, specFactory);
Object.assign(spec, updatedOpts);
// if (isRendered()) {
// chart?.release();
// }
chart?.updateSpec({ ...updatedOpts }, true);
await onUpdated?.(chart!);
}
function setSpec(newSpec: T) {
chart?.updateSpec(newSpec);
}
/** render chart */
async function render() {
if (!isRendered()) {
// apply the theme
if (darkMode.value) {
VChart.ThemeManager.setCurrentTheme('dark');
} else {
VChart.ThemeManager.setCurrentTheme('light');
}
chart = new VChart(spec, { dom: domRef.value as HTMLElement });
chart.renderSync();
await onRender?.(chart);
}
}
/** resize chart */
function resize() {
// chart?.resize();
}
/** destroy chart */
async function destroy() {
if (!chart) return;
await onDestroy?.(chart);
chart?.release();
chart = null;
}
/** change chart theme */
async function changeTheme() {
await destroy();
await render();
await onUpdated?.(chart!);
}
/**
* render chart by size
*
* @param w width
* @param h height
*/
async function renderChartBySize(w: number, h: number) {
initialSize.width = w;
initialSize.height = h;
// size is abnormal, destroy chart
if (!canRender()) {
await destroy();
return;
}
// resize chart
if (isRendered()) {
resize();
}
// render chart
await render();
}
scope.run(() => {
watch([width, height], ([newWidth, newHeight]) => {
renderChartBySize(newWidth, newHeight);
});
watch(darkMode, () => {
changeTheme();
});
});
onScopeDispose(() => {
destroy();
scope.stop();
});
return {
domRef,
updateSpec,
setSpec
};
}

View File

@@ -178,16 +178,6 @@ const local: App.I18n.Schema = {
infra: 'Infra',
'infra_state-machine': 'State Machine',
'infra_rd-code': 'R&D Code',
function: 'System Function',
function_tab: 'Tab',
'function_multi-tab': 'Multi Tab',
'function_hide-child': 'Hide Child',
'function_hide-child_one': 'Hide Child',
'function_hide-child_two': 'Two',
'function_hide-child_three': 'Three',
function_request: 'Request',
'function_toggle-auth': 'Toggle Auth',
'function_super-page': 'Super Admin Visible',
product: 'Product',
product_list: 'Product List',
product_dashboard: 'Dashboard',
@@ -211,28 +201,7 @@ const local: App.I18n.Schema = {
exception: 'Exception',
exception_403: '403',
exception_404: '404',
exception_500: '500',
plugin: 'Plugin',
plugin_copy: 'Copy',
plugin_charts: 'Charts',
plugin_charts_echarts: 'ECharts',
plugin_charts_antv: 'AntV',
plugin_charts_vchart: 'VChart',
plugin_icon: 'Icon',
plugin_map: 'Map',
plugin_print: 'Print',
plugin_swiper: 'Swiper',
plugin_video: 'Video',
plugin_barcode: 'Barcode',
plugin_pinyin: 'pinyin',
plugin_excel: 'Excel',
plugin_pdf: 'PDF preview',
plugin_gantt: 'Gantt Chart',
plugin_gantt_dhtmlx: 'dhtmlxGantt',
plugin_gantt_vtable: 'VTableGantt',
plugin_typeit: 'Typeit',
plugin_tables: 'Tables',
plugin_tables_vtable: 'VTable'
exception_500: '500'
},
page: {
login: {
@@ -328,45 +297,6 @@ const local: App.I18n.Schema = {
},
creativity: 'Creativity'
},
function: {
tab: {
tabOperate: {
title: 'Tab Operation',
addTab: 'Add Tab',
addTabDesc: 'To user management page',
closeTab: 'Close Tab',
closeCurrentTab: 'Close Current Tab',
closeAboutTab: 'Close "User Management" Tab',
addMultiTab: 'Add Multi Tab',
addMultiTabDesc1: 'To MultiTab page',
addMultiTabDesc2: 'To MultiTab page(with query params)'
},
tabTitle: {
title: 'Tab Title',
changeTitle: 'Change Title',
change: 'Change',
resetTitle: 'Reset Title',
reset: 'Reset'
}
},
multiTab: {
routeParam: 'Route Param',
backTab: 'Back function_tab'
},
toggleAuth: {
toggleAccount: 'Toggle Account',
authHook: 'Auth Hook Function `hasAuth`',
superAdminVisible: 'Super Admin Visible',
adminVisible: 'Admin Visible',
adminOrUserVisible: 'Admin and User Visible'
},
request: {
repeatedErrorOccurOnce: 'Repeated Request Error Occurs Once',
repeatedError: 'Repeated Request Error',
repeatedErrorMsg1: 'Custom Request Error 1',
repeatedErrorMsg2: 'Custom Request Error 2'
}
},
system: {
common: {
status: {

View File

@@ -178,16 +178,6 @@ const local: App.I18n.Schema = {
infra: '基础设施',
'infra_state-machine': '状态机管理',
'infra_rd-code': '研发令号',
function: '系统功能',
function_tab: '标签页',
'function_multi-tab': '多标签页',
'function_hide-child': '隐藏子菜单',
'function_hide-child_one': '隐藏子菜单',
'function_hide-child_two': '菜单二',
'function_hide-child_three': '菜单三',
function_request: '请求',
'function_toggle-auth': '切换权限',
'function_super-page': '超级管理员可见',
product: '产品管理',
product_list: '产品列表',
product_dashboard: '产品仪表盘',
@@ -211,28 +201,7 @@ const local: App.I18n.Schema = {
exception: '异常页',
exception_403: '403',
exception_404: '404',
exception_500: '500',
plugin: '插件示例',
plugin_copy: '剪贴板',
plugin_charts: '图表',
plugin_charts_echarts: 'ECharts',
plugin_charts_antv: 'AntV',
plugin_charts_vchart: 'VChart',
plugin_icon: '图标',
plugin_map: '地图',
plugin_print: '打印',
plugin_swiper: 'Swiper',
plugin_video: '视频',
plugin_barcode: '条形码',
plugin_pinyin: '拼音',
plugin_excel: 'Excel',
plugin_pdf: 'PDF 预览',
plugin_gantt: '甘特图',
plugin_gantt_dhtmlx: 'dhtmlxGantt',
plugin_gantt_vtable: 'VTableGantt',
plugin_typeit: '打字机',
plugin_tables: '表格',
plugin_tables_vtable: 'VTable'
exception_500: '500'
},
page: {
login: {
@@ -327,45 +296,6 @@ const local: App.I18n.Schema = {
},
creativity: '创意'
},
function: {
tab: {
tabOperate: {
title: '标签页操作',
addTab: '添加标签页',
addTabDesc: '跳转到用户管理页面',
closeTab: '关闭标签页',
closeCurrentTab: '关闭当前标签页',
closeAboutTab: '关闭"用户管理"标签页',
addMultiTab: '添加多标签页',
addMultiTabDesc1: '跳转到多标签页页面',
addMultiTabDesc2: '跳转到多标签页页面(带有查询参数)'
},
tabTitle: {
title: '标签页标题',
changeTitle: '修改标题',
change: '修改',
resetTitle: '重置标题',
reset: '重置'
}
},
multiTab: {
routeParam: '路由参数',
backTab: '返回 function_tab'
},
toggleAuth: {
toggleAccount: '切换账号',
authHook: '权限钩子函数 `hasAuth`',
superAdminVisible: '超级管理员可见',
adminVisible: '管理员可见',
adminOrUserVisible: '管理员和用户可见'
},
request: {
repeatedErrorOccurOnce: '重复请求错误只出现一次',
repeatedError: '重复请求错误',
repeatedErrorMsg1: '自定义请求错误 1',
repeatedErrorMsg2: '自定义请求错误 2'
}
},
system: {
common: {
status: {

View File

@@ -3,6 +3,3 @@ import 'element-plus/dist/index.css';
import 'element-plus/theme-chalk/dark/css-vars.css';
import 'uno.css';
import '../styles/css/global.css';
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';

View File

@@ -20,14 +20,6 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
500: () => import("@/views/_builtin/500/index.vue"),
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
login: () => import("@/views/_builtin/login/index.vue"),
"function_hide-child_one": () => import("@/views/function/hide-child/one/index.vue"),
"function_hide-child_three": () => import("@/views/function/hide-child/three/index.vue"),
"function_hide-child_two": () => import("@/views/function/hide-child/two/index.vue"),
"function_multi-tab": () => import("@/views/function/multi-tab/index.vue"),
function_request: () => import("@/views/function/request/index.vue"),
"function_super-page": () => import("@/views/function/super-page/index.vue"),
function_tab: () => import("@/views/function/tab/index.vue"),
"function_toggle-auth": () => import("@/views/function/toggle-auth/index.vue"),
"infra_rd-code": () => import("@/views/infra/rd-code/index.vue"),
"infra_state-machine": () => import("@/views/infra/state-machine/index.vue"),
"metrics_member-efficiency": () => import("@/views/metrics/member-efficiency/index.vue"),
@@ -41,23 +33,6 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
"personal-center_my-weekly": () => import("@/views/personal-center/my-weekly/index.vue"),
"personal-center_overtime-application": () => import("@/views/personal-center/overtime-application/index.vue"),
"personal-center_pending-approval": () => import("@/views/personal-center/pending-approval/index.vue"),
plugin_barcode: () => import("@/views/plugin/barcode/index.vue"),
plugin_charts_antv: () => import("@/views/plugin/charts/antv/index.vue"),
plugin_charts_echarts: () => import("@/views/plugin/charts/echarts/index.vue"),
plugin_charts_vchart: () => import("@/views/plugin/charts/vchart/index.vue"),
plugin_copy: () => import("@/views/plugin/copy/index.vue"),
plugin_excel: () => import("@/views/plugin/excel/index.vue"),
plugin_gantt_dhtmlx: () => import("@/views/plugin/gantt/dhtmlx/index.vue"),
plugin_gantt_vtable: () => import("@/views/plugin/gantt/vtable/index.vue"),
plugin_icon: () => import("@/views/plugin/icon/index.vue"),
plugin_map: () => import("@/views/plugin/map/index.vue"),
plugin_pdf: () => import("@/views/plugin/pdf/index.vue"),
plugin_pinyin: () => import("@/views/plugin/pinyin/index.vue"),
plugin_print: () => import("@/views/plugin/print/index.vue"),
plugin_swiper: () => import("@/views/plugin/swiper/index.vue"),
plugin_tables_vtable: () => import("@/views/plugin/tables/vtable/index.vue"),
plugin_typeit: () => import("@/views/plugin/typeit/index.vue"),
plugin_video: () => import("@/views/plugin/video/index.vue"),
product_dashboard: () => import("@/views/product/dashboard/index.vue"),
product_list: () => import("@/views/product/list/index.vue"),
product_requirement: () => import("@/views/product/requirement/index.vue"),

View File

@@ -39,124 +39,6 @@ export const generatedRoutes: GeneratedRoute[] = [
hideInMenu: true
}
},
{
name: 'function',
path: '/function',
component: 'layout.base',
meta: {
title: 'function',
i18nKey: 'route.function',
icon: 'icon-park-outline:all-application',
order: 6
},
children: [
{
name: 'function_hide-child',
path: '/function/hide-child',
meta: {
title: 'function_hide-child',
i18nKey: 'route.function_hide-child',
icon: 'material-symbols:filter-list-off',
order: 2
},
redirect: '/function/hide-child/one',
children: [
{
name: 'function_hide-child_one',
path: '/function/hide-child/one',
component: 'view.function_hide-child_one',
meta: {
title: 'function_hide-child_one',
i18nKey: 'route.function_hide-child_one',
icon: 'material-symbols:filter-list-off',
hideInMenu: true,
activeMenu: 'function_hide-child'
}
},
{
name: 'function_hide-child_three',
path: '/function/hide-child/three',
component: 'view.function_hide-child_three',
meta: {
title: 'function_hide-child_three',
i18nKey: 'route.function_hide-child_three',
hideInMenu: true,
activeMenu: 'function_hide-child'
}
},
{
name: 'function_hide-child_two',
path: '/function/hide-child/two',
component: 'view.function_hide-child_two',
meta: {
title: 'function_hide-child_two',
i18nKey: 'route.function_hide-child_two',
hideInMenu: true,
activeMenu: 'function_hide-child'
}
}
]
},
{
name: 'function_multi-tab',
path: '/function/multi-tab',
component: 'view.function_multi-tab',
meta: {
title: 'function_multi-tab',
i18nKey: 'route.function_multi-tab',
icon: 'ic:round-tab',
multiTab: true,
hideInMenu: true,
activeMenu: 'function_tab'
}
},
{
name: 'function_request',
path: '/function/request',
component: 'view.function_request',
meta: {
title: 'function_request',
i18nKey: 'route.function_request',
icon: 'carbon:network-overlay',
order: 3
}
},
{
name: 'function_super-page',
path: '/function/super-page',
component: 'view.function_super-page',
meta: {
title: 'function_super-page',
i18nKey: 'route.function_super-page',
icon: 'ic:round-supervisor-account',
order: 5,
roles: ['R_SUPER']
}
},
{
name: 'function_tab',
path: '/function/tab',
component: 'view.function_tab',
meta: {
title: 'function_tab',
i18nKey: 'route.function_tab',
icon: 'ic:round-tab',
order: 1
}
},
{
name: 'function_toggle-auth',
path: '/function/toggle-auth',
component: 'view.function_toggle-auth',
meta: {
title: 'function_toggle-auth',
i18nKey: 'route.function_toggle-auth',
icon: 'ic:round-construction',
order: 4
}
}
]
},
{
name: 'iframe-page',
path: '/iframe-page/:url',
@@ -377,223 +259,6 @@ export const generatedRoutes: GeneratedRoute[] = [
}
]
},
{
name: 'plugin',
path: '/plugin',
component: 'layout.base',
meta: {
title: '插件示例',
i18nKey: 'route.plugin',
order: 7,
icon: 'clarity:plugin-line'
},
children: [
{
name: 'plugin_barcode',
path: '/plugin/barcode',
component: 'view.plugin_barcode',
meta: {
title: 'plugin_barcode',
i18nKey: 'route.plugin_barcode',
icon: 'ic:round-barcode'
}
},
{
name: 'plugin_charts',
path: '/plugin/charts',
meta: {
title: 'plugin_charts',
i18nKey: 'route.plugin_charts',
icon: 'mdi:chart-areaspline'
},
children: [
{
name: 'plugin_charts_antv',
path: '/plugin/charts/antv',
component: 'view.plugin_charts_antv',
meta: {
title: 'plugin_charts_antv',
i18nKey: 'route.plugin_charts_antv',
icon: 'hugeicons:flow-square'
}
},
{
name: 'plugin_charts_echarts',
path: '/plugin/charts/echarts',
component: 'view.plugin_charts_echarts',
meta: {
title: 'plugin_charts_echarts',
i18nKey: 'route.plugin_charts_echarts',
icon: 'simple-icons:apacheecharts'
}
},
{
name: 'plugin_charts_vchart',
path: '/plugin/charts/vchart',
component: 'view.plugin_charts_vchart',
meta: {
title: 'plugin_charts_vchart',
i18nKey: 'route.plugin_charts_vchart',
localIcon: 'visactor'
}
}
]
},
{
name: 'plugin_copy',
path: '/plugin/copy',
component: 'view.plugin_copy',
meta: {
title: 'plugin_copy',
i18nKey: 'route.plugin_copy',
icon: 'mdi:clipboard-outline'
}
},
{
name: 'plugin_excel',
path: '/plugin/excel',
component: 'view.plugin_excel',
meta: {
title: 'plugin_excel',
i18nKey: 'route.plugin_excel',
icon: 'ri:file-excel-2-line',
keepAlive: true
}
},
{
name: 'plugin_gantt',
path: '/plugin/gantt',
meta: {
title: 'plugin_gantt',
i18nKey: 'route.plugin_gantt',
icon: 'ant-design:bar-chart-outlined'
},
children: [
{
name: 'plugin_gantt_dhtmlx',
path: '/plugin/gantt/dhtmlx',
component: 'view.plugin_gantt_dhtmlx',
meta: {
title: 'plugin_gantt_dhtmlx',
i18nKey: 'route.plugin_gantt_dhtmlx',
icon: 'gridicons:posts'
}
},
{
name: 'plugin_gantt_vtable',
path: '/plugin/gantt/vtable',
component: 'view.plugin_gantt_vtable',
meta: {
title: 'plugin_gantt_vtable',
i18nKey: 'route.plugin_gantt_vtable',
localIcon: 'visactor'
}
}
]
},
{
name: 'plugin_icon',
path: '/plugin/icon',
component: 'view.plugin_icon',
meta: {
title: 'plugin_icon',
i18nKey: 'route.plugin_icon',
localIcon: 'custom-icon'
}
},
{
name: 'plugin_map',
path: '/plugin/map',
component: 'view.plugin_map',
meta: {
title: 'plugin_map',
i18nKey: 'route.plugin_map',
icon: 'mdi:map'
}
},
{
name: 'plugin_pdf',
path: '/plugin/pdf',
component: 'view.plugin_pdf',
meta: {
title: 'plugin_pdf',
i18nKey: 'route.plugin_pdf',
icon: 'uiw:file-pdf'
}
},
{
name: 'plugin_pinyin',
path: '/plugin/pinyin',
component: 'view.plugin_pinyin',
meta: {
title: 'plugin_pinyin',
i18nKey: 'route.plugin_pinyin',
icon: 'entypo-social:google-hangouts'
}
},
{
name: 'plugin_print',
path: '/plugin/print',
component: 'view.plugin_print',
meta: {
title: 'plugin_print',
i18nKey: 'route.plugin_print',
icon: 'mdi:printer'
}
},
{
name: 'plugin_swiper',
path: '/plugin/swiper',
component: 'view.plugin_swiper',
meta: {
title: 'plugin_swiper',
i18nKey: 'route.plugin_swiper',
icon: 'simple-icons:swiper'
}
},
{
name: 'plugin_tables',
path: '/plugin/tables',
meta: {
title: 'plugin_tables',
i18nKey: 'route.plugin_tables',
icon: 'icon-park-outline:table'
},
children: [
{
name: 'plugin_tables_vtable',
path: '/plugin/tables/vtable',
component: 'view.plugin_tables_vtable',
meta: {
title: 'plugin_tables_vtable',
i18nKey: 'route.plugin_tables_vtable',
localIcon: 'visactor'
}
}
]
},
{
name: 'plugin_typeit',
path: '/plugin/typeit',
component: 'view.plugin_typeit',
meta: {
title: 'plugin_typeit',
i18nKey: 'route.plugin_typeit',
icon: 'mdi:typewriter'
}
},
{
name: 'plugin_video',
path: '/plugin/video',
component: 'view.plugin_video',
meta: {
title: 'plugin_video',
i18nKey: 'route.plugin_video',
icon: 'mdi:video'
}
}
]
},
{
name: 'product',
path: '/product',

View File

@@ -170,16 +170,6 @@ const routeMap: RouteMap = {
"403": "/403",
"404": "/404",
"500": "/500",
"function": "/function",
"function_hide-child": "/function/hide-child",
"function_hide-child_one": "/function/hide-child/one",
"function_hide-child_three": "/function/hide-child/three",
"function_hide-child_two": "/function/hide-child/two",
"function_multi-tab": "/function/multi-tab",
"function_request": "/function/request",
"function_super-page": "/function/super-page",
"function_tab": "/function/tab",
"function_toggle-auth": "/function/toggle-auth",
"iframe-page": "/iframe-page/:url",
"infra": "/infra",
"infra_rd-code": "/infra/rd-code",
@@ -198,27 +188,6 @@ const routeMap: RouteMap = {
"personal-center_my-weekly": "/personal-center/my-weekly",
"personal-center_overtime-application": "/personal-center/overtime-application",
"personal-center_pending-approval": "/personal-center/pending-approval",
"plugin": "/plugin",
"plugin_barcode": "/plugin/barcode",
"plugin_charts": "/plugin/charts",
"plugin_charts_antv": "/plugin/charts/antv",
"plugin_charts_echarts": "/plugin/charts/echarts",
"plugin_charts_vchart": "/plugin/charts/vchart",
"plugin_copy": "/plugin/copy",
"plugin_excel": "/plugin/excel",
"plugin_gantt": "/plugin/gantt",
"plugin_gantt_dhtmlx": "/plugin/gantt/dhtmlx",
"plugin_gantt_vtable": "/plugin/gantt/vtable",
"plugin_icon": "/plugin/icon",
"plugin_map": "/plugin/map",
"plugin_pdf": "/plugin/pdf",
"plugin_pinyin": "/plugin/pinyin",
"plugin_print": "/plugin/print",
"plugin_swiper": "/plugin/swiper",
"plugin_tables": "/plugin/tables",
"plugin_tables_vtable": "/plugin/tables/vtable",
"plugin_typeit": "/plugin/typeit",
"plugin_video": "/plugin/video",
"product": "/product",
"product_dashboard": "/product/dashboard",
"product_list": "/product/list",

39
src/typings/app.d.ts vendored
View File

@@ -504,45 +504,6 @@ declare namespace App {
};
creativity: string;
};
function: {
tab: {
tabOperate: {
title: string;
addTab: string;
addTabDesc: string;
closeTab: string;
closeCurrentTab: string;
closeAboutTab: string;
addMultiTab: string;
addMultiTabDesc1: string;
addMultiTabDesc2: string;
};
tabTitle: {
title: string;
changeTitle: string;
change: string;
resetTitle: string;
reset: string;
};
};
multiTab: {
routeParam: string;
backTab: string;
};
toggleAuth: {
toggleAccount: string;
authHook: string;
superAdminVisible: string;
adminVisible: string;
adminOrUserVisible: string;
};
request: {
repeatedErrorOccurOnce: string;
repeatedError: string;
repeatedErrorMsg1: string;
repeatedErrorMsg2: string;
};
};
system: {
common: {
status: {

View File

@@ -24,16 +24,6 @@ declare module "@elegant-router/types" {
"403": "/403";
"404": "/404";
"500": "/500";
"function": "/function";
"function_hide-child": "/function/hide-child";
"function_hide-child_one": "/function/hide-child/one";
"function_hide-child_three": "/function/hide-child/three";
"function_hide-child_two": "/function/hide-child/two";
"function_multi-tab": "/function/multi-tab";
"function_request": "/function/request";
"function_super-page": "/function/super-page";
"function_tab": "/function/tab";
"function_toggle-auth": "/function/toggle-auth";
"iframe-page": "/iframe-page/:url";
"infra": "/infra";
"infra_rd-code": "/infra/rd-code";
@@ -52,27 +42,6 @@ declare module "@elegant-router/types" {
"personal-center_my-weekly": "/personal-center/my-weekly";
"personal-center_overtime-application": "/personal-center/overtime-application";
"personal-center_pending-approval": "/personal-center/pending-approval";
"plugin": "/plugin";
"plugin_barcode": "/plugin/barcode";
"plugin_charts": "/plugin/charts";
"plugin_charts_antv": "/plugin/charts/antv";
"plugin_charts_echarts": "/plugin/charts/echarts";
"plugin_charts_vchart": "/plugin/charts/vchart";
"plugin_copy": "/plugin/copy";
"plugin_excel": "/plugin/excel";
"plugin_gantt": "/plugin/gantt";
"plugin_gantt_dhtmlx": "/plugin/gantt/dhtmlx";
"plugin_gantt_vtable": "/plugin/gantt/vtable";
"plugin_icon": "/plugin/icon";
"plugin_map": "/plugin/map";
"plugin_pdf": "/plugin/pdf";
"plugin_pinyin": "/plugin/pinyin";
"plugin_print": "/plugin/print";
"plugin_swiper": "/plugin/swiper";
"plugin_tables": "/plugin/tables";
"plugin_tables_vtable": "/plugin/tables/vtable";
"plugin_typeit": "/plugin/typeit";
"plugin_video": "/plugin/video";
"product": "/product";
"product_dashboard": "/product/dashboard";
"product_list": "/product/list";
@@ -135,13 +104,11 @@ declare module "@elegant-router/types" {
| "403"
| "404"
| "500"
| "function"
| "iframe-page"
| "infra"
| "login"
| "metrics"
| "personal-center"
| "plugin"
| "product"
| "project"
| "system"
@@ -169,14 +136,6 @@ declare module "@elegant-router/types" {
| "500"
| "iframe-page"
| "login"
| "function_hide-child_one"
| "function_hide-child_three"
| "function_hide-child_two"
| "function_multi-tab"
| "function_request"
| "function_super-page"
| "function_tab"
| "function_toggle-auth"
| "infra_rd-code"
| "infra_state-machine"
| "metrics_member-efficiency"
@@ -190,23 +149,6 @@ declare module "@elegant-router/types" {
| "personal-center_my-weekly"
| "personal-center_overtime-application"
| "personal-center_pending-approval"
| "plugin_barcode"
| "plugin_charts_antv"
| "plugin_charts_echarts"
| "plugin_charts_vchart"
| "plugin_copy"
| "plugin_excel"
| "plugin_gantt_dhtmlx"
| "plugin_gantt_vtable"
| "plugin_icon"
| "plugin_map"
| "plugin_pdf"
| "plugin_pinyin"
| "plugin_print"
| "plugin_swiper"
| "plugin_tables_vtable"
| "plugin_typeit"
| "plugin_video"
| "product_dashboard"
| "product_list"
| "product_requirement"

View File

@@ -1,20 +0,0 @@
/// <reference types="@amap/amap-jsapi-types" />
/// <reference types="bmapgl" />
declare namespace BMap {
class Map extends BMapGL.Map {}
class Point extends BMapGL.Point {}
}
declare const TMap: any;
interface Window {
/**
* make baidu map request under https protocol
*
* - 0: http
* - 1: https
* - 2: https
*/
HOST_TYPE: '0' | '1' | '2';
}

View File

@@ -1,85 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { Component } from 'vue';
import { getPaletteColorByNumber, mixColor } from '@sa/color';
import { loginModuleRecord } from '@/constants/app';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { $t } from '@/locales';
import PwdLogin from './modules/pwd-login.vue';
import ResetPwd from './modules/reset-pwd.vue';
defineOptions({ name: 'LoginPage' });
interface Props {
/** The login module */
module?: UnionKey.LoginModule;
}
const props = defineProps<Props>();
const appStore = useAppStore();
const themeStore = useThemeStore();
interface LoginModule {
label: App.I18n.I18nKey;
component: Component;
}
const moduleMap: Record<UnionKey.LoginModule, LoginModule> = {
'pwd-login': { label: loginModuleRecord['pwd-login'], component: PwdLogin },
'reset-pwd': { label: loginModuleRecord['reset-pwd'], component: ResetPwd }
};
const activeModule = computed(() => moduleMap[props.module || 'pwd-login']);
const bgThemeColor = computed(() =>
themeStore.darkMode ? getPaletteColorByNumber(themeStore.themeColor, 600) : themeStore.themeColor
);
const bgColor = computed(() => {
const COLOR_WHITE = '#ffffff';
const ratio = themeStore.darkMode ? 0.5 : 0.2;
return mixColor(COLOR_WHITE, themeStore.themeColor, ratio);
});
</script>
<template>
<div class="relative size-full flex-center overflow-hidden" :style="{ backgroundColor: bgColor }">
<WaveBg :theme-color="bgThemeColor" />
<ElCard class="relative z-4 w-auto rd-12px">
<div class="w-400px lt-sm:w-300px">
<header class="flex-y-center justify-between">
<SystemLogo class="text-64px text-primary lt-sm:text-48px" />
<h3 class="text-28px text-primary font-500 lt-sm:text-22px">{{ $t('system.title') }}</h3>
<div class="i-flex-col">
<ThemeSchemaSwitch
:theme-schema="themeStore.themeScheme"
:show-tooltip="false"
class="text-20px lt-sm:text-18px"
@switch="themeStore.toggleThemeScheme"
/>
<LangSwitch
v-if="themeStore.header.multilingual.visible"
:lang="appStore.locale"
:lang-options="appStore.localeOptions"
:show-tooltip="false"
@change-lang="appStore.changeLocale"
/>
</div>
</header>
<main class="pt-15px">
<div class="pt-15px">
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
<component :is="activeModule.component" />
</Transition>
</div>
</main>
</div>
</ElCard>
</div>
</template>
<style scoped></style>

View File

@@ -1,7 +0,0 @@
<script setup lang="ts"></script>
<template>
<LookForward />
</template>
<style scoped></style>

View File

@@ -1,7 +0,0 @@
<script setup lang="ts"></script>
<template>
<LookForward />
</template>
<style scoped></style>

View File

@@ -1,7 +0,0 @@
<script setup lang="ts"></script>
<template>
<LookForward />
</template>
<style scoped></style>

View File

@@ -1,24 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useRouterPush } from '@/hooks/common/router';
import { $t } from '@/locales';
const route = useRoute();
const { routerPushByKey } = useRouterPush();
const routeQuery = computed(() => JSON.stringify(route.query));
</script>
<template>
<div>
<LookForward>
<div>
<ElButton @click="routerPushByKey('function_tab')">{{ $t('page.function.multiTab.backTab') }}</ElButton>
<div class="py-24px">{{ $t('page.function.multiTab.routeParam') }}: {{ routeQuery }}</div>
</div>
</LookForward>
</div>
</template>
<style scoped></style>

View File

@@ -1,57 +0,0 @@
<script setup lang="ts">
import { fetchCustomBackendError } from '@/service/api';
import { $t } from '@/locales';
async function logout() {
await fetchCustomBackendError('8888', $t('request.logoutMsg'));
}
async function logoutWithModal() {
await fetchCustomBackendError('7777', $t('request.logoutWithModalMsg'));
}
async function refreshToken() {
await fetchCustomBackendError('9999', $t('request.tokenExpired'));
}
async function handleRepeatedMessageError() {
await Promise.all([
fetchCustomBackendError('2222', $t('page.function.request.repeatedErrorMsg1')),
fetchCustomBackendError('2222', $t('page.function.request.repeatedErrorMsg1')),
fetchCustomBackendError('2222', $t('page.function.request.repeatedErrorMsg1')),
fetchCustomBackendError('3333', $t('page.function.request.repeatedErrorMsg2')),
fetchCustomBackendError('3333', $t('page.function.request.repeatedErrorMsg2')),
fetchCustomBackendError('3333', $t('page.function.request.repeatedErrorMsg2'))
]);
}
async function handleRepeatedModalError() {
await Promise.all([
fetchCustomBackendError('7777', $t('request.logoutWithModalMsg')),
fetchCustomBackendError('7777', $t('request.logoutWithModalMsg')),
fetchCustomBackendError('7777', $t('request.logoutWithModalMsg'))
]);
}
</script>
<template>
<ElSpace direction="vertical" fill :size="16">
<ElCard :header="$t('request.logout')" class="card-wrapper">
<ElButton @click="logout">{{ $t('common.trigger') }}</ElButton>
</ElCard>
<ElCard :header="$t('request.logoutWithModal')" class="card-wrapper">
<ElButton @click="logoutWithModal">{{ $t('common.trigger') }}</ElButton>
</ElCard>
<ElCard :header="$t('request.refreshToken')" class="card-wrapper">
<ElButton @click="refreshToken">{{ $t('common.trigger') }}</ElButton>
</ElCard>
<ElCard :header="$t('page.function.request.repeatedErrorOccurOnce')" class="card-wrapper">
<ElButton @click="handleRepeatedMessageError">{{ $t('page.function.request.repeatedError') }}(Message)</ElButton>
<ElButton class="ml-12px" @click="handleRepeatedModalError">
{{ $t('page.function.request.repeatedError') }}(Modal)
</ElButton>
</ElCard>
</ElSpace>
</template>
<style scoped></style>

View File

@@ -1,7 +0,0 @@
<script setup lang="ts"></script>
<template>
<LookForward />
</template>
<style scoped></style>

View File

@@ -1,62 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useTabStore } from '@/store/modules/tab';
import { useRouterPush } from '@/hooks/common/router';
import { $t } from '@/locales';
defineOptions({ name: 'TabPage' });
const tabStore = useTabStore();
const { routerPushByKey } = useRouterPush();
const tabLabel = ref('');
function changeTabLabel() {
tabStore.setTabLabel(tabLabel.value);
}
function resetTabLabel() {
tabStore.resetTabLabel();
}
</script>
<template>
<ElSpace direction="vertical" fill :size="16">
<ElCard :header="$t('page.function.tab.tabOperate.title')" class="card-wrapper">
<ElDivider content-position="left">{{ $t('page.function.tab.tabOperate.addTab') }}</ElDivider>
<ElButton @click="routerPushByKey('system_user')">{{ $t('page.function.tab.tabOperate.addTabDesc') }}</ElButton>
<ElDivider content-position="left">{{ $t('page.function.tab.tabOperate.closeTab') }}</ElDivider>
<ElSpace>
<ElButton @click="tabStore.removeActiveTab">
{{ $t('page.function.tab.tabOperate.closeCurrentTab') }}
</ElButton>
<ElButton @click="tabStore.removeTabByRouteName('system_user')">
{{ $t('page.function.tab.tabOperate.closeAboutTab') }}
</ElButton>
</ElSpace>
<ElDivider content-position="left">{{ $t('page.function.tab.tabOperate.addMultiTab') }}</ElDivider>
<ElSpace>
<ElButton @click="routerPushByKey('function_multi-tab')">
{{ $t('page.function.tab.tabOperate.addMultiTabDesc1') }}
</ElButton>
<ElButton @click="routerPushByKey('function_multi-tab', { query: { a: '1' } })">
{{ $t('page.function.tab.tabOperate.addMultiTabDesc2') }}
</ElButton>
</ElSpace>
</ElCard>
<ElCard :header="$t('page.function.tab.tabTitle.title')" class="card-wrapper">
<ElDivider content-position="left">{{ $t('page.function.tab.tabTitle.changeTitle') }}</ElDivider>
<ElInput v-model="tabLabel" class="max-w-240px">
<template #append>
<ElButton type="primary" @click="changeTabLabel">{{ $t('page.function.tab.tabTitle.change') }}</ElButton>
</template>
</ElInput>
<ElDivider content-position="left">{{ $t('page.function.tab.tabTitle.resetTitle') }}</ElDivider>
<ElButton type="danger" plain class="w-80px" @click="resetTabLabel">
{{ $t('page.function.tab.tabTitle.reset') }}
</ElButton>
</ElCard>
</ElSpace>
</template>
<style scoped></style>

View File

@@ -1,99 +0,0 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useLoading } from '@sa/hooks';
import { useAppStore } from '@/store/modules/app';
import { useAuthStore } from '@/store/modules/auth';
import { useTabStore } from '@/store/modules/tab';
import { useAuth } from '@/hooks/business/auth';
import { $t } from '@/locales';
defineOptions({ name: 'ToggleAuth' });
const route = useRoute();
const appStore = useAppStore();
const authStore = useAuthStore();
const tabStore = useTabStore();
const { hasAuth } = useAuth();
const { loading, startLoading, endLoading } = useLoading();
type AccountKey = 'super' | 'admin' | 'user';
interface Account {
key: AccountKey;
label: string;
userName: string;
password: string;
}
const accounts = computed<Account[]>(() => [
{
key: 'super',
label: $t('page.login.pwdLogin.superAdmin'),
userName: 'Super',
password: '123456'
},
{
key: 'admin',
label: $t('page.login.pwdLogin.admin'),
userName: 'Admin',
password: '123456'
},
{
key: 'user',
label: $t('page.login.pwdLogin.user'),
userName: 'User',
password: '123456'
}
]);
const loginAccount = ref<AccountKey>('super');
async function handleToggleAccount(account: Account) {
loginAccount.value = account.key;
startLoading();
await authStore.login(account.userName, account.password, false);
tabStore.initTabStore(route);
endLoading();
appStore.reloadPage();
}
</script>
<template>
<ElSpace direction="vertical" fill :size="16">
<ElCard :header="$t('route.function_toggle-auth')" class="card-wrapper">
<ElDescriptions direction="vertical" border :column="1">
<ElDescriptionsItem :label="$t('page.system.user.userRole')">
<ElSpace>
<ElTag v-for="role in authStore.userInfo.roles" :key="role">{{ role }}</ElTag>
</ElSpace>
</ElDescriptionsItem>
<ElDescriptionsItem ions-item :label="$t('page.function.toggleAuth.toggleAccount')">
<ElSpace>
<ElButton
v-for="account in accounts"
:key="account.key"
:loading="loading && loginAccount === account.key"
:disabled="loading && loginAccount !== account.key"
@click="handleToggleAccount(account)"
>
{{ account.label }}
</ElButton>
</ElSpace>
</ElDescriptionsItem>
</ElDescriptions>
</ElCard>
<ElCard :header="$t('page.function.toggleAuth.authHook')" class="card-wrapper">
<ElSpace>
<ElButton v-if="hasAuth('B_CODE1')">{{ $t('page.function.toggleAuth.superAdminVisible') }}</ElButton>
<ElButton v-if="hasAuth('B_CODE2')">{{ $t('page.function.toggleAuth.adminVisible') }}</ElButton>
<ElButton v-if="hasAuth('B_CODE3')">
{{ $t('page.function.toggleAuth.adminOrUserVisible') }}
</ElButton>
</ElSpace>
</ElCard>
</ElSpace>
</template>
<style scoped></style>

View File

@@ -1,116 +0,0 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import JsBarcode from 'jsbarcode';
import type { Options } from 'jsbarcode';
defineOptions({ name: 'BarcodePage' });
const text = 'CN-RDMS';
interface CodeConfig {
id: string;
title: string;
text: string;
options: Options;
}
const codes: CodeConfig[] = [
{
id: 'code39',
title: 'CODE 39 正常尺寸',
text: 'Hello',
options: { format: 'code39' }
},
{
id: 'code128',
title: 'CODE 128 正常尺寸',
text,
options: {}
},
{
id: 'ean-13',
title: 'ENA-13 商品条形码',
text: '1234567890128',
options: { format: 'ean13' }
},
{
id: 'upc-a',
title: 'UPC-A 商品条形码',
text: '123456789012',
options: { format: 'upc' }
},
{
id: 'barcode',
title: '不一样的高度,不一样的颜色',
text: 'Hello',
options: {
height: 30,
lineColor: '#9ca3af'
}
},
{
id: 'barcode1',
title: '加个背景色',
text,
options: {
background: '#9ca3af',
lineColor: '#ffffff'
}
},
{
id: 'barcode2',
title: '字体好大',
text,
options: {
fontSize: 40
}
},
{
id: 'barcode3',
title: '粗狂的条码,文字离远点',
text: 'Hi',
options: {
textMargin: 30,
width: 4
}
},
{
id: 'barcode4',
title: '字体跑上面来,还是粗体',
text,
options: {
textPosition: 'top',
fontOptions: 'bold'
}
}
];
function generateBarcode() {
codes.forEach(code => {
JsBarcode(`#${code.id}`, code.text, code.options);
});
}
onMounted(() => {
generateBarcode();
});
</script>
<template>
<div class="overflow-hidden">
<ElCard header="条形码" class="h-full card-wrapper">
<ElScrollbar class="h-full">
<ElRow :gutter="12" class="w-[calc(100%-12px)]">
<ElCol v-for="item in codes" :key="item.id" :lg="8" :md="12" :sm="24" class="mb-24px">
<div class="flex-col-center">
<h3>{{ item.title }}</h3>
<svg :id="item.id" class="h-130px" />
</div>
</ElCol>
</ElRow>
</ElScrollbar>
</ElCard>
</div>
</template>
<style scoped></style>

View File

@@ -1,60 +0,0 @@
import type { CustomGraphData } from './modules/types';
// 日期可以自己随便设置,就是字符串展示,也可以修改为业务需要的字段
export function getFlowData(): CustomGraphData {
return {
nodes: [
{
id: 'NS',
name: 'Start',
status: 'COMPLETED',
startDate: '2024-10-01',
endDate: '2024-10-07',
actualStartDate: '2024-10-01',
actualEndDate: '2024-10-07'
},
{
id: 'N1',
name: 'Node1',
status: 'COMPLETED_EARLY',
startDate: '2024-10-08',
endDate: '2024-10-10',
actualStartDate: '2024-10-08',
actualEndDate: '2024-10-09',
milestone: true
},
{
id: 'N2',
name: 'Node2',
status: 'COMPLETED_EARLY',
startDate: '2024-10-11',
endDate: '2024-10-13',
actualStartDate: '2024-10-11',
actualEndDate: '2024-10-12'
},
{ id: 'N3', name: 'Node3', status: 'IN_PROGRESS', isDeleted: true },
{ id: 'N4', name: 'Node4', status: 'COMPLETED_LATE' },
{ id: 'N5', name: 'Node5', status: 'DELAYED', isDelayed: true, milestone: true },
{ id: 'N6', name: 'Node6', status: 'PAUSED' },
{ id: 'N7', name: 'Node7', status: 'NOT_STARTED' },
{ id: 'N8', name: 'Node8', status: 'NOT_STARTED' },
{ id: 'N9', name: 'End', status: 'NOT_STARTED' },
{ id: 'NX', name: 'NodeX', status: 'NOT_STARTED', isDeleted: true }
],
edges: [
{ id: 'E1', source: 'NS', target: 'N1' },
{ id: 'E2', source: 'N1', target: 'N2' },
{ id: 'E3', source: 'N1', target: 'N3', isDeleted: true },
{ id: 'E4', source: 'N1', target: 'N4' },
{ id: 'E5', source: 'N2', target: 'N5' },
{ id: 'E6', source: 'N3', target: 'N5', isDeleted: true },
{ id: 'E7', source: 'N4', target: 'N5' },
{ id: 'E8', source: 'N5', target: 'N6' },
{ id: 'E9', source: 'N6', target: 'N7' },
{ id: 'E10', source: 'N6', target: 'N8' },
{ id: 'E11', source: 'N7', target: 'N9' },
{ id: 'EX', source: 'N8', target: 'N9' },
{ id: 'EO', source: 'N5', target: 'NX', isDeleted: true }
]
};
}

View File

@@ -1,67 +0,0 @@
<script setup lang="tsx">
import { computed, onMounted, ref, useTemplateRef } from 'vue';
import type { Ref } from 'vue';
import type { CustomBehaviorOption, IPointerEvent } from '@antv/g6';
import AntvFlow from './modules/antv-flow.vue';
import type { CustomGraphData } from './modules/types';
import { getFlowData } from './data';
defineOptions({ name: 'AntVCharts' });
const antvFlowRef = useTemplateRef('antvFlowRef');
const flowData = ref({
nodes: [],
edges: []
}) as Ref<CustomGraphData>;
const selectedNode = ref<string | undefined>('N2');
const behaviors: CustomBehaviorOption[] = [
{
type: 'click-select',
enable: (event: IPointerEvent) => event.targetType === 'node',
onClick: (event: IPointerEvent) => {
const node = event.target as unknown as HTMLElement;
const nodeData = flowData.value.nodes.find(item => item.id === node.id);
selectedNode.value = nodeData?.id;
window.$message?.success(`选中节点:[${node.id}]${nodeData?.name}`);
}
}
];
const hasNodeN = computed(() => flowData.value.nodes.some(node => node.id === 'NN'));
function addNode() {
const { nodes, edges } = flowData.value;
nodes.push({ id: 'NN', name: 'New node', status: 'NOT_STARTED' });
edges.push({ id: 'EN', source: 'N5', target: 'NN' });
flowData.value = { nodes, edges };
}
function removeNode(id: string) {
const { nodes, edges } = flowData.value;
// 删除node的同时也需要删除包含NX的edge
flowData.value = {
nodes: nodes.filter(node => node.id !== id),
edges: edges.filter(edge => edge.source !== id && edge.target !== id)
};
}
onMounted(() => {
flowData.value = getFlowData();
});
</script>
<template>
<div class="h-full">
<ElCard header="AntV G6 Next" class="h-full card-wrapper">
<AntvFlow ref="antvFlowRef" :data="flowData" :selected="selectedNode" :behaviors="behaviors" />
<ElDivider />
<ElButton @click="selectedNode = 'N5'">选中节点N5(需要自行处理选中事件不会触发元素点击)</ElButton>
<ElButton v-if="!hasNodeN" @click="addNode">添加节点并与Node5连线</ElButton>
<ElButton v-else @click="() => removeNode('NN')">删除新添加的节点</ElButton>
<ElButton @click="() => removeNode('NX')">删除NodeX</ElButton>
</ElCard>
</div>
</template>

View File

@@ -1,135 +0,0 @@
<script setup lang="tsx">
import { shallowRef, useTemplateRef, watch } from 'vue';
import { useDebounceFn } from '@vueuse/core';
import { vResizeObserver } from '@vueuse/components';
import type { CustomBehaviorOption, Graph } from '@antv/g6';
import { useAntFlow } from './antv-g6-flow';
import { nodeStatus } from './status';
import type { CustomGraphData } from './types';
defineOptions({ name: 'AntvFLow' });
interface Props {
behaviors?: CustomBehaviorOption[];
data: CustomGraphData;
selected?: string;
height?: string;
autoFit?: 'view' | 'center';
}
const props = defineProps<Props>();
const containerRef = useTemplateRef('containerRef');
const graphRef = shallowRef<Graph | null>(null);
// 监听容器尺寸变化,调整画布大小为图容器大小
const onContainerResize = useDebounceFn(() => {
if (graphRef.value) {
graphRef.value.resize();
}
}, 5);
async function draw() {
if (graphRef.value) {
graphRef.value.destroy();
}
const { graph } = useAntFlow({
container: 'antv-flow',
data: props.data,
behaviors: props.behaviors,
autoFit: props.autoFit
});
graphRef.value = graph;
await selectNode();
}
async function selectNode() {
if (props.selected && graphRef.value) {
try {
await graphRef.value.setElementState(props.selected, 'selected');
} catch {}
}
}
function zoomOut() {
graphRef.value?.zoomBy(0.9);
}
function zoomIn() {
graphRef.value?.zoomBy(1.1);
}
function resetZoom() {
graphRef.value?.zoomTo(1);
graphRef.value?.fitCenter();
}
function fitZoom() {
graphRef.value?.fitView();
graphRef.value?.fitCenter();
}
watch(
[() => props.data, () => props.selected],
() => {
draw();
},
{ deep: true }
);
defineExpose({ selectNode, graph: graphRef });
</script>
<template>
<div class="relative">
<!-- canvas toolbar -->
<div class="absolute left-0 right-0 z-1 flex items-center items-stretch justify-between">
<ElButtonGroup size="small" class="bg-white!">
<ElButton @click="zoomOut">
<icon-mingcute:zoom-out-line />
</ElButton>
<ElButton @click="zoomIn">
<icon-mingcute:zoom-in-line />
</ElButton>
<ElButton @click="resetZoom">
<icon-icon-park-outline:equal-ratio />
</ElButton>
<ElButton @click="fitZoom">
<icon-gg:ratio />
</ElButton>
</ElButtonGroup>
<div class="flex-center gap-12px">
<ElPopover placement="bottom-end" :width="200" :animated="false">
<template #reference>
<ElButton size="small" class="bg-white!">
<icon-fe:question />
</ElButton>
</template>
<div class="flex-col gap-8px">
<div span="2" class="text-12px font-bold">节点图例</div>
<ElRow>
<ElCol v-for="(config, status) in nodeStatus" :key="status" :span="12" class="mb-8px flex-center">
<ElTag size="small" round :bordered="false">
<template #default>
<icon-f7:flag-circle-fill v-if="status === 'MILESTONE'" :style="{ color: config.color }" />
<icon-f7:circle-fill v-else :style="{ color: config.color }" />
{{ config.type }}
</template>
</ElTag>
</ElCol>
</ElRow>
</div>
</ElPopover>
</div>
</div>
<!-- canvas container -->
<div
id="antv-flow"
ref="containerRef"
v-resize-observer="onContainerResize"
class="w-full"
:style="{ height: props.height || '300px' }"
@contextmenu="event => event.preventDefault()"
></div>
</div>
</template>

View File

@@ -1,170 +0,0 @@
import { Graph } from '@antv/g6';
import type { CustomBehaviorOption, IPointerEvent } from '@antv/g6';
import type { Canvas } from '@antv/g6/lib/runtime/canvas';
import { useThemeStore } from '@/store/modules/theme';
import { getNodeIcon, nodeStatus } from './status';
import type { CustomEdgeData, CustomGraphData, CustomNodeData } from './types';
interface AntFlowConfig {
container: string | HTMLElement | Canvas;
data: CustomGraphData;
behaviors?: CustomBehaviorOption[];
autoFit?: 'view' | 'center';
}
export function useAntFlow(config: AntFlowConfig) {
const themeStore = useThemeStore();
const baseColor = 'rgb(158 163 171)';
const { container, autoFit = 'center', data, behaviors = [] } = config;
const graph = new Graph({
container,
animation: false,
padding: 16,
theme: 'light',
autoFit,
data,
node: {
type: 'rect',
style: (node: CustomNodeData) => {
const iconS = getNodeIcon(node);
let labelFill = '#000000';
if (node.taskState === 'NOT_STARTED') {
labelFill = '#787878';
}
return {
labelText: node.name as string,
size: [120, 26],
radius: 99,
fill: '#FFFFFF',
stroke: node.isDeleted ? themeStore.otherColor.error : baseColor,
lineDash: node.isDeleted ? 4 : 0,
lineWidth: 1,
labelFill,
labelX: 2,
labelY: 2,
labelTextBaseline: 'middle',
labelTextAlign: 'center',
labelLineHeight: 13,
labelWordWrap: true,
labelMaxWidth: 72,
iconSrc: iconS,
iconWidth: 16,
iconHeight: 16,
iconX: -45,
labelFontSize: 12,
labelPlacement: 'center',
badgeLineWidth: 6,
badgeFontSize: 8,
badges: [
{ text: '延期', placement: 'top', offsetY: -11, visibility: node.isDelayed ? 'visible' : 'hidden' },
{ text: '已删除', placement: 'bottom', offsetY: 11, visibility: node.isDeleted ? 'visible' : 'hidden' }
],
badgePalette: [themeStore.otherColor.error, themeStore.otherColor.error],
ports: [{ placement: 'left' }, { placement: 'right' }]
};
},
state: {
selected: {
lineWidth: 2,
stroke: themeStore.themeColor,
labelFill: themeStore.themeColor,
halo: true,
haloStroke: themeStore.themeColor,
haloLineWidth: 6
},
active: (node: CustomNodeData) => ({
halo: true,
haloStroke: node.isDeleted ? themeStore.otherColor.error : themeStore.themeColor,
haloLineWidth: 6,
zIndex: 2
})
}
},
edge: {
type: 'cubic-horizontal',
style: (node: CustomEdgeData) => ({
curveOffset: 10,
curvePosition: 0.5,
stroke: node.isDeleted ? themeStore.otherColor.error : baseColor,
lineDash: node.isDeleted ? 4 : 0
}),
state: {
active: (node: CustomEdgeData) => ({
lineWidth: 2,
stroke: node.isDeleted ? themeStore.otherColor.error : themeStore.themeColor,
halo: true,
haloStroke: node.isDeleted ? themeStore.otherColor.error : themeStore.themeColor,
haloLineWidth: 6,
zIndex: 2
})
}
},
layout: {
type: 'antv-dagre',
rankdir: 'LR',
ranksep: 20,
nodesep: -20,
controlPoints: true
},
behaviors: [
{
key: 'hover-activate',
type: 'hover-activate',
degree: 1,
direction: 'both'
},
'drag-canvas',
...behaviors
],
plugins: [
{
type: 'tooltip',
enable: (event: IPointerEvent) => event.targetType === 'node',
getContent: (_event: IPointerEvent, items?: CustomNodeData[]) => {
let result = '<div style="display: flex; flex-direction: column; gap: 8px;">';
// 弹出提示可以自定义各种内容但是这里很奇怪有的class不跟随unocss的样式
items?.forEach(item => {
result += `
<h3 style="display: flex; align-items: center; gap: 8px;">${item.name}</h3>
<div style="display: flex;">
<b>状态:</b>
<div style="display: flex; gap: 4px;">
<img src="${getNodeIcon(item)}" />
<span style="font-weight: 400 !important;">${nodeStatus[item.status as keyof typeof nodeStatus].type}</span>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 32px; row-gap: 4px;">
<div style="display: flex; flex-direction: column;"><div style="color: rgb(156 163 175);">预计开始</div>
<div style="font-weight: 700;">${item.startDate || '-'}</div>
</div>
<div style="display: flex; flex-direction: column;">
<div style="color: rgb(156 163 175);">预计结束</div>
<div style="font-weight: 700;">${item.endDate || '-'}</div>
</div>
<div style="display: flex; flex-direction: column;">
<div style="color: rgb(156 163 175);">实际开始</div>
<div style="font-weight: 700;">${item.actualStartDate || '-'}</div>
</div>
<div style="display: flex; flex-direction: column;">
<div style="color: rgb(156 163 175);">实际结束</div>
<div style="font-weight: 700;">${item.actualEndDate || '-'}</div>
</div>
`;
});
result += '</div>';
return result;
}
}
]
});
graph.render();
return { graph };
}

View File

@@ -1,95 +0,0 @@
import { h } from 'vue';
import { ElTag } from 'element-plus';
import type { TagProps } from 'element-plus';
import type { CustomNodeData, NodeStatus } from './types';
interface NodeStatusConfig {
type: string;
color: string;
textColor: string;
base64: string;
flag64: string;
}
export const nodeStatus: Record<NodeStatus, NodeStatusConfig> = {
MILESTONE: {
type: '里程碑',
color: '#5b5b5b',
textColor: '',
base64: '',
flag64: ''
},
NOT_STARTED: {
type: '未开始',
color: '#CCCDD0',
textColor: '#5b5b5b',
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNDQ0NERDAiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNDQ0NERDAiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
},
DELAYED: {
type: '已延期',
color: '#B81111',
textColor: '#dccbcb',
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNCODExMTEiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNCODExMTEiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
},
PAUSED: {
type: '已暂停',
color: '#0E42D2',
textColor: '#dae0f0',
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiMwRTQyRDIiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiMwRTQyRDIiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
},
IN_PROGRESS: {
type: '进行中',
color: '#E1BE0D',
textColor: '#4f4304',
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNFMUJFMEQiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNFMUJFMEQiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
},
COMPLETED: {
type: '已完成',
color: '#33C73D',
textColor: '#084e0c',
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiMzM0M3M0QiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiMzM0M3M0QiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
},
COMPLETED_EARLY: {
type: '提前完成',
color: '#CCFF99',
textColor: '#42681d',
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNDQ0ZGOTkiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNDQ0ZGOTkiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
},
COMPLETED_LATE: {
type: '延期完成',
color: '#CC6699',
textColor: '#4b092a',
base64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNDQzY2OTkiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjgyOCAyMy45MDYtMjMuOTA2YzAtMTMuMDU1LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45NDUgNC4wOTUgMjhjMCAxMy4wNzggMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2Ii8+PC9zdmc+`,
flag64: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTYgNTYiPjxwYXRoIGZpbGw9IiNDQzY2OTkiIGQ9Ik0yOCA1MS45MDZjMTMuMDU1IDAgMjMuOTA2LTEwLjg1MSAyMy45MDYtMjMuOTA2YzAtMTMuMDc4LTEwLjg3NS0yMy45MDYtMjMuOTMtMjMuOTA2QzE0Ljg5OSA0LjA5NCA0LjA5NSAxNC45MjIgNC4wOTUgMjhjMCAxMy4wNTUgMTAuODI4IDIzLjkwNiAyMy45MDYgMjMuOTA2bS05LjA3LTExLjExYy0uNTg2IDAtMS4xMDItLjUxNS0xLjEwMi0xLjA3N1YxOS44MmMwLTEuMDA4LjQ5Mi0xLjc1OCAxLjQ1My0yLjE4Yy44NDQtLjM3NCAxLjU3LS41ODUgMy4zNzUtLjU4NWM0LjI0MiAwIDYuODkgMi4wODYgMTAuODc1IDIuMDg2YzEuOTIyIDAgMi45My0uNDkzIDMuNTQtLjQ5M2MuNzk2IDAgMS40MDYuNDkzIDEuNDA2IDEuMTcydjExLjU1NWMwIDEuMDU1LS40NDYgMS43MzQtMS40NTQgMi4xOGMtLjg2Ny4zOTgtMS42MTcuNjA5LTMuMzc1LjYwOWMtNC4wNzggMC02LjY4LTIuMDYyLTEwLjg3NS0yLjA2MmMtMS40MyAwLTIuMzY3LjI4LTIuNzg5LjQ2OHY3LjE0OWMwIC41ODYtLjQ0NSAxLjA3OC0xLjA1NCAxLjA3OCIvPjwvc3ZnPg==`
}
};
export function getNodeIcon(node: CustomNodeData) {
if (!node.status) return '';
const type = node.milestone ? 'flag64' : 'base64';
return nodeStatus[node.status][type];
}
export function getNodeStatusTag(state: NodeStatus, tagProperty?: TagProps) {
const { color, type } = nodeStatus[state] || {};
return h(
ElTag,
{
color,
size: 'small',
...tagProperty
},
{
default: () => type
}
);
}

View File

@@ -1,28 +0,0 @@
import type { EdgeData, GraphData, NodeData } from '@antv/g6';
export type NodeStatus =
| 'MILESTONE'
| 'NOT_STARTED'
| 'DELAYED'
| 'PAUSED'
| 'IN_PROGRESS'
| 'COMPLETED'
| 'COMPLETED_EARLY'
| 'COMPLETED_LATE';
export interface CustomNodeData extends NodeData {
isDelayed?: boolean;
isDeleted?: boolean;
milestone?: boolean;
status?: NodeStatus;
}
export interface CustomEdgeData extends EdgeData {
isDelayed?: boolean;
isDeleted?: boolean;
}
export interface CustomGraphData extends GraphData {
nodes: CustomNodeData[];
edges: CustomEdgeData[];
}

View File

@@ -1,706 +0,0 @@
import { graphic } from 'echarts';
import type { ScatterSeriesOption } from 'echarts/charts';
import type { SingleAxisComponentOption, TitleComponentOption } from 'echarts/components';
import type { ECOption } from '@/hooks/common/echarts';
export const pieOptions: ECOption = {
legend: {},
toolbox: {
show: true,
feature: {
mark: { show: true },
dataView: { show: true, readOnly: false },
restore: { show: true },
saveAsImage: { show: true }
}
},
series: [
{
name: 'Nightingale Chart',
type: 'pie',
radius: [50, 150],
center: ['50%', '50%'],
roseType: 'area',
itemStyle: {
borderRadius: 8
},
data: [
{ value: 40, name: 'rose 1' },
{ value: 38, name: 'rose 2' },
{ value: 32, name: 'rose 3' },
{ value: 30, name: 'rose 4' },
{ value: 28, name: 'rose 5' },
{ value: 26, name: 'rose 6' },
{ value: 22, name: 'rose 7' },
{ value: 18, name: 'rose 8' }
]
}
]
};
export const lineOptions: ECOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
title: {
text: 'Stacked Line'
},
legend: {
data: ['Email', 'Union Ads', 'Video Ads', 'Direct', 'Search Engine']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
toolbox: {
feature: {
saveAsImage: {}
}
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
color: '#37a2da',
name: 'Email',
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#37a2da'
},
{
offset: 1,
color: '#fff'
}
]
}
},
emphasis: {
focus: 'series'
},
data: [120, 132, 101, 134, 90, 230, 210]
},
{
color: '#9fe6b8',
name: 'Union Ads',
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#9fe6b8'
},
{
offset: 1,
color: '#fff'
}
]
}
},
emphasis: {
focus: 'series'
},
data: [220, 182, 191, 234, 290, 330, 310]
},
{
color: '#fedb5c',
name: 'Video Ads',
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#fedb5c'
},
{
offset: 1,
color: '#fff'
}
]
}
},
emphasis: {
focus: 'series'
},
data: [150, 232, 201, 154, 190, 330, 410]
},
{
color: '#fb7293',
name: 'Direct',
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#fb7293'
},
{
offset: 1,
color: '#fff'
}
]
}
},
emphasis: {
focus: 'series'
},
data: [320, 332, 301, 334, 390, 330, 320]
},
{
color: '#e7bcf3',
name: 'Search Engine',
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#e7bcf3'
},
{
offset: 1,
color: '#fff'
}
]
}
},
emphasis: {
focus: 'series'
},
data: [820, 932, 901, 934, 1290, 1330, 1320]
}
]
};
export const barOptions: ECOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: [120, 200, 150, 80, 70, 110, 130],
type: 'bar',
color: '#8378ea',
showBackground: true,
barGap: 100,
itemStyle: {
borderRadius: [40, 40, 0, 0]
},
backgroundStyle: {
color: 'rgba(180, 180, 180, 0.2)'
}
}
]
};
export function getPictorialBarOption(): ECOption {
const category: string[] = [];
let dottedBase = Number(new Date());
const lineData: number[] = [];
const barData: number[] = [];
for (let i = 0; i < 20; i += 1) {
const date = new Date((dottedBase += 3600 * 24 * 1000));
category.push([date.getFullYear(), date.getMonth() + 1, date.getDate()].join('-'));
const b = Math.random() * 200;
const d = Math.random() * 200;
barData.push(b);
lineData.push(d + b);
}
const options: ECOption = {
backgroundColor: '#0f375f',
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: ['line', 'bar'],
textStyle: {
color: '#ccc'
}
},
xAxis: {
data: category,
axisLine: {
lineStyle: {
color: '#ccc'
}
}
},
yAxis: {
splitLine: { show: false },
axisLine: {
lineStyle: {
color: '#ccc'
}
}
},
series: [
{
name: 'line',
type: 'line',
smooth: true,
showAllSymbol: true,
symbol: 'emptyCircle',
symbolSize: 15,
data: lineData
},
{
name: 'bar',
type: 'bar',
barWidth: 10,
itemStyle: {
borderRadius: 5,
color: new graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#14c8d4' },
{ offset: 1, color: '#43eec6' }
])
},
data: barData
},
{
name: 'line',
type: 'bar',
barGap: '-100%',
barWidth: 10,
itemStyle: {
color: new graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(20,200,212,0.5)' },
{ offset: 0.2, color: 'rgba(20,200,212,0.2)' },
{ offset: 1, color: 'rgba(20,200,212,0)' }
])
},
z: -12,
data: lineData
},
{
name: 'dotted',
type: 'pictorialBar',
symbol: 'rect',
itemStyle: {
color: '#0f375f'
},
symbolRepeat: true,
symbolSize: [12, 4],
symbolMargin: 1,
z: -10,
data: lineData
}
]
};
return options;
}
export function getScatterOption() {
// prettier-ignore
const hours = ['12a', '1a', '2a', '3a', '4a', '5a', '6a', '7a', '8a', '9a','10a','11a', '12p', '1p', '2p', '3p', '4p', '5p', '6p', '7p', '8p', '9p', '10p', '11p'];
// prettier-ignore
const days = ['Saturday', 'Friday', 'Thursday', 'Wednesday', 'Tuesday', 'Monday', 'Sunday'];
// prettier-ignore
const data: [number, number, number][] = [[0,0,5],[0,1,1],[0,2,0],[0,3,0],[0,4,0],[0,5,0],[0,6,0],[0,7,0],[0,8,0],[0,9,0],[0,10,0],[0,11,2],[0,12,4],[0,13,1],[0,14,1],[0,15,3],[0,16,4],[0,17,6],[0,18,4],[0,19,4],[0,20,3],[0,21,3],[0,22,2],[0,23,5],[1,0,7],[1,1,0],[1,2,0],[1,3,0],[1,4,0],[1,5,0],[1,6,0],[1,7,0],[1,8,0],[1,9,0],[1,10,5],[1,11,2],[1,12,2],[1,13,6],[1,14,9],[1,15,11],[1,16,6],[1,17,7],[1,18,8],[1,19,12],[1,20,5],[1,21,5],[1,22,7],[1,23,2],[2,0,1],[2,1,1],[2,2,0],[2,3,0],[2,4,0],[2,5,0],[2,6,0],[2,7,0],[2,8,0],[2,9,0],[2,10,3],[2,11,2],[2,12,1],[2,13,9],[2,14,8],[2,15,10],[2,16,6],[2,17,5],[2,18,5],[2,19,5],[2,20,7],[2,21,4],[2,22,2],[2,23,4],[3,0,7],[3,1,3],[3,2,0],[3,3,0],[3,4,0],[3,5,0],[3,6,0],[3,7,0],[3,8,1],[3,9,0],[3,10,5],[3,11,4],[3,12,7],[3,13,14],[3,14,13],[3,15,12],[3,16,9],[3,17,5],[3,18,5],[3,19,10],[3,20,6],[3,21,4],[3,22,4],[3,23,1],[4,0,1],[4,1,3],[4,2,0],[4,3,0],[4,4,0],[4,5,1],[4,6,0],[4,7,0],[4,8,0],[4,9,2],[4,10,4],[4,11,4],[4,12,2],[4,13,4],[4,14,4],[4,15,14],[4,16,12],[4,17,1],[4,18,8],[4,19,5],[4,20,3],[4,21,7],[4,22,3],[4,23,0],[5,0,2],[5,1,1],[5,2,0],[5,3,3],[5,4,0],[5,5,0],[5,6,0],[5,7,0],[5,8,2],[5,9,0],[5,10,4],[5,11,1],[5,12,5],[5,13,10],[5,14,5],[5,15,7],[5,16,11],[5,17,6],[5,18,0],[5,19,5],[5,20,3],[5,21,4],[5,22,2],[5,23,0],[6,0,1],[6,1,0],[6,2,0],[6,3,0],[6,4,0],[6,5,0],[6,6,0],[6,7,0],[6,8,0],[6,9,0],[6,10,1],[6,11,0],[6,12,2],[6,13,1],[6,14,3],[6,15,4],[6,16,0],[6,17,0],[6,18,0],[6,19,0],[6,20,1],[6,21,2],[6,22,2],[6,23,6]];
const title: TitleComponentOption[] = [];
const singleAxis: SingleAxisComponentOption[] = [];
const series: ScatterSeriesOption[] = [];
days.forEach((day, idx) => {
title.push({
textBaseline: 'middle',
top: `${((idx + 0.5) * 100) / 7}%`,
text: day
});
singleAxis.push({
left: 150,
type: 'category',
boundaryGap: false,
data: hours,
top: `${(idx * 100) / 7 + 5}%`,
height: `${100 / 7 - 10}%`,
axisLabel: {
interval: 2
}
});
series.push({
singleAxisIndex: idx,
coordinateSystem: 'singleAxis',
type: 'scatter',
data: [],
symbolSize(dataItem) {
return dataItem[1] * 4;
}
});
});
data.forEach(dataItem => {
(series as any)[dataItem[0]].data.push([dataItem[1], dataItem[2]]);
});
const option: ECOption = {
tooltip: {
position: 'top'
},
title,
singleAxis,
series: series as any
};
return option;
}
export const radarOptions: ECOption = {
title: {
text: 'Multiple Radar'
},
tooltip: {
trigger: 'axis'
},
legend: {
left: 'center',
data: ['A Software', 'A Phone', 'Another Phone', 'Precipitation', 'Evaporation']
},
radar: [
{
indicator: [
{ name: 'Brand', max: 100 },
{ name: 'Content', max: 100 },
{ name: 'Usability', max: 100 },
{ name: 'Function', max: 100 }
],
center: ['25%', '40%'],
radius: 80
},
{
indicator: [
{ name: 'Look', max: 100 },
{ name: 'Photo', max: 100 },
{ name: 'System', max: 100 },
{ name: 'Performance', max: 100 },
{ name: 'Screen', max: 100 }
],
radius: 80,
center: ['50%', '60%']
},
{
indicator: (() => {
const res = [];
for (let i = 1; i <= 12; i += 1) {
res.push({ name: `${i}`, max: 100 });
}
return res;
})(),
center: ['75%', '40%'],
radius: 80
}
],
series: [
{
type: 'radar',
tooltip: {
trigger: 'item'
},
areaStyle: {},
data: [
{
value: [60, 73, 85, 40],
name: 'A Software'
}
]
},
{
type: 'radar',
radarIndex: 1,
areaStyle: {},
data: [
{
value: [85, 90, 90, 95, 95],
name: 'A Phone'
},
{
value: [95, 80, 95, 90, 93],
name: 'Another Phone'
}
]
},
{
type: 'radar',
radarIndex: 2,
areaStyle: {},
data: [
{
name: 'Precipitation',
value: [2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 75.6, 82.2, 48.7, 18.8, 6.0, 2.3]
},
{
name: 'Evaporation',
value: [2.0, 4.9, 7.0, 23.2, 25.6, 76.7, 35.6, 62.2, 32.6, 20.0, 6.4, 3.3]
}
]
}
]
};
export const gaugeOptions: ECOption = {
series: [
{
name: 'hour',
type: 'gauge',
startAngle: 90,
endAngle: -270,
min: 0,
max: 12,
splitNumber: 12,
clockwise: true,
axisLine: {
lineStyle: {
width: 15,
color: [[1, 'rgba(0,0,0,0.7)']],
shadowColor: 'rgba(0, 0, 0, 0.5)',
shadowBlur: 15
}
},
splitLine: {
lineStyle: {
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 3,
shadowOffsetX: 1,
shadowOffsetY: 2
}
},
axisLabel: {
fontSize: 50,
distance: 25,
formatter(value) {
if (value === 0) {
return '';
}
return `${value}`;
}
},
anchor: {
show: true,
icon: 'path://M532.8,70.8C532.8,70.8,532.8,70.8,532.8,70.8L532.8,70.8C532.7,70.8,532.8,70.8,532.8,70.8z M456.1,49.6c-2.2-6.2-8.1-10.6-15-10.6h-37.5v10.6h37.5l0,0c2.9,0,5.3,2.4,5.3,5.3c0,2.9-2.4,5.3-5.3,5.3v0h-22.5c-1.5,0.1-3,0.4-4.3,0.9c-4.5,1.6-8.1,5.2-9.7,9.8c-0.6,1.7-0.9,3.4-0.9,5.3v16h10.6v-16l0,0l0,0c0-2.7,2.1-5,4.7-5.3h10.3l10.4,21.2h11.8l-10.4-21.2h0c6.9,0,12.8-4.4,15-10.6c0.6-1.7,0.9-3.5,0.9-5.3C457,53,456.7,51.2,456.1,49.6z M388.9,92.1h11.3L381,39h-3.6h-11.3L346.8,92v0h11.3l3.9-10.7h7.3h7.7l3.9-10.6h-7.7h-7.3l7.7-21.2v0L388.9,92.1z M301,38.9h-10.6v53.1H301V70.8h28.4l3.7-10.6H301V38.9zM333.2,38.9v10.6v10.7v31.9h10.6V38.9H333.2z M249.5,81.4L249.5,81.4L249.5,81.4c-2.9,0-5.3-2.4-5.3-5.3h0V54.9h0l0,0c0-2.9,2.4-5.3,5.3-5.3l0,0l0,0h33.6l3.9-10.6h-37.5c-1.9,0-3.6,0.3-5.3,0.9c-4.5,1.6-8.1,5.2-9.7,9.7c-0.6,1.7-0.9,3.5-0.9,5.3l0,0v21.3c0,1.9,0.3,3.6,0.9,5.3c1.6,4.5,5.2,8.1,9.7,9.7c1.7,0.6,3.5,0.9,5.3,0.9h33.6l3.9-10.6H249.5z M176.8,38.9v10.6h49.6l3.9-10.6H176.8z M192.7,81.4L192.7,81.4L192.7,81.4c-2.9,0-5.3-2.4-5.3-5.3l0,0v-5.3h38.9l3.9-10.6h-53.4v10.6v5.3l0,0c0,1.9,0.3,3.6,0.9,5.3c1.6,4.5,5.2,8.1,9.7,9.7c1.7,0.6,3.4,0.9,5.3,0.9h23.4h10.2l3.9-10.6l0,0H192.7z M460.1,38.9v10.6h21.4v42.5h10.6V49.6h17.5l3.8-10.6H460.1z M541.6,68.2c-0.2,0.1-0.4,0.3-0.7,0.4C541.1,68.4,541.4,68.3,541.6,68.2L541.6,68.2z M554.3,60.2h-21.6v0l0,0c-2.9,0-5.3-2.4-5.3-5.3c0-2.9,2.4-5.3,5.3-5.3l0,0l0,0h33.6l3.8-10.6h-37.5l0,0c-6.9,0-12.8,4.4-15,10.6c-0.6,1.7-0.9,3.5-0.9,5.3c0,1.9,0.3,3.7,0.9,5.3c2.2,6.2,8.1,10.6,15,10.6h21.6l0,0c2.9,0,5.3,2.4,5.3,5.3c0,2.9-2.4,5.3-5.3,5.3l0,0h-37.5v10.6h37.5c6.9,0,12.8-4.4,15-10.6c0.6-1.7,0.9-3.5,0.9-5.3c0-1.9-0.3-3.7-0.9-5.3C567.2,64.6,561.3,60.2,554.3,60.2z',
showAbove: false,
offsetCenter: [0, '-35%'],
size: 120,
keepAspect: true,
itemStyle: {
color: '#707177'
}
},
pointer: {
icon: 'path://M2.9,0.7L2.9,0.7c1.4,0,2.6,1.2,2.6,2.6v115c0,1.4-1.2,2.6-2.6,2.6l0,0c-1.4,0-2.6-1.2-2.6-2.6V3.3C0.3,1.9,1.4,0.7,2.9,0.7z',
width: 12,
length: '55%',
offsetCenter: [0, '8%'],
itemStyle: {
color: '#C0911F',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 8,
shadowOffsetX: 2,
shadowOffsetY: 4
}
},
detail: {
show: false
},
title: {
offsetCenter: [0, '30%']
},
data: [
{
value: 0
}
]
},
{
name: 'minute',
type: 'gauge',
startAngle: 90,
endAngle: -270,
min: 0,
max: 60,
clockwise: true,
axisLine: {
show: false
},
splitLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
show: false
},
pointer: {
icon: 'path://M2.9,0.7L2.9,0.7c1.4,0,2.6,1.2,2.6,2.6v115c0,1.4-1.2,2.6-2.6,2.6l0,0c-1.4,0-2.6-1.2-2.6-2.6V3.3C0.3,1.9,1.4,0.7,2.9,0.7z',
width: 8,
length: '70%',
offsetCenter: [0, '8%'],
itemStyle: {
color: '#C0911F',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 8,
shadowOffsetX: 2,
shadowOffsetY: 4
}
},
anchor: {
show: true,
size: 20,
showAbove: false,
itemStyle: {
borderWidth: 15,
borderColor: '#C0911F',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 8,
shadowOffsetX: 2,
shadowOffsetY: 4
}
},
detail: {
show: false
},
title: {
offsetCenter: ['0%', '-40%']
},
data: [
{
value: 0
}
]
},
{
name: 'second',
type: 'gauge',
startAngle: 90,
endAngle: -270,
min: 0,
max: 60,
animationEasingUpdate: 'bounceOut',
clockwise: true,
axisLine: {
show: false
},
splitLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
show: false
},
pointer: {
icon: 'path://M2.9,0.7L2.9,0.7c1.4,0,2.6,1.2,2.6,2.6v115c0,1.4-1.2,2.6-2.6,2.6l0,0c-1.4,0-2.6-1.2-2.6-2.6V3.3C0.3,1.9,1.4,0.7,2.9,0.7z',
width: 4,
length: '85%',
offsetCenter: [0, '8%'],
itemStyle: {
color: '#C0911F',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 8,
shadowOffsetX: 2,
shadowOffsetY: 4
}
},
anchor: {
show: true,
size: 15,
showAbove: true,
itemStyle: {
color: '#C0911F',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 8,
shadowOffsetX: 2,
shadowOffsetY: 4
}
},
detail: {
show: false
},
title: {
offsetCenter: ['0%', '-40%']
},
data: [
{
value: 0
}
]
}
]
};

View File

@@ -1,93 +0,0 @@
<script setup lang="ts">
import { onUnmounted } from 'vue';
import { useEcharts } from '@/hooks/common/echarts';
import {
barOptions,
gaugeOptions,
getPictorialBarOption,
getScatterOption,
lineOptions,
pieOptions,
radarOptions
} from './data';
defineOptions({ name: 'EchartsDemo' });
const { domRef: pieRef } = useEcharts(() => pieOptions, { onRender() {} });
const { domRef: lineRef } = useEcharts(() => lineOptions, { onRender() {} });
const { domRef: barRef } = useEcharts(() => barOptions, { onRender() {} });
const { domRef: pictorialBarRef } = useEcharts(() => getPictorialBarOption(), { onRender() {} });
const { domRef: radarRef } = useEcharts(() => radarOptions, { onRender() {} });
const { domRef: scatterRef } = useEcharts(() => getScatterOption(), { onRender() {} });
const { domRef: gaugeRef, setOptions: setGaugeOptions } = useEcharts(() => gaugeOptions, { onRender() {} });
let intervalId: NodeJS.Timeout;
function initGaugeChart() {
intervalId = setInterval(() => {
const date = new Date();
const second = date.getSeconds();
const minute = date.getMinutes() + second / 60;
const hour = (date.getHours() % 12) + minute / 60;
setGaugeOptions({
animationDurationUpdate: 300,
series: [
{
name: 'hour',
animation: hour !== 0,
data: [{ value: hour }]
},
{
name: 'minute',
animation: minute !== 0,
data: [{ value: minute }]
},
{
animation: second !== 0,
name: 'second',
data: [{ value: second }]
}
]
});
}, 1000);
}
function clearGaugeChart() {
clearInterval(intervalId);
}
initGaugeChart();
onUnmounted(() => {
clearGaugeChart();
});
</script>
<template>
<ElSpace fill :size="16">
<ElCard class="card-wrapper">
<div ref="pieRef" class="h-400px" />
</ElCard>
<ElCard class="card-wrapper">
<div ref="lineRef" class="h-400px" />
</ElCard>
<ElCard class="card-wrapper">
<div ref="barRef" class="h-400px" />
</ElCard>
<ElCard class="card-wrapper">
<div ref="radarRef" class="h-400px"></div>
</ElCard>
<ElCard class="card-wrapper">
<div ref="scatterRef" class="h-600px"></div>
</ElCard>
<ElCard class="card-wrapper">
<div ref="pictorialBarRef" class="h-600px" />
</ElCard>
<ElCard class="card-wrapper">
<div ref="gaugeRef" class="h-640px" />
</ElCard>
</ElSpace>
</template>
<style scoped></style>

View File

@@ -1,872 +0,0 @@
import type {
IAnimationConfig,
IAreaChartSpec,
IBarChartSpec,
ICircularProgressChartSpec,
IHistogramChartSpec,
IIndicatorSpec,
ILiquidChartSpec,
IWordCloudChartSpec
} from '@visactor/vchart';
export const shapeWordCloudSpec: IWordCloudChartSpec = {
type: 'wordCloud',
maskShape: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/log.jpeg',
nameField: 'challenge_name',
valueField: 'sum_count',
seriesField: 'challenge_name',
data: [
{
name: 'data',
values: [
{
challenge_name: '刘浩存',
sum_count: 957
},
{
challenge_name: '刘昊然',
sum_count: 942
},
{
challenge_name: '喜欢',
sum_count: 842
},
{
challenge_name: '真的',
sum_count: 828
},
{
challenge_name: '四海',
sum_count: 665
},
{
challenge_name: '好看',
sum_count: 627
},
{
challenge_name: '评论',
sum_count: 574
},
{
challenge_name: '好像',
sum_count: 564
},
{
challenge_name: '沈腾',
sum_count: 554
},
{
challenge_name: '不像',
sum_count: 540
},
{
challenge_name: '多少钱',
sum_count: 513
},
{
challenge_name: '韩寒',
sum_count: 513
},
{
challenge_name: '不知道',
sum_count: 499
},
{
challenge_name: '感觉',
sum_count: 499
},
{
challenge_name: '尹正',
sum_count: 495
},
{
challenge_name: '不看',
sum_count: 487
},
{
challenge_name: '奥特之父',
sum_count: 484
},
{
challenge_name: '阿姨',
sum_count: 482
},
{
challenge_name: '支持',
sum_count: 482
},
{
challenge_name: '父母',
sum_count: 479
},
{
challenge_name: '一条',
sum_count: 462
},
{
challenge_name: '女主',
sum_count: 456
},
{
challenge_name: '确实',
sum_count: 456
},
{
challenge_name: '票房',
sum_count: 456
},
{
challenge_name: '无语',
sum_count: 443
},
{
challenge_name: '干干净净',
sum_count: 443
},
{
challenge_name: '为啥',
sum_count: 426
},
{
challenge_name: '爱情',
sum_count: 425
},
{
challenge_name: '喜剧',
sum_count: 422
},
{
challenge_name: '春节',
sum_count: 414
},
{
challenge_name: '剧情',
sum_count: 414
},
{
challenge_name: '人生',
sum_count: 409
},
{
challenge_name: '风格',
sum_count: 408
},
{
challenge_name: '演员',
sum_count: 403
},
{
challenge_name: '成长',
sum_count: 403
},
{
challenge_name: '玩意',
sum_count: 402
},
{
challenge_name: '文学',
sum_count: 397
}
]
}
]
};
export const circularProgressTickSpec: ICircularProgressChartSpec & { indicator: IIndicatorSpec } = {
type: 'circularProgress',
data: [
{
id: 'id0',
values: [
{
type: 'Tradition Industries',
value: 0.795,
text: '79.5%'
},
{
type: 'Business Companies',
value: 0.5,
text: '50%'
},
{
type: 'Customer-facing Companies',
value: 0.25,
text: '25%'
}
]
}
],
color: ['rgb(255, 222, 0)', 'rgb(171, 205, 5)', 'rgb(0, 154, 68)'],
valueField: 'value',
categoryField: 'type',
seriesField: 'type',
radius: 0.8,
innerRadius: 0.4,
tickMask: {
visible: true,
angle: 10,
offsetAngle: 0,
forceAlign: true,
style: {
cornerRadius: 15
}
},
axes: [
{
visible: false,
type: 'linear',
orient: 'angle'
},
{
visible: false,
type: 'band',
orient: 'radius'
}
],
indicator: {
visible: true,
trigger: 'hover',
title: {
visible: true,
field: 'type',
autoLimit: true,
style: {
fontSize: 20,
fill: 'black'
}
},
content: [
{
visible: true,
field: 'text',
style: {
fontSize: 16,
fill: 'gray'
}
}
]
},
legends: {
visible: true,
orient: 'bottom',
title: {
visible: false
}
}
};
export const liquidChartSmartInvertSpec: ILiquidChartSpec & { indicator: IIndicatorSpec } = {
type: 'liquid',
valueField: 'value',
data: {
id: 'data',
values: [
{
value: 0.8
}
]
},
maskShape: 'drop', // 水滴
// maskShape: 'circle',
// maskShape: 'star',
indicatorSmartInvert: true,
indicator: {
visible: true,
title: {
visible: true,
style: {
text: '进度'
}
},
content: [
{
visible: true,
style: {
fill: 'black',
text: '80%'
}
}
]
},
liquidBackground: {
style: {
fill: 'blue'
}
}
};
const goldenMedals: Record<number, any[]> = {
2000: [
{ country: 'USA', value: 37 },
{ country: 'Russia', value: 32 },
{ country: 'China', value: 28 },
{ country: 'Australia', value: 16 },
{ country: 'Germany', value: 13 },
{ country: 'France', value: 13 },
{ country: 'Italy', value: 13 },
{ country: 'Netherlands', value: 12 },
{ country: 'Cuba', value: 11 },
{ country: 'U.K.', value: 11 }
],
2004: [
{ country: 'USA', value: 36 },
{ country: 'China', value: 32 },
{ country: 'Russia', value: 28 },
{ country: 'Australia', value: 17 },
{ country: 'Japan', value: 16 },
{ country: 'Germany', value: 13 },
{ country: 'France', value: 11 },
{ country: 'Italy', value: 10 },
{ country: 'South Korea', value: 9 },
{ country: 'U.K.', value: 9 }
],
2008: [
{ country: 'China', value: 48 },
{ country: 'USA', value: 36 },
{ country: 'Russia', value: 24 },
{ country: 'U.K.', value: 19 },
{ country: 'Germany', value: 16 },
{ country: 'Australia', value: 14 },
{ country: 'South Korea', value: 13 },
{ country: 'Japan', value: 9 },
{ country: 'Italy', value: 8 },
{ country: 'France', value: 7 }
],
2012: [
{ country: 'USA', value: 46 },
{ country: 'China', value: 39 },
{ country: 'U.K.', value: 29 },
{ country: 'Russia', value: 19 },
{ country: 'South Korea', value: 13 },
{ country: 'Germany', value: 11 },
{ country: 'France', value: 11 },
{ country: 'Australia', value: 8 },
{ country: 'Italy', value: 8 },
{ country: 'Hungary', value: 8 }
],
2016: [
{ country: 'USA', value: 46 },
{ country: 'U.K.', value: 27 },
{ country: 'China', value: 26 },
{ country: 'Russia', value: 19 },
{ country: 'Germany', value: 17 },
{ country: 'Japan', value: 12 },
{ country: 'France', value: 10 },
{ country: 'South Korea', value: 9 },
{ country: 'Italy', value: 8 },
{ country: 'Australia', value: 8 }
],
2020: [
{ country: 'USA', value: 39 },
{ country: 'China', value: 38 },
{ country: 'Japan', value: 27 },
{ country: 'U.K.', value: 22 },
{ country: 'Russian Olympic Committee', value: 20 },
{ country: 'Australia', value: 17 },
{ country: 'Netherlands', value: 10 },
{ country: 'France', value: 10 },
{ country: 'Germany', value: 10 },
{ country: 'Italy', value: 10 }
]
};
const colors = {
China: '#d62728',
USA: '#1664FF',
Russia: '#B2CFFF',
'U.K.': '#1AC6FF',
Australia: '#94EFFF',
Japan: '#FF8A00',
Cuba: '#FFCE7A',
Germany: '#3CC780',
France: '#B9EDCD',
Italy: '#7442D4',
'South Korea': '#DDC5FA',
'Russian Olympic Committee': '#B2CFFF',
Netherlands: '#FFC400',
Hungary: '#FAE878'
};
const dataSpecs = Object.keys(goldenMedals).map(year => {
return {
data: [
{
id: 'id',
values: (goldenMedals[year as unknown as number] as any)
.sort((a: any, b: any) => b.value - a.value)
.map((v: any) => {
return { ...v, fill: (colors as any)[v.country] };
})
},
{
id: 'year',
values: [{ year }]
}
]
};
});
const duration = 1000;
const exchangeDuration = 600;
export const rankingBarSpec: IBarChartSpec = {
type: 'bar',
padding: {
top: 12,
right: 100,
bottom: 12
},
data: dataSpecs[0].data,
direction: 'horizontal',
yField: 'country',
xField: 'value',
seriesField: 'country',
bar: {
style: {
fill: (datum: any) => datum.fill
}
},
axes: [
{
animation: true,
orient: 'bottom',
type: 'linear',
visible: true,
max: 50,
grid: {
visible: true
}
},
{
animation: true,
id: 'axis-left',
orient: 'left',
width: 130,
tick: { visible: false },
label: { visible: true },
type: 'band'
}
],
title: {
visible: true,
text: 'Top 10 Olympic Gold Medals by Country Since 2000'
},
animationUpdate: {
bar: [
{
type: 'update',
options: { excludeChannels: ['y'] },
easing: 'linear',
duration
},
{
channel: ['y'],
easing: 'circInOut',
duration: exchangeDuration
}
],
axis: {
duration: exchangeDuration,
easing: 'circInOut'
}
} as Record<string, IAnimationConfig>,
animationEnter: {
bar: [
{
type: 'moveIn',
duration: exchangeDuration,
easing: 'circInOut',
options: {
direction: 'y',
orient: 'negative'
}
}
]
},
animationExit: {
bar: [
{
type: 'fadeOut',
duration: exchangeDuration
}
]
},
customMark: [
{
type: 'text',
dataId: 'year',
style: {
textBaseline: 'bottom',
fontSize: 200,
textAlign: 'right',
fontFamily: 'PingFang SC',
fontWeight: 600,
text: (datum: any) => datum.year,
x: (_datum: any, ctx: any) => {
return ctx.vchart.getChart().getCanvasRect()?.width - 50;
},
y: (_datum: any, ctx: any) => {
return ctx.vchart.getChart().getCanvasRect()?.height - 50;
},
fill: 'grey',
fillOpacity: 0.5
}
}
],
player: {
type: 'continuous',
orient: 'bottom',
auto: true,
loop: true,
dx: 80,
position: 'middle',
interval: duration,
specs: dataSpecs,
slider: {
railStyle: {
height: 6
}
},
controller: {
backward: {
style: {
size: 12
}
},
forward: {
style: {
size: 12
}
},
start: {
order: 1,
position: 'end'
}
}
}
};
export const stackedDashAreaSpec: IAreaChartSpec = {
type: 'area',
data: {
values: [
{ month: 'Jan', country: 'Africa', value: 4229 },
{ month: 'Jan', country: 'EU', value: 4376 },
{ month: 'Jan', country: 'China', value: 3054 },
{ month: 'Jan', country: 'USA', value: 12814 },
{ month: 'Feb', country: 'Africa', value: 3932 },
{ month: 'Feb', country: 'EU', value: 3987 },
{ month: 'Feb', country: 'China', value: 5067 },
{ month: 'Feb', country: 'USA', value: 13012 },
{ month: 'Mar', country: 'Africa', value: 5221 },
{ month: 'Mar', country: 'EU', value: 3574 },
{ month: 'Mar', country: 'China', value: 7004 },
{ month: 'Mar', country: 'USA', value: 11624 },
{ month: 'Apr', country: 'Africa', value: 9256 },
{ month: 'Apr', country: 'EU', value: 4376 },
{ month: 'Apr', country: 'China', value: 9054 },
{ month: 'Apr', country: 'USA', value: 8814 },
{ month: 'May', country: 'Africa', value: 3308 },
{ month: 'May', country: 'EU', value: 4572 },
{ month: 'May', country: 'China', value: 12043 },
{ month: 'May', country: 'USA', value: 12998 },
{ month: 'Jun', country: 'Africa', value: 5432 },
{ month: 'Jun', country: 'EU', value: 3417 },
{ month: 'Jun', country: 'China', value: 15067 },
{ month: 'Jun', country: 'USA', value: 12321 },
{ month: 'Jul', country: 'Africa', value: 13701 },
{ month: 'Jul', country: 'EU', value: 5231 },
{ month: 'Jul', country: 'China', value: 10119 },
{ month: 'Jul', country: 'USA', value: 10342 },
{ month: 'Aug', country: 'Africa', value: 4008, forecast: true },
{ month: 'Aug', country: 'EU', value: 4572, forecast: true },
{ month: 'Aug', country: 'China', value: 12043, forecast: true },
{ month: 'Aug', country: 'USA', value: 22998, forecast: true },
{ month: 'Sept', country: 'Africa', value: 18712, forecast: true },
{ month: 'Sept', country: 'EU', value: 6134, forecast: true },
{ month: 'Sept', country: 'China', value: 10419, forecast: true },
{ month: 'Sept', country: 'USA', value: 11261, forecast: true }
]
},
stack: true,
xField: 'month',
yField: 'value',
seriesField: 'country',
point: {
style: {
size: 0
},
state: {
dimension_hover: {
size: 10,
outerBorder: {
distance: 0,
lineWidth: 6,
strokeOpacity: 0.2
}
}
}
},
line: {
style: {
// Configure the lineDash attribute based on the forecast field value of the data
lineDash: (data: any) => {
if (data.forecast) {
return [5, 5];
}
return [0];
}
}
},
area: {
style: {
fillOpacity: 0.5,
textureColor: '#fff',
textureSize: 14,
// Configure the texture attribute based on the forecast field value of the data
texture: (data: any) => {
if (data.forecast) {
return 'bias-rl';
}
return '';
}
}
},
legends: [{ visible: true, position: 'middle', orient: 'bottom' }],
crosshair: {
xField: {
visible: true,
line: {
type: 'line'
}
}
}
};
export const barMarkPointSpec: IBarChartSpec = {
type: 'bar',
height: 300,
data: [
{
id: 'barData',
values: [
{ time: '10:20', cost: 2 },
{ time: '10:30', cost: 1 },
{ time: '10:40', cost: 1 },
{ time: '10:50', cost: 2 },
{ time: '11:00', cost: 2 },
{ time: '11:10', cost: 2 },
{ time: '11:20', cost: 1 },
{ time: '11:30', cost: 1 },
{ time: '11:40', cost: 2 },
{ time: '11:50', cost: 1 }
]
}
],
xField: 'time',
yField: 'cost',
crosshair: {
xField: {
visible: true,
line: {
type: 'rect',
style: {
fill: 'rgb(85,208,93)',
fillOpacity: 0.1
}
},
bindingAxesIndex: [1],
defaultSelect: {
axisIndex: 1,
datum: '10:20'
}
}
},
label: {
visible: true,
animation: false,
formatMethod: (datum: any) => `${datum}分钟`,
style: {
fill: 'rgb(155,155,155)'
}
},
bar: {
style: {
fill: 'rgb(85,208,93)',
cornerRadius: [4, 4, 0, 0],
width: 30
}
},
markPoint: [
{
coordinate: {
time: '10:20',
cost: 2
},
itemContent: {
type: 'text',
// autoRotate: false,
offsetY: -10,
text: {
dy: 14,
text: '2分钟',
style: {
fill: 'white',
fontSize: 14
},
labelBackground: {
padding: [5, 10, 5, 10],
style: {
fill: '#000',
cornerRadius: 5
}
}
}
},
itemLine: {
endSymbol: {
visible: true,
style: {
angle: Math.PI,
scaleY: 0.4,
fill: '#000',
dy: 4,
stroke: '#000'
}
},
startSymbol: { visible: false },
line: {
style: {
visible: false
}
}
}
}
],
animationUpdate: false,
axes: [
{
orient: 'left',
max: 10,
label: { visible: false },
grid: {
style: { lineDash: [4, 4] }
}
},
{
orient: 'bottom',
label: {
formatMethod: (datum: any) => {
return datum === '10:20' ? '当前' : datum;
},
style: (datum: any) => {
return {
fontSize: datum === '10:20' ? 14 : 12,
fill: datum === '10:20' ? 'black' : 'grey'
};
}
},
paddingOuter: 0.5,
paddingInner: 0,
grid: {
visible: true,
alignWithLabel: false,
style: { lineDash: [4, 4] }
}
}
]
};
export const histogramDifferentBinSpec: IHistogramChartSpec = {
type: 'histogram',
xField: 'from',
x2Field: 'to',
yField: 'profit',
seriesField: 'type',
bar: {
style: {
stroke: 'white',
lineWidth: 1
}
},
title: {
text: 'Profit',
textStyle: {
align: 'center',
height: 50,
lineWidth: 3,
fill: '#333',
fontSize: 25,
fontFamily: 'Times New Roman'
}
},
tooltip: {
visible: true,
mark: {
title: {
key: 'title',
value: 'profit'
},
content: [
{
key: (datum?: Record<string, any>) => `${datum?.from}${datum?.to}`,
value: (datum?: Record<string, any>) => datum?.profit
}
]
}
},
axes: [
{
orient: 'bottom',
nice: false
}
],
data: [
{
name: 'data1',
values: [
{
from: 0,
to: 10,
profit: 2,
type: 'A'
},
{
from: 10,
to: 16,
profit: 3,
type: 'B'
},
{
from: 16,
to: 18,
profit: 15,
type: 'C'
},
{
from: 18,
to: 26,
profit: 12,
type: 'D'
},
{
from: 26,
to: 32,
profit: 22,
type: 'E'
},
{
from: 32,
to: 56,
profit: 7,
type: 'F'
},
{
from: 56,
to: 62,
profit: 17,
type: 'G'
}
]
}
]
};

View File

@@ -1,51 +0,0 @@
<script setup lang="ts">
import { useVChart } from '@/hooks/common/vchart';
import {
barMarkPointSpec,
circularProgressTickSpec,
histogramDifferentBinSpec,
liquidChartSmartInvertSpec,
rankingBarSpec,
shapeWordCloudSpec,
stackedDashAreaSpec
} from './data';
const { domRef: stackedDashAreaRef } = useVChart(() => stackedDashAreaSpec);
const { domRef: barMarkPointRef } = useVChart(() => barMarkPointSpec);
const { domRef: histogramDifferentBinRef } = useVChart(() => histogramDifferentBinSpec);
const { domRef: rankingBarRef } = useVChart(() => rankingBarSpec);
const { domRef: shapeWordCloudRef } = useVChart(() => shapeWordCloudSpec);
const { domRef: circularProgressTickRef } = useVChart(() => circularProgressTickSpec);
const { domRef: liquidChartSmartInvertRef } = useVChart(() => liquidChartSmartInvertSpec);
</script>
<template>
<ElSpace direction="vertical" fill :size="16">
<ElCard header="VChart" class="h-full card-wrapper">
<WebSiteLink label="More Demos: " link="https://www.visactor.com/vchart/example" />
</ElCard>
<ElCard header="Stacked Dash Area Chart" class="h-full card-wrapper">
<div ref="stackedDashAreaRef" class="h-400px" />
</ElCard>
<ElCard header="Bar Mark Point Chart" class="h-full card-wrapper">
<div ref="barMarkPointRef" class="h-400px" />
</ElCard>
<ElCard header="Histogram Different Bin Chart" class="h-full card-wrapper">
<div ref="histogramDifferentBinRef" class="h-400px" />
</ElCard>
<ElCard header="Ranking Bar Chart" class="h-full card-wrapper">
<div ref="rankingBarRef" class="h-400px" />
</ElCard>
<ElCard header="Circular Progress Tick Chart" class="h-full card-wrapper">
<div ref="circularProgressTickRef" class="h-400px" />
</ElCard>
<ElCard header="Liquid Chart Smart Invert Chart" class="h-full card-wrapper">
<div ref="liquidChartSmartInvertRef" class="h-400px" />
</ElCard>
<ElCard header="Shape Word Cloud Chart" class="h-full card-wrapper">
<div ref="shapeWordCloudRef" class="h-400px" />
</ElCard>
</ElSpace>
</template>
<style scoped></style>

View File

@@ -1,37 +0,0 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useClipboard } from '@vueuse/core';
defineOptions({ name: 'CopyPage' });
const { copy, isSupported } = useClipboard();
const source = ref('');
async function handleCopy() {
if (!isSupported) {
window.$message?.error('您的浏览器不支持Clipboard API');
return;
}
if (!source.value) {
window.$message?.error('请输入要复制的内容');
return;
}
await copy(source.value);
window.$message?.success(`复制成功:${source.value}`);
}
</script>
<template>
<div class="h-full">
<ElCard header="文本复制" class="h-full card-wrapper">
<ElInput v-model="source" placeholder="请输入要复制的内容吧">
<template #append>
<ElButton type="primary" @click="handleCopy">复制</ElButton>
</template>
</ElInput>
</ElCard>
</div>
</template>

View File

@@ -1,171 +0,0 @@
<script setup lang="tsx">
import { reactive } from 'vue';
import { ElButton, ElTag } from 'element-plus';
import type { FlatResponseData } from '@sa/axios';
import { utils, writeFile } from 'xlsx';
import { commonStatusRecord, userGenderRecord } from '@/constants/business';
import { fetchGetUserList } from '@/service/api';
import { useUIPaginatedTable } from '@/hooks/common/table';
import { $t } from '@/locales';
defineOptions({ name: 'ExcelPage' });
const searchParams: Api.SystemManage.UserSearchParams = reactive({
pageNo: 1,
pageSize: 10,
status: undefined,
username: undefined,
mobile: undefined,
deptId: undefined,
roleId: undefined
});
const { columns, data, loading } = useUIPaginatedTable<
FlatResponseData<any, Api.SystemManage.UserList>,
Api.SystemManage.User
>({
api: () => fetchGetUserList(searchParams),
transform: response => {
if (!response.error) {
return {
data: response.data.list,
pageNum: searchParams.pageNo ?? 1,
pageSize: searchParams.pageSize ?? 10,
total: response.data.total
};
}
return {
data: [],
pageNum: 1,
pageSize: 10,
total: 0
};
},
onPaginationParamsChange: params => {
searchParams.pageNo = params.currentPage;
searchParams.pageSize = params.pageSize;
},
columns: () => [
{ type: 'selection', width: 48 },
{ type: 'index', label: $t('common.index'), width: 64 },
{ prop: 'username', label: $t('page.system.user.userName'), minWidth: 100 },
{
prop: 'sex',
label: $t('page.system.user.userGender'),
width: 100,
formatter: row => {
const tagMap: Record<Api.SystemManage.UserGender, UI.ThemeColor> = {
0: 'info',
1: 'primary',
2: 'danger'
};
const value = row.sex ?? 0;
const label = $t(userGenderRecord[value]);
return <ElTag type={tagMap[value]}>{label}</ElTag>;
}
},
{ prop: 'nickname', label: $t('page.system.user.nickName'), minWidth: 100 },
{ prop: 'mobile', label: $t('page.system.user.userPhone'), width: 120 },
{ prop: 'email', label: $t('page.system.user.userEmail'), minWidth: 200 },
{
prop: 'status',
label: $t('page.system.user.userStatus'),
width: 100,
formatter: row => {
const tagMap: Record<Api.SystemManage.CommonStatus, UI.ThemeColor> = {
0: 'success',
1: 'warning'
};
const label = $t(commonStatusRecord[row.status]);
return <ElTag type={tagMap[row.status]}>{label}</ElTag>;
}
}
]
});
function exportExcel() {
const exportColumns = columns.value.slice(2);
const excelList = data.value.map(item => exportColumns.map(col => getTableValue(col, item)));
const titleList = exportColumns.map(col => (isTableColumnHasTitle(col) && col.label) || undefined);
excelList.unshift(titleList);
const workBook = utils.book_new();
const workSheet = utils.aoa_to_sheet(excelList);
workSheet['!cols'] = exportColumns.map(item => ({
width: Math.round(Number(item.width) / 10 || 20)
}));
utils.book_append_sheet(workBook, workSheet, '用户列表');
writeFile(workBook, '用户数据.xlsx');
}
function getTableValue(col: UI.TableColumn<Api.SystemManage.User>, item: Api.SystemManage.User) {
if (!isTableColumnHasKey(col)) {
return '';
}
const { prop } = col;
if (prop === 'operate' || prop === undefined) {
return '';
}
if (prop === 'status') {
return $t(commonStatusRecord[item.status]);
}
if (prop === 'sex') {
return $t(userGenderRecord[item.sex ?? 0]);
}
if (prop in item) {
return item[prop as keyof Api.SystemManage.User];
}
return '';
}
function isTableColumnHasKey<T>(column: UI.TableColumn<T>): boolean {
return Boolean((column as UI.TableColumnWithKey<T>).prop);
}
function isTableColumnHasTitle<T>(column: UI.TableColumn<T>): boolean {
return Boolean((column as UI.TableColumnWithKey<T>).label);
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<ElCard class="card-wrapper sm:flex-1-hidden">
<template #header>
<div class="flex items-center justify-between">
<p>Excel导出</p>
<ElButton plain type="primary" @click="exportExcel">
<template #icon>
<icon-file-icons:microsoft-excel class="text-icon" />
</template>
导出excel
</ElButton>
</div>
</template>
<div class="h-[calc(100%-50px)]">
<ElTable v-loading="loading" height="100%" border class="sm:h-full" :data="data" row-key="id">
<ElTableColumn v-for="col in columns" :key="col.prop" v-bind="col" />
</ElTable>
</div>
</ElCard>
</div>
</template>
<style scoped></style>

View File

@@ -1,173 +0,0 @@
import type { Task } from 'dhtmlx-gantt';
export const ganttTasks: Task[] = [
{
id: 11,
text: 'CN-RDMS 架构设计',
type: 'project',
progress: 0,
open: true,
start_date: new Date('2024-01-10 00:00'),
duration: 12,
parent: 0
},
{
id: 12,
text: '测试版本',
start_date: new Date('2024-03-20 00:00'),
type: 'project',
duration: 5,
render: 'split',
parent: '11',
progress: 0,
open: true
},
{
id: 99,
text: '测试版本1 发布',
start_date: new Date('2024-03-20 00:00'),
end_date: new Date('2024-03-25 00:00'),
parent: '12',
progress: 0,
open: true
},
{
id: 98,
text: '测试版本2 发布',
start_date: new Date('2024-03-26 00:00'),
duration: 4,
parent: '12',
progress: 0,
open: true
},
{
id: 97,
text: '测试版本3 发布',
start_date: new Date('2024-03-31 00:00'),
duration: 10,
parent: '12',
progress: 0,
open: true
},
{
id: 13,
text: '1.0 版本',
start_date: new Date('2024-03-31 00:00'),
type: 'project',
render: 'split',
parent: '11',
progress: 0.5,
open: false,
duration: 11
},
{
id: 17,
text: '1.0正式发布',
start_date: new Date('2024-03-31 00:00'),
end_date: new Date('2024-04-03 00:00'),
parent: '13',
progress: 0,
open: true
},
{
id: 18,
text: '1.0.1 版本',
start_date: new Date('2024-04-03 00:00'),
duration: 5,
parent: '13',
progress: 0,
open: true
},
{
id: 19,
text: '1.0.2 版本',
start_date: new Date('2024-04-08 00:00'),
duration: 6,
parent: '13',
progress: 0,
open: true
},
{
id: 20,
text: '1.0.3 版本',
start_date: new Date('2024-04-16 00:00'),
duration: 8,
parent: '13',
progress: 0,
open: true
},
{
id: 31,
text: '1.0.4 版本',
start_date: new Date('2024-04-17 00:00'),
duration: 8,
parent: '13',
progress: 0,
open: true
},
{
id: 32,
text: '1.0.5 版本',
start_date: new Date('2024-04-26 00:00'),
duration: 9,
parent: '13',
progress: 0,
open: true
},
{
id: 33,
text: '1.0.9 版本',
start_date: new Date('2024-05-05 00:00'),
duration: 2,
parent: '13',
progress: 0,
open: true
},
{
id: 14,
text: '1.1 版本',
start_date: new Date('2024-05-07 00:00'),
duration: 30,
parent: '11',
progress: 0,
open: true
},
{
id: 15,
text: '1.2 版本',
start_date: new Date('2024-06-06 00:00'),
duration: 46,
parent: '11',
progress: 0,
open: true
},
{
id: 16,
text: '1.3版本',
type: 'project',
render: 'split',
parent: '11',
progress: 0,
open: true,
start_date: new Date('2024-07-22 00:00'),
duration: 11
},
{
id: 21,
text: '1.3.1版本',
start_date: new Date('2024-07-22 00:00'),
duration: 7,
parent: '16',
progress: 0,
open: true
},
{
id: 22,
text: '1.3.2版本',
start_date: new Date('2024-07-29 00:00'),
duration: 7,
parent: '16',
progress: 0,
open: true
}
];

View File

@@ -1,169 +0,0 @@
<script setup lang="tsx">
import { onMounted, shallowRef } from 'vue';
import { gantt } from 'dhtmlx-gantt';
import type { GanttConfigOptions, ZoomLevel } from 'dhtmlx-gantt';
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css';
import { ganttTasks } from './data';
defineOptions({ name: 'GanttPage' });
const ganttRef = shallowRef<HTMLElement>();
type TimeType = 'day' | 'week' | 'month' | 'quarter' | 'year';
const timeType = shallowRef<TimeType>('quarter');
interface TimeData {
label: string;
value: TimeType;
}
const data: TimeData[] = [
{ label: '天', value: 'day' },
{ label: '周', value: 'week' },
{ label: '月', value: 'month' },
{ label: '季', value: 'quarter' },
{ label: '年', value: 'year' }
];
function initGantt() {
if (!ganttRef.value) return;
const config: Partial<GanttConfigOptions> = {
grid_width: 350,
add_column: false,
autofit: false,
row_height: 60,
bar_height: 34,
auto_types: true,
xml_date: '%Y-%m-%d',
columns: [
{ name: 'text', label: '项目名称', tree: true, width: '*' },
{ name: 'start_date', label: '开始时间', align: 'center', width: 150 }
]
};
Object.assign(gantt.config, config);
gantt.i18n.setLocale('cn');
gantt.init(ganttRef.value);
gantt.parse({ data: ganttTasks });
const zoomLevels: ZoomLevel[] = [
{
name: 'day',
scale_height: 60,
scales: [{ unit: 'day', step: 1, format: '%d %M' }]
},
{
name: 'week',
scale_height: 60,
scales: [
{
unit: 'week',
step: 1,
format(date: Date) {
const dateToStr = gantt.date.date_to_str('%m-%d');
const endDate = gantt.date.add(date, -6, 'day'); // 第几周
return `${dateToStr(endDate)}${dateToStr(date)}`;
}
},
{
unit: 'day',
step: 1,
format: '%d',
css(date: Date) {
if (date.getDay() === 0 || date.getDay() === 6) {
return 'day-item weekend weekend-border-bottom';
}
return 'day-item';
}
}
]
},
{
name: 'month',
scale_height: 60,
min_column_width: 18,
scales: [
{ unit: 'month', format: '%Y-%m' },
{
unit: 'day',
step: 1,
format: '%d',
css(date: Date) {
if (date.getDay() === 0 || date.getDay() === 6) {
return 'day-item weekend weekend-border-bottom';
}
return 'day-item';
}
}
]
},
{
name: 'quarter',
height: 60,
min_column_width: 110,
scales: [
{
unit: 'quarter',
step: 1,
format(date: Date) {
const yearStr = `${new Date(date).getFullYear()}`;
const dateToStr = gantt.date.date_to_str('%M');
const endDate = gantt.date.add(gantt.date.add(date, 3, 'month'), -1, 'day');
return `${yearStr + dateToStr(date)} - ${dateToStr(endDate)}`;
}
},
{
unit: 'week',
step: 1,
format(date: Date) {
const dateToStr = gantt.date.date_to_str('%m-%d');
const endDate = gantt.date.add(date, 6, 'day');
return `${dateToStr(date)}${dateToStr(endDate)}`;
}
}
]
},
{
name: 'year',
scale_height: 50,
min_column_width: 150,
scales: [
{ unit: 'year', step: 1, format: '%Y年' },
{ unit: 'month', format: '%Y-%m' }
]
}
];
gantt.ext.zoom.init({ levels: zoomLevels });
gantt.ext.zoom.setLevel(timeType.value);
}
function changeTime(value: string | number) {
timeType.value = value as TimeType;
gantt.ext.zoom.setLevel(value);
}
onMounted(() => {
initGantt();
});
</script>
<template>
<div class="overflow-hidden lt-sm:overflow-auto">
<ElCard header="甘特图演示" content-class="overflow-y-hidden overflow-x-auto" class="h-full card-wrapper">
<template #header>
<div class="flex items-center justify-between">
<p>甘特图演示</p>
<ElSegmented v-model="timeType" :options="data" @change="changeTime" />
</div>
</template>
<div ref="ganttRef" class="size-full min-w-800px"></div>
</ElCard>
</div>
</template>
<style scoped lang="scss"></style>

View File

@@ -1,721 +0,0 @@
export const basicGanttRecords = [
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-15',
progress: 31,
priority: 'P0',
children: [
{
id: 2,
title: 'Project Feature Review',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-07-24',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-25',
end: '2024-07-26',
progress: 100,
priority: 'P1'
},
{
id: 3,
title: 'Project Create',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-27',
end: '2024-07-26',
progress: 100,
priority: 'P1'
},
{
id: 3,
title: 'Develop feature 1',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-08-01',
end: '2024-08-15',
progress: 0,
priority: 'P1'
}
]
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-01',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 100,
priority: 'P1',
children: [
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-08-01',
end: '2024-08-01',
progress: 90,
priority: 'P0'
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-30',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024.07.26',
end: '2024.07.08',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1'
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '07.24.2024',
end: '08.04.2024',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-16',
end: '2024-07-18',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-08-09',
end: '2024-09-11',
progress: 100,
priority: 'P1'
}
]
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-26',
end: '2024-07-28',
progress: 60,
priority: 'P0',
children: [
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1',
children: [
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-26',
end: '2024-07-28',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1'
}
]
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0',
children: [
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-06',
end: '2024-07-08',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1'
}
]
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-26',
end: '2024-07-28',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1',
children: [
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-06',
end: '2024-07-08',
progress: 60,
priority: 'P0'
}
]
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-26',
end: '2024-07-28',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1'
}
]
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1',
children: [
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-23',
end: '2024-07-28',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1'
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-30',
end: '2024-08-14',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 60,
priority: 'P0'
}
]
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0',
children: [
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 100,
priority: 'P1'
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-08-04',
end: '2024-08-04',
progress: 90,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '07/24/2024',
end: '08/04/2024',
progress: 60,
priority: 'P0'
}
]
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-27',
end: '2024-07-28',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1',
children: [
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '07.24.2024',
end: '08.04.2024',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-26',
end: '2024-07-28',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-08-09',
end: '2024-09-11',
progress: 100,
priority: 'P1'
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-26',
end: '2024-07-28',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1'
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-26',
end: '2024-07-28',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1'
}
]
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0',
children: [
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 100,
priority: 'P1'
},
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024.07.06',
end: '2024.07.08',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-29',
end: '2024-07-31',
progress: 100,
priority: 'P1'
}
]
}
];
export const linkGanttRecords = [
{
id: 1,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-15',
end: '2024-07-16',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-16',
end: '2024-07-17',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-18',
end: '2024-07-19',
progress: 90,
priority: 'P0'
},
{
id: 4,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024/07/17',
end: '2024/07/18',
progress: 100,
priority: 'P1'
},
{
id: 5,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '07/19/2024',
end: '07/20/2024',
progress: 60,
priority: 'P0'
},
{
id: 6,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 100,
priority: 'P1'
},
{
id: 7,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 8,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024.07.06',
end: '2024.07.08',
progress: 60,
priority: 'P0'
},
{
id: 9,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024/07/09',
end: '2024/07/11',
progress: 100,
priority: 'P1'
},
{
id: 10,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '07.24.2024',
end: '08.04.2024',
progress: 31,
priority: 'P0'
},
{
id: 11,
title: 'Software Development',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-24',
end: '2024-08-04',
progress: 31,
priority: 'P0'
},
{
id: 12,
title: 'Scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-06',
end: '2024-07-08',
progress: 60,
priority: 'P0'
},
{
id: 13,
title: 'Determine project scope',
developer: 'liufangfang.jane@bytedance.com',
start: '2024-07-09',
end: '2024-07-11',
progress: 100,
priority: 'P1'
}
];
export const customGanttRecords = [
{
id: 1,
title: 'Project Task 1',
developer: 'bear.xiong',
avatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/bear.jpg',
start: '2024-07-24',
end: '2024-07-26',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Project Task 2',
developer: 'wolf.lang',
avatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/wolf.jpg',
start: '07/25/2024',
end: '07/28/2024',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Project Task 3',
developer: 'rabbit.tu',
avatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/rabbit.jpg',
start: '2024-07-28',
end: '2024-08-01',
progress: 100,
priority: 'P1'
},
{
id: 1,
title: 'Project Task 4',
developer: 'cat.mao',
avatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/cat.jpg',
start: '2024-07-31',
end: '2024-08-03',
progress: 31,
priority: 'P0'
},
{
id: 2,
title: 'Project Task 5',
developer: 'bird.niao',
avatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/bird.jpeg',
start: '2024-08-02',
end: '2024-08-04',
progress: 60,
priority: 'P0'
},
{
id: 3,
title: 'Project Task 6',
developer: 'flower.hua',
avatar: 'https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/custom-render/flower.jpg',
start: '2024-08-03',
end: '2024-08-10',
progress: 100,
priority: 'P1'
}
];

View File

@@ -1,792 +0,0 @@
<script setup lang="tsx">
import { onMounted, onUnmounted, shallowRef, watch } from 'vue';
import * as VTableGantt from '@visactor/vtable-gantt';
import * as VTable_editors from '@visactor/vtable-editors';
import { useThemeStore } from '@/store/modules/theme';
import { basicGanttRecords, customGanttRecords, linkGanttRecords } from './data';
const theme = useThemeStore();
const input_editor = new VTable_editors.InputEditor();
const date_input_editor = new VTable_editors.DateInputEditor();
VTableGantt.VTable.register.editor('input', input_editor);
VTableGantt.VTable.register.editor('date-input', date_input_editor);
const basicGanttDomRef = shallowRef<HTMLElement>();
const linkGanttDomRef = shallowRef<HTMLElement>();
const customGanttDomRef = shallowRef<HTMLElement>();
const basicGanttInstance = shallowRef<VTableGantt.Gantt>();
const linkGanttInstance = shallowRef<VTableGantt.Gantt>();
const customGanttInstance = shallowRef<VTableGantt.Gantt>();
const basicGanttColumns = [
{
field: 'title',
title: 'title',
width: 'auto',
sort: true,
tree: true,
editor: 'input'
},
{
field: 'start',
title: 'start',
width: 'auto',
sort: true,
editor: 'date-input'
},
{
field: 'end',
title: 'end',
width: 'auto',
sort: true,
editor: 'date-input'
},
{
field: 'priority',
title: 'priority',
width: 'auto',
sort: true,
editor: 'input'
},
{
field: 'progress',
title: 'progress',
width: 'auto',
sort: true,
headerStyle: {
borderColor: '#e1e4e8'
},
style: {
borderColor: '#e1e4e8',
color: 'green'
},
editor: 'input'
}
];
const basicGanttOption: VTableGantt.GanttConstructorOptions = {
overscrollBehavior: 'none',
records: basicGanttRecords,
taskListTable: {
columns: basicGanttColumns,
tableWidth: 250,
minTableWidth: 100,
maxTableWidth: 600
// rightFrozenColCount: 1
},
frame: {
outerFrameStyle: {
borderLineWidth: 2,
borderColor: '#e1e4e8',
cornerRadius: 8
},
verticalSplitLine: {
lineColor: '#e1e4e8',
lineWidth: 3
},
horizontalSplitLine: {
lineColor: '#e1e4e8',
lineWidth: 3
},
verticalSplitLineMoveable: true,
verticalSplitLineHighlight: {
lineColor: 'green',
lineWidth: 3
}
},
grid: {
// backgroundColor: 'gray',
verticalLine: {
lineWidth: 1,
lineColor: '#e1e4e8'
},
horizontalLine: {
lineWidth: 1,
lineColor: '#e1e4e8'
}
},
headerRowHeight: 40,
rowHeight: 40,
taskBar: {
startDateField: 'start',
endDateField: 'end',
progressField: 'progress',
// resizable: false,
moveable: true,
hoverBarStyle: {
barOverlayColor: 'rgba(99, 144, 0, 0.4)'
},
labelText: '{title} {progress}%',
labelTextStyle: {
// padding: 2,
fontFamily: 'Arial',
fontSize: 16,
textAlign: 'left',
textOverflow: 'ellipsis'
},
barStyle: {
width: 20,
/** 任务条的颜色 */
barColor: '#ee8800',
/** 已完成部分任务条的颜色 */
completedBarColor: '#91e8e0',
/** 任务条的圆角 */
cornerRadius: 8
}
},
timelineHeader: {
colWidth: 100,
backgroundColor: '#EEF1F5',
horizontalLine: {
lineWidth: 1,
lineColor: '#e1e4e8'
},
verticalLine: {
lineWidth: 1,
lineColor: '#e1e4e8'
},
scales: [
{
unit: 'week',
step: 1,
startOfWeek: 'sunday',
format(date: any) {
return `Week ${date.dateIndex}`;
},
style: {
fontSize: 20,
fontWeight: 'bold',
color: 'white',
strokeColor: 'black',
textAlign: 'right',
textBaseline: 'bottom',
textStick: true
// padding: [0, 30, 0, 20]
}
},
{
unit: 'day',
step: 1,
format(date: any) {
return date.dateIndex.toString();
},
style: {
fontSize: 20,
fontWeight: 'bold',
color: 'white',
strokeColor: 'black',
textAlign: 'right',
textBaseline: 'bottom'
}
}
]
},
markLine: [
{
date: '2024-07-28',
style: {
lineWidth: 1,
lineColor: 'blue',
lineDash: [8, 4]
}
},
{
date: '2024-08-17',
style: {
lineWidth: 2,
lineColor: 'red',
lineDash: [8, 4]
}
}
],
rowSeriesNumber: {
title: '行号',
dragOrder: true
},
scrollStyle: {
scrollRailColor: 'RGBA(246,246,246,0.5)',
visible: 'scrolling',
width: 6,
scrollSliderCornerRadius: 2,
scrollSliderColor: '#5cb85c'
}
};
const linkGanttColumns = [
{
field: 'title',
title: 'title',
width: 'auto',
tree: true
},
{
field: 'start',
title: 'start',
width: 'auto',
editor: 'date-input'
},
{
field: 'end',
title: 'end',
width: 'auto',
editor: 'date-input'
},
{
field: 'priority',
title: 'priority',
width: 'auto',
editor: 'input'
},
{
field: 'progress',
title: 'progress',
width: 'auto',
headerStyle: {
borderColor: '#e1e4e8'
},
style: {
borderColor: '#e1e4e8',
color: 'green'
},
editor: 'input'
}
];
const linkGanttOption: VTableGantt.GanttConstructorOptions = {
records: linkGanttRecords,
taskListTable: {
columns: linkGanttColumns,
tableWidth: 400,
minTableWidth: 100,
maxTableWidth: 600
},
dependency: {
links: [
{
type: VTableGantt.TYPES.DependencyType.FinishToStart,
linkedFromTaskKey: 1,
linkedToTaskKey: 2
},
{
type: VTableGantt.TYPES.DependencyType.StartToFinish,
linkedFromTaskKey: 2,
linkedToTaskKey: 3
},
{
type: VTableGantt.TYPES.DependencyType.StartToStart,
linkedFromTaskKey: 3,
linkedToTaskKey: 4
},
{
type: VTableGantt.TYPES.DependencyType.FinishToFinish,
linkedFromTaskKey: 4,
linkedToTaskKey: 5
}
],
// linkSelectable: false,
linkSelectedLineStyle: {
shadowBlur: 5, // 阴影宽度
shadowColor: 'red',
lineColor: 'red',
lineWidth: 1
}
},
frame: {
verticalSplitLineMoveable: true,
outerFrameStyle: {
borderLineWidth: 2,
// borderColor: 'red',
cornerRadius: 8
},
verticalSplitLine: {
lineWidth: 3,
lineColor: '#e1e4e8'
},
verticalSplitLineHighlight: {
lineColor: 'green',
lineWidth: 3
}
},
grid: {
// backgroundColor: 'gray',
verticalLine: {
lineWidth: 1,
lineColor: '#e1e4e8'
},
horizontalLine: {
lineWidth: 1,
lineColor: '#e1e4e8'
}
},
headerRowHeight: 60,
rowHeight: 40,
taskBar: {
startDateField: 'start',
endDateField: 'end',
progressField: 'progress',
labelText: '{title} {progress}%',
labelTextStyle: {
fontFamily: 'Arial',
fontSize: 16,
textAlign: 'left'
},
barStyle: {
width: 20,
/** 任务条的颜色 */
barColor: '#ee8800',
/** 已完成部分任务条的颜色 */
completedBarColor: '#91e8e0',
/** 任务条的圆角 */
cornerRadius: 10
},
selectedBarStyle: {
shadowBlur: 5, // 阴影宽度
shadowOffsetX: 0, // x方向偏移
shadowOffsetY: 0, // Y方向偏移
shadowColor: 'black', // 阴影颜色
borderColor: 'red', // 边框颜色
borderLineWidth: 1 // 边框宽度
}
},
timelineHeader: {
verticalLine: {
lineWidth: 1,
lineColor: '#e1e4e8'
},
horizontalLine: {
lineWidth: 1,
lineColor: '#e1e4e8'
},
backgroundColor: '#EEF1F5',
colWidth: 60,
scales: [
{
unit: 'week',
step: 1,
startOfWeek: 'sunday',
format(date: any) {
return `Week ${date.dateIndex}`;
},
style: {
fontSize: 20,
fontWeight: 'bold',
color: 'red'
}
},
{
unit: 'day',
step: 1,
format(date: any) {
return date.dateIndex.toString();
},
style: {
fontSize: 20,
fontWeight: 'bold',
color: 'red'
}
}
]
},
minDate: '2024-07-14',
maxDate: '2024-10-15',
rowSeriesNumber: {
title: '行号',
dragOrder: true
},
scrollStyle: {
visible: 'scrolling'
},
overscrollBehavior: 'none'
};
const barColors0 = ['#aecde6', '#c6a49a', '#ffb582', '#eec1de', '#b3d9b3', '#cccccc', '#e59a9c', '#d9d1a5', '#c9bede'];
const barColors = ['#1f77b4', '#8c564b', '#ff7f0e', '#e377c2', '#2ca02c', '#7f7f7f', '#d62728', '#bcbd22', '#9467bd'];
const customGanttColumns: VTableGantt.ColumnsDefine = [
{
field: 'title',
title: 'TASK',
width: '200',
headerStyle: {
textAlign: 'center',
fontSize: 20,
fontWeight: 'bold'
// color: 'black',
// bgColor: '#f0f0fb'
},
style: {
// bgColor: '#f0f0fb'
},
customLayout: (args: any) => {
const { table, row, col, rect } = args;
const taskRecord = table.getCellOriginRecord(col, row);
const { height, width } = rect ?? table.getCellRect(col, row);
const container = new VTableGantt.VRender.Group({
y: 10,
x: 20,
height: height - 20,
width: width - 40,
fill: '#ddd',
display: 'flex',
flexDirection: 'column',
cornerRadius: 30
});
const developer = new VTableGantt.VRender.Text({
text: taskRecord.developer,
fontSize: 16,
fontFamily: 'sans-serif',
fill: barColors[args.row],
fontWeight: 'bold',
maxLineWidth: width - 120,
boundsPadding: [10, 0, 0, 0],
alignSelf: 'center'
});
container.add(developer);
const days = new VTableGantt.VRender.Text({
text: `${VTableGantt.tools.formatDate(new Date(taskRecord.start), 'mm/dd')}-${VTableGantt.tools.formatDate(
new Date(taskRecord.end),
'mm/dd'
)}`,
fontSize: 12,
fontFamily: 'sans-serif',
fontWeight: 'bold',
fill: 'black',
boundsPadding: [10, 0, 0, 0],
alignSelf: 'center'
});
container.add(days);
return {
rootContainer: container,
expectedWidth: 160
};
}
}
];
const customGanttOption: VTableGantt.GanttConstructorOptions = {
records: customGanttRecords,
taskListTable: {
columns: customGanttColumns,
tableWidth: 'auto'
},
frame: {
outerFrameStyle: {
borderLineWidth: 2,
borderColor: '#E1E4E8',
cornerRadius: 8
}
// verticalSplitLineHighlight: {
// lineColor: 'green',
// lineWidth: 3
// }
},
grid: {
// backgroundColor: '#f0f0fb',
// vertical: {
// lineWidth: 1,
// lineColor: '#e1e4e8'
// },
horizontalLine: {
lineWidth: 2,
lineColor: '#d5d9ee'
}
},
headerRowHeight: 60,
rowHeight: 80,
taskBar: {
startDateField: 'start',
endDateField: 'end',
progressField: 'progress',
barStyle: { width: 60 },
customLayout: (args: any) => {
const colorLength = barColors.length;
const { width, height, index, taskDays, progress, taskRecord } = args;
const container = new VTableGantt.VRender.Group({
width,
height,
cornerRadius: 30,
fill: {
gradient: 'linear',
x0: 0,
y0: 0,
x1: 1,
y1: 0,
stops: [
{
offset: 0,
color: barColors0[index % colorLength]
},
{
offset: 0.5,
color: barColors[index % colorLength]
},
{
offset: 1,
color: barColors0[index % colorLength]
}
]
},
display: 'flex',
flexDirection: 'row',
flexWrap: 'nowrap'
});
const containerLeft = new VTableGantt.VRender.Group({
height,
width: 60,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'space-around'
// fill: 'red'
});
container.add(containerLeft as any);
const avatar = new VTableGantt.VRender.Image({
width: 50,
height: 50,
image: taskRecord.avatar,
cornerRadius: 25
});
containerLeft.add(avatar);
const containerCenter = new VTableGantt.VRender.Group({
height,
width: width - 120,
display: 'flex',
flexDirection: 'column'
// alignItems: 'left'
});
container.add(containerCenter as any);
const developer = new VTableGantt.VRender.Text({
text: taskRecord.developer,
fontSize: 16,
fontFamily: 'sans-serif',
fill: 'white',
fontWeight: 'bold',
maxLineWidth: width - 120,
boundsPadding: [10, 0, 0, 0]
});
containerCenter.add(developer);
const days = new VTableGantt.VRender.Text({
text: `${taskDays}`,
fontSize: 13,
fontFamily: 'sans-serif',
fill: 'white',
boundsPadding: [10, 0, 0, 0]
});
containerCenter.add(days);
if (width >= 120) {
const containerRight = new VTableGantt.VRender.Group({
cornerRadius: 20,
fill: 'white',
height: 40,
width: 40,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center', // 垂直方向居中对齐
boundsPadding: [10, 0, 0, 0]
});
container.add(containerRight as any);
const progressText = new VTableGantt.VRender.Text({
text: `${progress}%`,
fontSize: 12,
fontFamily: 'sans-serif',
fill: 'black',
alignSelf: 'center',
fontWeight: 'bold',
maxLineWidth: (width - 60) / 2,
boundsPadding: [0, 0, 0, 0]
});
containerRight.add(progressText);
}
return {
rootContainer: container
// renderDefaultBar: true
// renderDefaultText: true
};
},
hoverBarStyle: {
cornerRadius: 30
}
},
timelineHeader: {
backgroundColor: '#f0f0fb',
colWidth: 80,
// verticalLine: {
// lineColor: 'red',
// lineWidth: 1,
// lineDash: [4, 2]
// },
// horizontalLine: {
// lineColor: 'green',
// lineWidth: 1,
// lineDash: [4, 2]
// },
scales: [
{
unit: 'day',
step: 1,
format(date: any) {
return date.dateIndex.toString();
},
customLayout: (args: any) => {
const { width, height, startDate, dateIndex } = args;
const container = new VTableGantt.VRender.Group({
width,
height,
// fill: '#f0f0fb',
display: 'flex',
flexDirection: 'row',
flexWrap: 'nowrap'
});
const containerLeft = new VTableGantt.VRender.Group({
height,
width: 30,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'space-around'
// fill: 'red'
});
container.add(containerLeft as any);
const avatar = new VTableGantt.VRender.Image({
width: 20,
height: 30,
image:
'<svg t="1724675965803" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4299" width="200" height="200"><path d="M53.085678 141.319468C23.790257 141.319468 0 165.035326 0 194.34775L0 918.084273C0 947.295126 23.796789 971.112572 53.085678 971.112572L970.914322 971.112572C1000.209743 971.112572 1024 947.396696 1024 918.084273L1024 194.34775C1024 165.136896 1000.203211 141.319468 970.914322 141.319468L776.827586 141.319468 812.137931 176.629813 812.137931 88.275862C812.137931 68.774506 796.328942 52.965517 776.827586 52.965517 757.32623 52.965517 741.517241 68.774506 741.517241 88.275862L741.517241 176.629813 741.517241 211.940158 776.827586 211.940158 970.914322 211.940158C961.186763 211.940158 953.37931 204.125926 953.37931 194.34775L953.37931 918.084273C953.37931 908.344373 961.25643 900.491882 970.914322 900.491882L53.085678 900.491882C62.813237 900.491882 70.62069 908.306097 70.62069 918.084273L70.62069 194.34775C70.62069 204.087649 62.74357 211.940158 53.085678 211.940158L247.172414 211.940158C266.67377 211.940158 282.482759 196.131169 282.482759 176.629813 282.482759 157.128439 266.67377 141.319468 247.172414 141.319468L53.085678 141.319468ZM211.862069 176.629813C211.862069 196.131169 227.671058 211.940158 247.172414 211.940158 266.67377 211.940158 282.482759 196.131169 282.482759 176.629813L282.482759 88.275862C282.482759 68.774506 266.67377 52.965517 247.172414 52.965517 227.671058 52.965517 211.862069 68.774506 211.862069 88.275862L211.862069 176.629813ZM1024 353.181537 1024 317.871192 988.689655 317.871192 35.310345 317.871192 0 317.871192 0 353.181537 0 441.457399C0 460.958755 15.808989 476.767744 35.310345 476.767744 54.811701 476.767744 70.62069 460.958755 70.62069 441.457399L70.62069 353.181537 35.310345 388.491882 988.689655 388.491882 953.37931 353.181537 953.37931 441.457399C953.37931 460.958755 969.188299 476.767744 988.689655 476.767744 1008.191011 476.767744 1024 460.958755 1024 441.457399L1024 353.181537ZM776.937913 582.62069C796.439287 582.62069 812.248258 566.811701 812.248258 547.310345 812.248258 527.808989 796.439287 512 776.937913 512L247.172414 512C227.671058 512 211.862069 527.808989 211.862069 547.310345 211.862069 566.811701 227.671058 582.62069 247.172414 582.62069L776.937913 582.62069ZM247.172414 688.551724C227.671058 688.551724 211.862069 704.360713 211.862069 723.862069 211.862069 743.363425 227.671058 759.172414 247.172414 759.172414L600.386189 759.172414C619.887563 759.172414 635.696534 743.363425 635.696534 723.862069 635.696534 704.360713 619.887563 688.551724 600.386189 688.551724L247.172414 688.551724ZM776.827586 211.940158 741.517241 176.629813 741.517241 247.328574C741.517241 266.829948 757.32623 282.638919 776.827586 282.638919 796.328942 282.638919 812.137931 266.829948 812.137931 247.328574L812.137931 176.629813 812.137931 141.319468 776.827586 141.319468 247.172414 141.319468C227.671058 141.319468 211.862069 157.128439 211.862069 176.629813 211.862069 196.131169 227.671058 211.940158 247.172414 211.940158L776.827586 211.940158ZM282.482759 176.629813C282.482759 157.128439 266.67377 141.319468 247.172414 141.319468 227.671058 141.319468 211.862069 157.128439 211.862069 176.629813L211.862069 247.328574C211.862069 266.829948 227.671058 282.638919 247.172414 282.638919 266.67377 282.638919 282.482759 266.829948 282.482759 247.328574L282.482759 176.629813Z" fill="#389BFF" p-id="4300"></path></svg>'
});
containerLeft.add(avatar);
const containerCenter = new VTableGantt.VRender.Group({
height,
width: width - 30,
display: 'flex',
flexDirection: 'column'
// alignItems: 'left'
});
container.add(containerCenter as any);
const dayNumber = new VTableGantt.VRender.Text({
text: String(dateIndex).padStart(2, '0'),
fontSize: 20,
fontWeight: 'bold',
fontFamily: 'sans-serif',
fill: '#777',
textAlign: 'right',
maxLineWidth: width - 30,
boundsPadding: [15, 0, 0, 0]
});
containerCenter.add(dayNumber);
const weekDay = new VTableGantt.VRender.Text({
text: VTableGantt.tools.getWeekday(startDate, 'short').toLocaleUpperCase(),
fontSize: 12,
fontFamily: 'sans-serif',
fill: '#777',
boundsPadding: [0, 0, 0, 0]
});
containerCenter.add(weekDay);
return {
rootContainer: container
// renderDefaultText: true
};
}
}
]
},
minDate: '2024-07-20',
maxDate: '2024-08-15',
markLine: [
{
date: '2024-07-29',
style: {
lineWidth: 1,
lineColor: 'blue',
lineDash: [8, 4]
}
},
{
date: '2024-08-17',
style: {
lineWidth: 2,
lineColor: 'red',
lineDash: [8, 4]
}
}
],
scrollStyle: {
scrollRailColor: 'RGBA(246,246,246,0.5)',
visible: 'focus',
width: 6,
scrollSliderCornerRadius: 2,
scrollSliderColor: '#5cb85c'
}
};
function initVTableGantt() {
basicGanttInstance.value = new VTableGantt.Gantt(basicGanttDomRef.value as HTMLElement, getOption(basicGanttOption));
linkGanttInstance.value = new VTableGantt.Gantt(linkGanttDomRef.value as HTMLElement, getOption(linkGanttOption));
customGanttInstance.value = new VTableGantt.Gantt(
customGanttDomRef.value as HTMLElement,
getOption(customGanttOption)
);
}
function getOption(option: VTableGantt.GanttConstructorOptions) {
const isDark = theme.darkMode;
if (isDark) {
option.taskListTable!.theme = VTableGantt.VTable.themes.DARK;
option.timelineHeader.backgroundColor = '#212121';
option.underlayBackgroundColor = '#000';
} else {
option.taskListTable!.theme = VTableGantt.VTable.themes.DEFAULT;
option.timelineHeader.backgroundColor = '#f0f0fb';
option.underlayBackgroundColor = '#fff';
}
return option;
}
const stopHandle = watch(
() => theme.darkMode,
_newValue => {
basicGanttInstance.value?.release();
linkGanttInstance.value?.release();
customGanttInstance.value?.release();
initVTableGantt();
}
);
onMounted(() => {
initVTableGantt();
});
onUnmounted(() => {
stopHandle();
});
</script>
<template>
<ElSpace direction="vertical" fill :size="16">
<ElCard header="VTableGantt" class="h-full card-wrapper">
<WebSiteLink label="More Demos: " link="https://www.visactor.com/vtable/example" />
</ElCard>
<ElCard class="h-full card-wrapper">
<div ref="basicGanttDomRef" class="relative h-400px"></div>
</ElCard>
<ElCard class="h-full card-wrapper">
<div ref="linkGanttDomRef" class="relative h-400px"></div>
</ElCard>
<ElCard class="h-full card-wrapper">
<div ref="customGanttDomRef" class="relative h-400px"></div>
</ElCard>
</ElSpace>
</template>

View File

@@ -1,32 +0,0 @@
export const icons = [
'mdi:emoticon',
'mdi:ab-testing',
'ph:alarm',
'ph:android-logo',
'ph:align-bottom',
'ph:archive-box-light',
'uil:basketball',
'uil:brightness-plus',
'uil:capture',
'mdi:apps-box',
'mdi:alert',
'mdi:airballoon',
'mdi:airplane-edit',
'mdi:alpha-f-box-outline',
'mdi:arm-flex-outline',
'ic:baseline-10mp',
'ic:baseline-access-time',
'ic:baseline-brightness-4',
'ic:baseline-brightness-5',
'ic:baseline-credit-card',
'ic:baseline-filter-1',
'ic:baseline-filter-2',
'ic:baseline-filter-3',
'ic:baseline-filter-4',
'ic:baseline-filter-5',
'ic:baseline-filter-6',
'ic:baseline-filter-7',
'ic:baseline-filter-8',
'ic:baseline-filter-9',
'ic:baseline-filter-9-plus'
];

View File

@@ -1,53 +0,0 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { icons } from './icons';
defineOptions({ name: 'IconPage' });
const selectValue = ref('');
const localIcons = ['custom-icon', 'activity', 'at-sign', 'cast', 'chrome', 'copy', 'wind'];
</script>
<template>
<div class="h-full">
<ElCard header="Icon组件示例" class="card-wrapper">
<div class="grid grid-cols-10">
<template v-for="item in icons" :key="item">
<div class="mt-5px flex-x-center">
<SvgIcon :icon="item" class="text-30px" />
</div>
</template>
</div>
<div class="mt-50px">
<h1 class="mb-20px text-18px font-500">Icon图标选择器</h1>
<CustomIconSelect v-model:value="selectValue" :icons="icons" />
</div>
<template #footer>
<WebSiteLink label="iconify地址" link="https://icones.js.org/" class="mt-10px" />
</template>
</ElCard>
<ElCard header="自定义图标示例" class="mt-10px card-wrapper">
<div class="pb-12px text-16px">
在src/assets/svg-icon文件夹下的svg文件通过在template里面以 icon-local-{文件名} 直接渲染,
其中icon-local为.env文件里的 VITE_ICON_LOCAL_PREFIX
</div>
<div class="grid grid-cols-10">
<div class="mt-5px flex-x-center">
<icon-local-activity class="text-40px text-success" />
</div>
<div class="mt-5px flex-x-center">
<icon-local-cast class="text-20px text-error" />
</div>
</div>
<div class="py-12px text-16px">通过SvgIcon组件动态渲染, 菜单通过meta的localIcon属性渲染自定义图标</div>
<div class="grid grid-cols-10">
<div v-for="(fileName, index) in localIcons" :key="index" class="mt-5px flex-x-center">
<SvgIcon :local-icon="fileName" class="text-30px text-primary" />
</div>
</div>
</ElCard>
</div>
</template>
<style scoped></style>

View File

@@ -1,32 +0,0 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useScriptTag } from '@vueuse/core';
import { BAIDU_MAP_SDK_URL } from '@/constants/map-sdk';
defineOptions({ name: 'BaiduMap' });
window.HOST_TYPE = '2';
const { load } = useScriptTag(BAIDU_MAP_SDK_URL);
const domRef = ref<HTMLDivElement>();
async function renderMap() {
await load(true);
if (!domRef.value) return;
const map = new BMap.Map(domRef.value);
const point = new BMap.Point(114.05834626586915, 22.546789983033168);
map.centerAndZoom(point, 15);
map.enableScrollWheelZoom();
}
onMounted(() => {
renderMap();
});
</script>
<template>
<div ref="domRef" class="h-full w-full"></div>
</template>
<style scoped></style>

View File

@@ -1,32 +0,0 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useScriptTag } from '@vueuse/core';
import { AMAP_SDK_URL } from '@/constants/map-sdk';
defineOptions({ name: 'GaodeMap' });
const { load } = useScriptTag(AMAP_SDK_URL);
const domRef = ref<HTMLDivElement>();
async function renderMap() {
await load(true);
if (!domRef.value) return;
const map = new AMap.Map(domRef.value, {
zoom: 11,
center: [114.05834626586915, 22.546789983033168],
viewMode: '3D'
});
map.getCenter();
}
onMounted(() => {
renderMap();
});
</script>
<template>
<div ref="domRef" class="h-full w-full"></div>
</template>
<style scoped></style>

View File

@@ -1,5 +0,0 @@
import BaiduMap from './baidu-map.vue';
import GaodeMap from './gaode-map.vue';
import TencentMap from './tencent-map.vue';
export { BaiduMap, GaodeMap, TencentMap };

View File

@@ -1,32 +0,0 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useScriptTag } from '@vueuse/core';
import { TENCENT_MAP_SDK_URL } from '@/constants/map-sdk';
defineOptions({ name: 'TencentMap' });
const { load } = useScriptTag(TENCENT_MAP_SDK_URL);
const domRef = ref<HTMLDivElement | null>(null);
async function renderMap() {
await load(true);
if (!domRef.value) return;
// eslint-disable-next-line no-new
new TMap.Map(domRef.value, {
center: new TMap.LatLng(39.98412, 116.307484),
zoom: 11,
viewMode: '3D'
});
}
onMounted(() => {
renderMap();
});
</script>
<template>
<div ref="domRef" class="h-full w-full"></div>
</template>
<style scoped></style>

View File

@@ -1,42 +0,0 @@
<script setup lang="ts">
import { type Component, ref } from 'vue';
import { BaiduMap, GaodeMap, TencentMap } from './components';
defineOptions({ name: 'MapComp' });
interface Map {
id: string;
label: string;
component: Component;
}
const maps: Map[] = [
{ id: 'gaode', label: '高德地图', component: GaodeMap },
{ id: 'tencent', label: '腾讯地图', component: TencentMap },
{ id: 'baidu', label: '百度地图', component: BaiduMap }
];
const activeMap = ref(maps[0].id);
</script>
<template>
<div class="h-full">
<ElCard header="地图插件" class="h-full" content-style="overflow:hidden">
<ElTabs class="h-full">
<ElTabPane
v-for="item in maps"
:key="item.id"
v-model="activeMap"
class="h-full"
:value="item.id"
:label="item.label"
lazy
>
<component :is="item.component" />
</ElTabPane>
</ElTabs>
</ElCard>
</div>
</template>
<style scoped></style>

View File

@@ -1,91 +0,0 @@
<script setup lang="ts">
import { ref, shallowRef } from 'vue';
import VuePdfEmbed from 'vue-pdf-embed';
import { useLoading } from '@sa/hooks';
defineOptions({ name: 'PdfPage' });
const { loading, endLoading } = useLoading(true);
const pdfRef = shallowRef<InstanceType<typeof VuePdfEmbed> | null>(null);
const source = `https://xiaoxian521.github.io/hyperlink/pdf/Cookie%E5%92%8CSession%E5%8C%BA%E5%88%AB%E7%94%A8%E6%B3%95.pdf`;
const showAllPages = ref(false);
const currentPage = ref<number>(1);
const pageCount = ref(1);
function onPdfRendered() {
endLoading();
if (pdfRef.value?.doc) {
pageCount.value = pdfRef.value.doc.numPages;
}
}
function showAllPagesChange() {
currentPage.value = 1;
}
const rotations = [0, 90, 180, 270];
const currentRotation = ref(0);
function handleRotate() {
currentRotation.value = (currentRotation.value + 1) % 4;
}
async function handlePrint() {
await pdfRef.value?.print(undefined, 'test.pdf', true);
}
async function handleDownload() {
await pdfRef.value?.download('test.pdf');
}
</script>
<template>
<div class="overflow-hidden">
<ElCard header="PDF 预览" class="h-full card-wrapper" content-class="overflow-hidden">
<div class="h-[calc(100%-30px)] flex-col-stretch">
<GithubLink link="https://github.com/hrynko/vue-pdf-embed" />
<WebSiteLink label="文档地址:" link="https://www.npmjs.com/package/vue-pdf-embed" />
<div class="flex-y-center justify-end gap-12px">
<ElCheckbox v-model="showAllPages" @change="showAllPagesChange">显示所有页面</ElCheckbox>
<ButtonIcon tooltip-content="旋转90度" @click="handleRotate">
<icon-material-symbols-light:rotate-90-degrees-ccw-outline-rounded />
</ButtonIcon>
<ButtonIcon tooltip-content="打印" @click="handlePrint">
<icon-mdi:printer />
</ButtonIcon>
<ButtonIcon tooltip-content="下载" @click="handleDownload">
<icon-charm:download />
</ButtonIcon>
</div>
<ElScrollbar class="flex-1-hidden">
<NSkeleton v-if="loading" size="small" class="mt-12px" text :repeat="12" />
<VuePdfEmbed
ref="pdfRef"
class="container overflow-auto"
:class="{ 'h-0': loading }"
:rotation="rotations[currentRotation]"
:page="currentPage"
:source="source"
@rendered="onPdfRendered"
/>
</ElScrollbar>
<div class="flex-y-center justify-between">
<div v-if="showAllPages" class="text-18px font-medium">{{ pageCount }}</div>
<ElPagination
v-else
key="pdf-page"
layout="prev, pager, next"
background
:page-count="pageCount"
@current-change="currentPage = $event"
/>
</div>
</div>
</ElCard>
</div>
</template>
<style scoped></style>

View File

@@ -1,59 +0,0 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { html } from 'pinyin-pro';
import domPurify from 'dompurify';
defineOptions({ name: 'PinyinPage' });
const domRef = ref<HTMLElement | null>(null);
const domRef2 = ref<HTMLElement | null>(null);
const domRef3 = ref<HTMLElement | null>(null);
function renderHtml() {
if (!domRef.value || !domRef2.value || !domRef3.value) return;
const text = 'CN-RDMS 是灿能电力内部使用的研发管理系统前端项目';
const code = domPurify.sanitize(html(text));
const code2 = domPurify.sanitize(html(text, { toneType: 'none' }));
domRef.value.innerHTML = code;
domRef2.value.innerHTML = code2;
domRef3.value.innerHTML = code;
}
onMounted(() => {
renderHtml();
});
</script>
<template>
<div>
<ElCard header="pinyin 插件" class="h-full card-wrapper">
<ElSpace :vertical="true">
<GithubLink link="https://github.com/zh-lx/pinyin-pro" />
<WebSiteLink label="文档地址:" link="https://pinyin-pro.cn/" />
</ElSpace>
<ElDivider content-position="left">常规使用</ElDivider>
<p ref="domRef" class="text-18px"></p>
<ElDivider content-position="left">不带音调</ElDivider>
<p ref="domRef2" class="text-18px"></p>
<ElDivider content-position="left">自定义样式</ElDivider>
<p ref="domRef3" class="custom-style text-18px"></p>
</ElCard>
</div>
</template>
<style lang="scss" scoped>
.custom-style {
:deep(.py-result-item) {
.py-chinese-item {
--uno: text-primary;
}
.py-pinyin-item {
--uno: text-error;
}
}
}
</style>

View File

@@ -1,41 +0,0 @@
<script lang="ts" setup>
import printJS from 'print-js';
defineOptions({ name: 'PrintPage' });
function printTable() {
printJS({
printable: [
{ name: 'CN-RDMS', wechat: 'internal', remark: '内部演示数据' },
{ name: 'CN-RDMS', wechat: 'internal', remark: '内部演示数据' }
],
properties: ['name', 'wechat', 'remark'],
type: 'json'
});
}
function printImage() {
printJS({
printable: [
'https://i.loli.net/2021/11/24/1J6REWXiHomU2kM.jpg',
'https://i.loli.net/2021/11/24/1J6REWXiHomU2kM.jpg'
],
type: 'image',
header: 'Multiple Images',
imageStyle: 'width:100%;'
});
}
</script>
<template>
<div class="h-full">
<ElCard header="打印" class="card-wrapper">
<ElButton type="primary" class="mr-10px" @click="printTable">打印表格</ElButton>
<ElButton type="primary" @click="printImage">打印图片</ElButton>
<template #footer>
<GithubLink label="printJS" link="https://github.com/crabbly/Print.js" class="mt-10px" />
</template>
</ElCard>
</div>
</template>
<style scoped></style>

View File

@@ -1,60 +0,0 @@
<script setup lang="ts">
import SwiperCore from 'swiper';
import { Navigation, Pagination } from 'swiper/modules';
import { Swiper, SwiperSlide } from 'swiper/vue';
import type { SwiperOptions } from 'swiper/types';
defineOptions({ name: 'SwiperComp' });
type SwiperExampleOptions = Pick<
SwiperOptions,
'navigation' | 'pagination' | 'scrollbar' | 'slidesPerView' | 'slidesPerGroup' | 'spaceBetween' | 'direction' | 'loop'
>;
interface SwiperExample {
id: number;
label: string;
options: Partial<SwiperExampleOptions>;
}
SwiperCore.use([Navigation, Pagination]);
const swiperExample: SwiperExample[] = [
{ id: 0, label: 'Default', options: {} },
{ id: 1, label: 'Navigation', options: { navigation: true } },
{ id: 2, label: 'Pagination', options: { pagination: true } },
{ id: 3, label: 'Pagination dynamic', options: { pagination: { dynamicBullets: true } } },
{ id: 4, label: 'Pagination progress', options: { navigation: true, pagination: { type: 'progressbar' } } },
{ id: 5, label: 'Pagination fraction', options: { navigation: true, pagination: { type: 'fraction' } } },
{ id: 6, label: 'Slides per view', options: { pagination: { clickable: true }, slidesPerView: 3, spaceBetween: 30 } },
{ id: 7, label: 'Infinite loop', options: { navigation: true, pagination: { clickable: true }, loop: true } }
];
</script>
<template>
<div>
<ElCard header="Swiper插件" class="card-wrapper">
<ElSpace :vertical="true">
<GithubLink link="https://github.com/nolimits4web/swiper" />
<WebSiteLink label="vue3版文档地址" link="https://swiperjs.com/vue" />
<WebSiteLink label="插件demo地址" link="https://swiperjs.com/demos" />
</ElSpace>
<ElSpace class="w-full" direction="vertical">
<div v-for="item in swiperExample" :key="item.id" class="w-full">
<h3 class="py-24px text-24px font-bold">{{ item.label }}</h3>
<Swiper v-bind="item.options">
<SwiperSlide v-for="i in 5" :key="i">
<div class="h-240px w-full flex-center border-1px border-#999 text-18px font-bold">Slide{{ i }}</div>
</SwiperSlide>
</Swiper>
</div>
</ElSpace>
</ElCard>
</div>
</template>
<style scoped>
:deep(.el-space__item) {
width: 100%;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,408 +0,0 @@
<script setup lang="tsx">
import { computed, onMounted, ref } from 'vue';
import {
Group,
Image,
ListColumn,
ListTable,
Menu,
PivotChart,
PivotColumnDimension,
PivotCorner,
PivotIndicator,
PivotRowDimension,
PivotTable,
Tag,
Text,
VTable,
registerChartModule
} from '@visactor/vue-vtable';
import VChart from '@visactor/vchart';
import { useThemeStore } from '@/store/modules/theme';
import { customListRecords, listTableRecords, pivotChartColumns, pivotChartIndicators, pivotChartRows } from './data';
registerChartModule('vchart', VChart);
const titleColorPool = ['#3370ff', '#34c724', '#ff9f1a', '#ff4050', '#1f2329'];
const themeStore = useThemeStore();
// list table
const listTableRef = ref(null);
const listOptions = computed(() => {
const options = {
theme: themeStore.darkMode ? VTable.themes.DARK : VTable.themes.DEFAULT
};
return options;
});
const listRecords = ref<Record<string, string | number>[]>(listTableRecords);
// group table
const groupTableRef = ref(null);
const groupOptions = computed(() => {
const options = {
groupBy: ['Category', 'Sub-Category'],
theme: (themeStore.darkMode ? VTable.themes.DARK : VTable.themes.DEFAULT).extends({
groupTitleStyle: {
fontWeight: 'bold',
bgColor: (args: any) => {
const { col, row, table } = args;
const index = table.getGroupTitleLevel(col, row);
if (index !== undefined) {
return titleColorPool[index % titleColorPool.length] as string;
}
return 'white';
}
}
})
};
return options;
});
const groupRecords = ref<Record<string, string | number>[]>(listTableRecords);
// pivot table
const pivotTableRef = ref(null);
const pivotTableOptions = computed(() => {
return {
tooltip: {
isShowOverflowTextTooltip: true
},
dataConfig: {
sortRules: [
{
sortField: 'Category',
sortBy: ['Office Supplies', 'Technology', 'Furniture']
}
]
},
widthMode: 'standard',
theme: themeStore.darkMode ? VTable.themes.DARK : VTable.themes.DEFAULT,
emptyTip: {
text: 'no data records'
}
};
});
const pivotTableIndicators = ref([
{
indicatorKey: 'Quantity',
title: 'Quantity',
width: 'auto',
showSort: false,
headerStyle: { fontWeight: 'normal' },
style: {
padding: [16, 28, 16, 28],
color(args: any) {
return args.dataValue >= 0 ? 'black' : 'red';
}
}
},
{
indicatorKey: 'Sales',
title: 'Sales',
width: 'auto',
showSort: false,
headerStyle: { fontWeight: 'normal' },
format: (rec: string) => `$${Number(rec).toFixed(2)}`,
style: {
padding: [16, 28, 16, 28],
color(args: any) {
return args.dataValue >= 0 ? 'black' : 'red';
}
}
},
{
indicatorKey: 'Profit',
title: 'Profit',
width: 'auto',
showSort: false,
headerStyle: { fontWeight: 'normal' },
format: (rec: string) => `$${Number(rec).toFixed(2)}`,
style: {
padding: [16, 28, 16, 28],
color(args: any) {
return args.dataValue >= 0 ? 'black' : 'red';
}
}
}
]);
const pivotTableRows = ref([
{
dimensionKey: 'City',
title: 'City',
headerStyle: { textStick: true },
width: 'auto'
}
]);
const pivotTableRecords = ref([]);
// pivot chart
const pivotChartRef = ref(null);
const pivotChartOptions = computed(() => {
return {
rows: pivotChartRows,
columns: pivotChartColumns,
indicators: pivotChartIndicators,
indicatorsAsCol: false,
defaultRowHeight: 200,
defaultHeaderRowHeight: 50,
defaultColWidth: 280,
defaultHeaderColWidth: 100,
indicatorTitle: '指标',
autoWrapText: true,
corner: {
titleOnDimension: 'row',
headerStyle: { autoWrapText: true }
},
legends: {
orient: 'bottom',
type: 'discrete',
data: [
{ label: 'Consumer-Quantity', shape: { fill: '#2E62F1', symbolType: 'circle' } },
{ label: 'Consumer-Quantity', shape: { fill: '#4DC36A', symbolType: 'square' } },
{ label: 'Home Office-Quantity', shape: { fill: '#FF8406', symbolType: 'square' } },
{ label: 'Consumer-Sales', shape: { fill: '#FFCC00', symbolType: 'square' } },
{ label: 'Consumer-Sales', shape: { fill: '#4F44CF', symbolType: 'square' } },
{ label: 'Home Office-Sales', shape: { fill: '#5AC8FA', symbolType: 'square' } },
{ label: 'Consumer-Profit', shape: { fill: '#003A8C', symbolType: 'square' } },
{ label: 'Consumer-Profit', shape: { fill: '#B08AE2', symbolType: 'square' } },
{ label: 'Home Office-Profit', shape: { fill: '#FF6341', symbolType: 'square' } }
]
},
theme: (themeStore.darkMode ? VTable.themes.DARK : VTable.themes.DEFAULT).extends({
bodyStyle: { borderColor: 'gray', borderLineWidth: [1, 0, 0, 1] },
headerStyle: { borderColor: 'gray', borderLineWidth: [0, 0, 1, 1], hover: { cellBgColor: '#CCE0FF' } },
rowHeaderStyle: { borderColor: 'gray', borderLineWidth: [1, 1, 0, 0], hover: { cellBgColor: '#CCE0FF' } },
cornerHeaderStyle: { borderColor: 'gray', borderLineWidth: [0, 1, 1, 0], hover: { cellBgColor: '' } },
cornerRightTopCellStyle: { borderColor: 'gray', borderLineWidth: [0, 0, 1, 1], hover: { cellBgColor: '' } },
cornerLeftBottomCellStyle: { borderColor: 'gray', borderLineWidth: [1, 1, 0, 0], hover: { cellBgColor: '' } },
cornerRightBottomCellStyle: { borderColor: 'gray', borderLineWidth: [1, 0, 0, 1], hover: { cellBgColor: '' } },
rightFrozenStyle: { borderColor: 'gray', borderLineWidth: [1, 0, 1, 1], hover: { cellBgColor: '' } },
bottomFrozenStyle: { borderColor: 'gray', borderLineWidth: [1, 1, 0, 1], hover: { cellBgColor: '' } },
selectionStyle: { cellBgColor: '', cellBorderColor: '' },
frameStyle: { borderLineWidth: 0 }
}),
emptyTip: {
text: 'no data records'
}
};
});
const pivotChartRecords = ref([] as any);
const handleLegendItemClick = (args: { value: any }) => {
(pivotChartRef?.value as any)?.vTableInstance.updateFilterRules([
{
filterKey: 'Segment-Indicator',
filteredValues: args.value
}
]);
};
// custom layout list table
const customLayoutListTableRef = ref(null);
const customLayoutListTableOptions = computed(() => {
return {
defaultRowHeight: 80,
theme: themeStore.darkMode ? VTable.themes.DARK : VTable.themes.DEFAULT
};
});
const customLayoutListTableRecords = ref(customListRecords);
const customLayoutListTableColumnStyle = ref({ fontFamily: 'Arial', fontSize: 12, fontWeight: 'bold' });
onMounted(() => {
// pivot tablt records
fetch('https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/North_American_Superstore_Pivot_data.json')
.then(res => res.json())
.then(jsonData => {
// update record
pivotTableRecords.value = jsonData;
});
// pivot chart records
fetch('https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/North_American_Superstore_Pivot_Chart_data.json')
.then(res => res.json())
.then(data => {
// update record
pivotChartRecords.value = data;
});
});
</script>
<template>
<div class="h-full">
<ElSpace fill direction="vertical" class="mb-16px w-full" :size="16">
<ElCard header="List Table" class="h-full w-2/3 card-wrapper">
<ListTable ref="listTableRef" :options="listOptions" :records="listRecords" height="400px">
<ListColumn field="Order ID" title="Order ID" width="auto" />
<ListColumn field="Customer ID" title="Customer ID" width="auto" />
<ListColumn field="Product Name" title="Product Name" width="auto" />
<ListColumn field="Category" title="Category" width="auto" />
<ListColumn field="Sub-Category" title="Sub-Category" width="auto" />
<ListColumn field="Region" title="Region" width="auto" />
<ListColumn field="City" title="City" width="auto" />
<ListColumn field="Order Date" title="Order Date" width="auto" />
<ListColumn field="Quantity" title="Quantity" width="auto" />
<ListColumn field="Sales" title="Sales" width="auto" />
<ListColumn field="Profit" title="Profit" width="auto" />
</ListTable>
</ElCard>
<ElCard header="Group Table" class="h-full w-2/3 card-wrapper">
<ListTable ref="groupTableRef" :options="groupOptions" :records="groupRecords" height="400px">
<ListColumn field="Order ID" title="Order ID" width="auto" />
<ListColumn field="Customer ID" title="Customer ID" width="auto" />
<ListColumn field="Product Name" title="Product Name" width="auto" />
<ListColumn field="Category" title="Category" width="auto" />
<ListColumn field="Sub-Category" title="Sub-Category" width="auto" />
<ListColumn field="Region" title="Region" width="auto" />
<ListColumn field="City" title="City" width="auto" />
<ListColumn field="Order Date" title="Order Date" width="auto" />
<ListColumn field="Quantity" title="Quantity" width="auto" />
<ListColumn field="Sales" title="Sales" width="auto" />
<ListColumn field="Profit" title="Profit" width="auto" />
</ListTable>
</ElCard>
<ElCard header="Pivot Table" class="h-full w-2/3 card-wrapper">
<PivotTable ref="pivotTableRef" :options="pivotTableOptions" :records="pivotTableRecords" height="400px">
<PivotColumnDimension
title="Category"
dimension-key="Category"
:header-style="{ textStick: true }"
width="auto"
/>
<PivotRowDimension
v-for="(row, index) in pivotTableRows"
:key="index"
:dimension-key="row.dimensionKey"
:title="row.title"
:header-style="row.headerStyle"
:width="row.width"
/>
<PivotIndicator
v-for="(indicator, index) in pivotTableIndicators"
:key="index"
:indicator-key="indicator.indicatorKey"
:title="indicator.title"
:width="indicator.width"
:show-sort="indicator.showSort"
:header-style="indicator.headerStyle"
:format="indicator.format"
:style="indicator.style"
/>
<PivotCorner title-on-dimension="row" />
<Menu menu-type="html" :context-menu-items="['copy', 'paste', 'delete', '...']" />
</PivotTable>
</ElCard>
<ElCard header="Pivot Chart" class="h-full w-2/3 card-wrapper">
<PivotChart
ref="pivotChartRef"
:options="pivotChartOptions"
:records="pivotChartRecords"
height="800px"
@on-legend-item-click="handleLegendItemClick"
/>
</ElCard>
<ElCard header="Custom Component" class="h-full w-2/3 card-wrapper">
<ListTable
ref="customLayoutListTableRef"
:options="customLayoutListTableOptions"
:records="customLayoutListTableRecords"
height="400px"
>
<!-- Order Number Column -->
<ListColumn field="bloggerId" title="Order Number" width="100" />
<!-- Anchor Nickname Column with Custom Layout -->
<ListColumn field="bloggerName" title="Anchor Nickname" :width="330">
<template #customLayout="{ record, height, width }">
<Group :height="height" :width="width" display="flex" flex-direction="row" flex-wrap="nowrap">
<!-- Avatar Group -->
<Group
:height="height"
:width="60"
display="flex"
flex-direction="column"
align-items="center"
justify-content="space-around"
fill="red"
:opacity="0.1"
>
<Image id="icon0" :width="50" :height="50" :image="record.bloggerAvatar" :corner-radius="25" />
</Group>
<!-- Blogger Info Group -->
<Group :height="height" :width="width - 60" display="flex" flex-direction="column" flex-wrap="nowrap">
<Group
:height="height / 2"
:width="width - 60"
display="flex"
flex-wrap="wrap"
align-items="center"
fill="orange"
:opacity="0.1"
>
<Text
:text="record.bloggerName"
:font-size="13"
font-family="sans-serif"
fill="black"
:bounds-padding="[0, 0, 0, 10]"
/>
<Image
id="location"
image="https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/location.svg"
:width="15"
:height="15"
:bounds-padding="[0, 0, 0, 10]"
cursor="pointer"
/>
<Text :text="record.city" :font-size="11" font-family="sans-serif" fill="#6f7070" />
</Group>
<!-- Tags Group -->
<Group
:height="height / 2"
:width="width - 60"
display="flex"
align-items="center"
fill="yellow"
:opacity="0.1"
>
<Tag
v-for="tag in record?.tags"
:key="tag"
:text="tag"
:text-style="{ fontSize: 10, fontFamily: 'sans-serif', fill: 'rgb(51, 101, 238)' }"
:panel="{ visible: true, fill: '#f4f4f2', cornerRadius: 5 }"
:space="5"
:bounds-padding="[0, 0, 0, 5]"
/>
</Group>
</Group>
</Group>
</template>
</ListColumn>
<!-- Other Columns -->
<ListColumn
field="fansCount"
title="Fans Count"
width="120"
:field-format="rec => rec.fansCount + 'w'"
:style="customLayoutListTableColumnStyle"
/>
<ListColumn field="worksCount" title="Works Count" :style="customLayoutListTableColumnStyle" width="135" />
<ListColumn
field="viewCount"
title="View Count"
width="120"
:field-format="rec => rec.viewCount + 'w'"
:style="customLayoutListTableColumnStyle"
/>
</ListTable>
</ElCard>
<ElCard class="h-full w-2/3 card-wrapper">
<WebSiteLink label="More VTable Demos: " link="https://www.visactor.com/vtable/example" />
</ElCard>
</ElSpace>
</div>
</template>

View File

@@ -1,44 +0,0 @@
<script lang="ts" setup>
import { onMounted, shallowRef } from 'vue';
import TypeIt from 'typeit';
import type { Options } from 'typeit';
import type { El } from 'typeit/dist/types';
defineOptions({ name: 'TypeIt' });
const textRef = shallowRef<El>();
function init() {
if (!textRef.value) return;
const options: Options = {
strings: 'CN-RDMS 是灿能电力内部使用的研发管理系统前端项目',
lifeLike: true,
speed: 120,
loop: true
};
const initTypeIt = new TypeIt(textRef.value, options);
initTypeIt.go();
}
onMounted(() => {
init();
});
</script>
<template>
<div>
<ElCard header="打字机 插件" class="h-full card-wrapper">
<ElSpace direction="vertical">
<GithubLink link="https://github.com/alexmacarthur/typeit" />
<WebSiteLink label="文档地址:" link="https://www.typeitjs.com/docs/vanilla/usage/" />
</ElSpace>
<ElDivider content-position="left">基本示例</ElDivider>
<span ref="textRef" class="text-18px"></span>
</ElCard>
</div>
</template>
<style scoped></style>

View File

@@ -1,43 +0,0 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import Player from 'xgplayer';
import 'xgplayer/dist/index.min.css';
defineOptions({ name: 'VideoComp' });
const domRef = ref<HTMLElement>();
const player = ref<Player>();
function renderXgPlayer() {
if (!domRef.value) return;
const url = 'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/byted-player-videos/1.0.0/xgplayer-demo.mp4';
player.value = new Player({
el: domRef.value,
url,
playbackRate: [0.5, 0.75, 1, 1.5, 2]
});
}
function destroyXgPlayer() {
player.value?.destroy();
}
onMounted(() => {
renderXgPlayer();
});
onUnmounted(() => {
destroyXgPlayer();
});
</script>
<template>
<div>
<ElCard header="视频播放器插件" class="h-full card-wrapper">
<div class="flex-center">
<div ref="domRef" class="h-auto w-full shadow-md"></div>
</div>
</ElCard>
</div>
</template>
<style scoped></style>

View File

@@ -1,767 +0,0 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { RDMS_OBJECT_DIRECTION_DICT_CODE } from '@/constants/dict';
import { fetchGetProduct, fetchGetProductMembers, fetchGetProductSettings } from '@/service/api';
import { useDict } from '@/hooks/business/dict';
import { useCurrentProduct } from '../shared/use-current-product';
import ProductActivityTimelinePanel from './modules/product-activity-timeline-panel.vue';
import {
buildProductHomepageBanner,
buildRequirementPoolRecentChanges,
buildRequirementPoolSummary,
getProductHomepageExtensionModules
} from './homepage';
import { productHomepageExtensionMock, productRequirementPoolMock } from './mock';
defineOptions({ name: 'ProductDashboard' });
const { currentObjectId } = useCurrentProduct();
const { getLabel: getDirectionDictLabel } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE);
const pageLoading = ref(false);
const productDetail = ref<Api.Product.Product | null>(null);
const settings = ref<Api.Product.ProductSettings | null>(null);
const members = ref<Api.Product.ProductMember[]>([]);
const latestActivityTime = ref('');
const requirementPoolSummary = computed(() => buildRequirementPoolSummary(productRequirementPoolMock.summary));
const requirementPoolRecentChanges = computed(() =>
buildRequirementPoolRecentChanges(productRequirementPoolMock.recentChanges)
);
const homepageBanner = computed(() =>
buildProductHomepageBanner({
product: productDetail.value,
settings: settings.value,
members: members.value,
requirementSummary: requirementPoolSummary.value,
latestActivityTime: latestActivityTime.value
})
);
const extensionModules = computed(() => getProductHomepageExtensionModules(productHomepageExtensionMock));
const directionLabel = computed(() => getDirectionDictLabel(homepageBanner.value.identity.directionCode, '--'));
const bannerFacts = computed(() => {
const [managerFact, roleFact] = homepageBanner.value.identity.facts;
return [
{
label: '产品方向',
value: directionLabel.value,
fullWidth: false
},
{
label: managerFact?.label || '产品经理',
value: managerFact?.value || '--',
fullWidth: false
},
{
label: roleFact?.label || '角色摘要',
value: roleFact?.value || '--',
fullWidth: true
}
];
});
const bannerStatusClass = computed(() => {
const statusCode = homepageBanner.value.identity.statusCode;
return statusCode ? `product-homepage-banner--${statusCode}` : 'product-homepage-banner--default';
});
const bannerStatusWordClass = computed(() => {
const statusCode = homepageBanner.value.identity.statusCode;
return statusCode
? `product-homepage-banner__status-word--${statusCode}`
: 'product-homepage-banner__status-word--default';
});
function handleLatestActivityTimeChange(value: string) {
latestActivityTime.value = value;
}
async function loadDashboardData(objectId: string) {
pageLoading.value = true;
try {
const [productResult, settingsResult, membersResult] = await Promise.all([
fetchGetProduct(objectId),
fetchGetProductSettings(objectId),
fetchGetProductMembers(objectId)
]);
productDetail.value = productResult.error ? null : productResult.data || null;
settings.value = settingsResult.error ? null : settingsResult.data || null;
members.value = membersResult.error ? [] : membersResult.data || [];
} finally {
pageLoading.value = false;
}
}
watch(
() => currentObjectId.value,
async objectId => {
if (!objectId) {
productDetail.value = null;
settings.value = null;
members.value = [];
latestActivityTime.value = '';
return;
}
await loadDashboardData(objectId);
},
{ immediate: true }
);
</script>
<template>
<div v-loading="pageLoading" class="product-homepage">
<section class="product-homepage-banner" :class="bannerStatusClass">
<div class="product-homepage-banner__identity">
<div class="product-homepage-banner__title-group">
<div class="product-homepage-banner__title-main min-w-0">
<div class="product-homepage-banner__title-row">
<h1 class="product-homepage-banner__title">{{ homepageBanner.identity.name }}</h1>
<span class="product-homepage-banner__status-word" :class="bannerStatusWordClass">
{{ homepageBanner.identity.statusLabel }}
</span>
</div>
<div class="product-homepage-banner__subtitle">
<span class="product-homepage-banner__code">编号 {{ homepageBanner.identity.code }}</span>
<p v-if="homepageBanner.identity.description" class="product-homepage-banner__description">
{{ homepageBanner.identity.description }}
</p>
</div>
</div>
</div>
<div class="product-homepage-banner__facts">
<div
v-for="item in bannerFacts"
:key="item.label"
class="product-homepage-banner__fact"
:class="{ 'product-homepage-banner__fact--full': item.fullWidth }"
>
<span class="product-homepage-banner__fact-label">{{ item.label }}</span>
<strong class="product-homepage-banner__fact-value">{{ item.value }}</strong>
</div>
</div>
</div>
<div class="product-homepage-banner__metrics">
<article v-for="item in homepageBanner.metrics" :key="item.label" class="product-homepage-banner__metric">
<span class="product-homepage-banner__metric-label">{{ item.label }}</span>
<strong class="product-homepage-banner__metric-value">{{ item.value }}</strong>
</article>
</div>
</section>
<section class="product-homepage-main">
<ProductActivityTimelinePanel
:product-id="currentObjectId || ''"
@latest-time-change="handleLatestActivityTimeChange"
/>
<div class="product-homepage-main__aside">
<ElCard class="product-homepage-panel card-wrapper">
<template #header>
<div>
<h3 class="product-homepage-panel__title">需求池管理概览</h3>
<p class="product-homepage-panel__desc">先看需求池现在的总体规模状态结构和待处理压力</p>
</div>
</template>
<div class="product-homepage-requirement-summary">
<div class="product-homepage-requirement-summary__metrics">
<article
v-for="item in requirementPoolSummary.metrics"
:key="item.label"
class="product-homepage-requirement-summary__metric"
>
<span class="product-homepage-requirement-summary__metric-label">{{ item.label }}</span>
<strong class="product-homepage-requirement-summary__metric-value">{{ item.value }}</strong>
</article>
</div>
<div class="product-homepage-requirement-summary__distribution">
<div
v-for="item in requirementPoolSummary.distribution"
:key="item.label"
class="product-homepage-requirement-summary__distribution-item"
>
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
</div>
</div>
</div>
</ElCard>
<ElCard class="product-homepage-panel card-wrapper">
<template #header>
<div>
<h3 class="product-homepage-panel__title">需求池最近变化</h3>
<p class="product-homepage-panel__desc">承接需求新增状态流转和关闭情况和产品动态时间线分开表达</p>
</div>
</template>
<div v-if="requirementPoolRecentChanges.length" class="product-homepage-requirement-changes">
<article
v-for="item in requirementPoolRecentChanges"
:key="item.id"
class="product-homepage-requirement-changes__item"
>
<div class="product-homepage-requirement-changes__meta">
<ElTag type="info" effect="plain" size="small">{{ item.actionLabel }}</ElTag>
<span class="product-homepage-requirement-changes__time">{{ item.time }}</span>
</div>
<strong class="product-homepage-requirement-changes__title">{{ item.title }}</strong>
<p class="product-homepage-requirement-changes__status">当前状态{{ item.statusLabel }}</p>
</article>
</div>
<ElEmpty v-else description="当前暂无需求池最近变化" :image-size="72" />
</ElCard>
</div>
</section>
<section class="product-homepage-extension">
<ElCard v-for="module in extensionModules" :key="module.key" class="product-homepage-panel card-wrapper">
<template #header>
<div>
<h3 class="product-homepage-panel__title">{{ module.title }}</h3>
<p class="product-homepage-panel__desc">{{ module.description }}</p>
</div>
</template>
<div class="product-homepage-extension__list">
<div v-for="item in module.items" :key="item" class="product-homepage-extension__item">
<span class="product-homepage-extension__dot" />
<span>{{ item }}</span>
</div>
</div>
</ElCard>
</section>
</div>
</template>
<style scoped>
.product-homepage {
display: flex;
flex-direction: column;
gap: 16px;
}
.product-homepage-banner {
display: grid;
grid-template-columns: minmax(0, 1.55fr) minmax(320px, 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%));
}
.product-homepage-banner--default {
border-color: rgb(226 232 240 / 92%);
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%));
}
.product-homepage-banner--active {
border-color: rgb(167 243 208 / 88%);
background:
radial-gradient(circle at top left, rgb(5 150 105 / 16%), transparent 32%),
radial-gradient(circle at bottom right, rgb(16 185 129 / 14%), transparent 26%),
linear-gradient(135deg, rgb(236 253 245 / 99%), rgb(255 255 255 / 98%));
}
.product-homepage-banner--paused {
border-color: rgb(253 230 138 / 90%);
background:
radial-gradient(circle at top left, rgb(245 158 11 / 18%), transparent 32%),
radial-gradient(circle at bottom right, rgb(251 191 36 / 16%), transparent 24%),
linear-gradient(135deg, rgb(255 251 235 / 99%), rgb(255 255 255 / 98%));
}
.product-homepage-banner--archived {
border-color: rgb(203 213 225 / 92%);
background:
radial-gradient(circle at top left, rgb(100 116 139 / 14%), transparent 34%),
radial-gradient(circle at bottom right, rgb(148 163 184 / 10%), transparent 26%),
linear-gradient(135deg, rgb(248 250 252 / 99%), rgb(255 255 255 / 98%));
}
.product-homepage-banner--abandoned {
border-color: rgb(254 205 211 / 92%);
background:
radial-gradient(circle at top left, rgb(244 63 94 / 16%), transparent 32%),
radial-gradient(circle at bottom right, rgb(251 113 133 / 14%), transparent 24%),
linear-gradient(135deg, rgb(255 241 242 / 99%), rgb(255 255 255 / 98%));
}
.product-homepage-banner__identity {
display: flex;
min-width: 0;
flex-direction: column;
gap: 16px;
}
.product-homepage-banner__title-group {
display: flex;
align-items: flex-start;
gap: 12px;
}
.product-homepage-banner__title-main {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
gap: 12px;
}
.product-homepage-banner__title-row {
display: flex;
min-width: 0;
align-items: baseline;
gap: 14px;
}
.product-homepage-banner__code {
margin: 0;
color: rgb(14 116 144 / 92%);
font-size: 13px;
font-weight: 600;
white-space: nowrap;
}
.product-homepage-banner__title {
margin: 0;
color: rgb(15 23 42 / 98%);
font-size: 34px;
line-height: 1.15;
letter-spacing: -0.03em;
}
.product-homepage-banner__subtitle {
display: flex;
min-width: 0;
flex-wrap: wrap;
align-items: baseline;
gap: 10px 14px;
}
.product-homepage-banner__description {
margin: 0;
min-width: 0;
color: rgb(71 85 105 / 94%);
font-size: 14px;
line-height: 1.8;
}
.product-homepage-banner__status-word {
flex-shrink: 0;
font-size: 26px;
font-weight: 800;
line-height: 1;
letter-spacing: 0.18em;
text-transform: uppercase;
user-select: none;
}
.product-homepage-banner__status-word--default {
color: rgb(148 163 184 / 48%);
}
.product-homepage-banner__status-word--active {
color: transparent;
background: linear-gradient(180deg, rgb(5 150 105 / 94%), rgb(16 185 129 / 70%));
background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 10px 24px rgb(5 150 105 / 16%);
}
.product-homepage-banner__status-word--paused {
color: transparent;
background: linear-gradient(180deg, rgb(217 119 6 / 94%), rgb(245 158 11 / 70%));
background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 10px 24px rgb(245 158 11 / 16%);
}
.product-homepage-banner__status-word--archived {
color: transparent;
background: linear-gradient(180deg, rgb(71 85 105 / 92%), rgb(148 163 184 / 64%));
background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 10px 24px rgb(100 116 139 / 14%);
}
.product-homepage-banner__status-word--abandoned {
color: transparent;
background: linear-gradient(180deg, rgb(225 29 72 / 94%), rgb(251 113 133 / 68%));
background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 10px 24px rgb(225 29 72 / 16%);
}
.product-homepage-banner__facts {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.product-homepage-banner__fact {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-height: 58px;
padding: 14px 16px;
border: 1px solid rgb(226 232 240 / 88%);
border-radius: 18px;
background-color: rgb(255 255 255 / 78%);
}
.product-homepage-banner__fact--full {
grid-column: 1 / -1;
align-items: flex-start;
}
.product-homepage-banner__fact-label {
color: rgb(100 116 139 / 94%);
font-size: 13px;
white-space: nowrap;
}
.product-homepage-banner__fact-value {
color: rgb(15 23 42 / 96%);
font-size: 15px;
line-height: 1.6;
text-align: right;
}
.product-homepage-banner__fact--full .product-homepage-banner__fact-value {
max-width: 72%;
text-align: left;
}
.product-homepage-banner__metrics {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.product-homepage-banner__metric {
display: flex;
min-height: 112px;
flex-direction: column;
justify-content: center;
gap: 16px;
padding: 18px;
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%));
}
.product-homepage-banner__metric-label {
color: rgb(100 116 139 / 92%);
font-size: 12px;
}
.product-homepage-banner__metric-value {
color: rgb(15 23 42 / 98%);
font-size: 28px;
line-height: 1.1;
letter-spacing: -0.02em;
word-break: break-word;
}
.product-homepage-main {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.product-homepage-main__aside {
display: flex;
min-width: 0;
flex-direction: column;
gap: 16px;
}
.product-homepage-panel {
overflow: hidden;
}
.product-homepage-panel__title {
margin: 0;
color: rgb(15 23 42 / 98%);
font-size: 16px;
font-weight: 700;
}
.product-homepage-panel__desc {
margin: 4px 0 0;
color: rgb(100 116 139 / 92%);
font-size: 13px;
line-height: 1.7;
}
.product-homepage-timeline {
display: flex;
flex-direction: column;
gap: 10px;
}
.product-homepage-timeline__item {
display: grid;
grid-template-columns: 20px minmax(0, 1fr);
gap: 12px;
}
.product-homepage-timeline__rail {
display: flex;
flex-direction: column;
align-items: center;
}
.product-homepage-timeline__dot {
width: 12px;
height: 12px;
border-radius: 999px;
margin-top: 6px;
box-shadow: 0 0 0 4px rgb(255 255 255 / 96%);
}
.product-homepage-timeline__dot--sky {
background-color: rgb(14 165 233 / 88%);
}
.product-homepage-timeline__dot--emerald {
background-color: rgb(5 150 105 / 88%);
}
.product-homepage-timeline__dot--amber {
background-color: rgb(217 119 6 / 88%);
}
.product-homepage-timeline__dot--rose {
background-color: rgb(225 29 72 / 88%);
}
.product-homepage-timeline__dot--slate {
background-color: rgb(100 116 139 / 88%);
}
.product-homepage-timeline__line {
flex: 1;
width: 2px;
min-height: 30px;
margin-top: 4px;
background: linear-gradient(180deg, rgb(203 213 225 / 96%), rgb(226 232 240 / 28%));
}
.product-homepage-timeline__item:last-child .product-homepage-timeline__line {
opacity: 0;
}
.product-homepage-timeline__content {
padding: 12px 14px;
border: 1px solid rgb(226 232 240 / 92%);
border-radius: 16px;
background-color: rgb(255 255 255 / 98%);
}
.product-homepage-timeline__meta {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.product-homepage-timeline__time {
color: rgb(100 116 139 / 90%);
font-size: 12px;
}
.product-homepage-timeline__sentence {
margin: 6px 0 0;
color: rgb(71 85 105 / 94%);
font-size: 13px;
line-height: 1.65;
}
.product-homepage-timeline__headline {
margin-right: 6px;
color: rgb(15 23 42 / 98%);
font-weight: 600;
}
.product-homepage-requirement-summary {
display: flex;
flex-direction: column;
gap: 16px;
}
.product-homepage-requirement-summary__metrics {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.product-homepage-requirement-summary__metric {
display: flex;
min-height: 100px;
flex-direction: column;
justify-content: center;
gap: 14px;
padding: 14px 16px;
border-radius: 18px;
background: linear-gradient(180deg, rgb(248 250 252 / 98%), rgb(241 245 249 / 94%));
}
.product-homepage-requirement-summary__metric-label {
color: rgb(100 116 139 / 92%);
font-size: 12px;
}
.product-homepage-requirement-summary__metric-value {
color: rgb(15 23 42 / 98%);
font-size: 24px;
line-height: 1.1;
}
.product-homepage-requirement-summary__distribution {
display: flex;
flex-direction: column;
gap: 10px;
}
.product-homepage-requirement-summary__distribution-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 13px 14px;
border: 1px solid rgb(226 232 240 / 88%);
border-radius: 14px;
background-color: rgb(255 255 255 / 96%);
color: rgb(51 65 85 / 95%);
font-size: 14px;
}
.product-homepage-requirement-summary__distribution-item strong {
color: rgb(15 23 42 / 98%);
font-size: 18px;
}
.product-homepage-requirement-changes {
display: flex;
flex-direction: column;
gap: 12px;
}
.product-homepage-requirement-changes__item {
padding: 14px 16px;
border: 1px solid rgb(226 232 240 / 90%);
border-radius: 18px;
background-color: rgb(255 255 255 / 98%);
}
.product-homepage-requirement-changes__meta {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.product-homepage-requirement-changes__time {
color: rgb(100 116 139 / 90%);
font-size: 12px;
}
.product-homepage-requirement-changes__title {
display: block;
margin-top: 10px;
color: rgb(15 23 42 / 98%);
font-size: 15px;
line-height: 1.65;
}
.product-homepage-requirement-changes__status {
margin: 8px 0 0;
color: rgb(71 85 105 / 94%);
font-size: 13px;
line-height: 1.7;
}
.product-homepage-extension {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
.product-homepage-extension__list {
display: flex;
flex-direction: column;
gap: 10px;
}
.product-homepage-extension__item {
display: flex;
align-items: center;
gap: 10px;
padding: 13px 14px;
border-radius: 16px;
background-color: rgb(248 250 252 / 96%);
color: rgb(51 65 85 / 95%);
font-size: 14px;
line-height: 1.7;
}
.product-homepage-extension__dot {
width: 8px;
height: 8px;
border-radius: 999px;
background-color: rgb(14 116 144 / 88%);
flex-shrink: 0;
}
@media (width <= 1280px) {
.product-homepage-banner,
.product-homepage-main,
.product-homepage-extension {
grid-template-columns: 1fr;
}
}
@media (width <= 768px) {
.product-homepage-banner {
padding: 18px;
}
.product-homepage-banner__title-row {
flex-wrap: wrap;
}
.product-homepage-banner__title {
font-size: 28px;
}
.product-homepage-banner__status-word {
font-size: 22px;
}
.product-homepage-banner__facts,
.product-homepage-banner__metrics,
.product-homepage-requirement-summary__metrics {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -1,785 +0,0 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { RDMS_OBJECT_DIRECTION_DICT_CODE, RDMS_PROJECT_TYPE_DICT_CODE } from '@/constants/dict';
import { fetchGetProject, fetchGetProjectMembers, fetchGetProjectSettings } from '@/service/api';
import { useDict } from '@/hooks/business/dict';
import { useCurrentProject } from '../../shared/use-current-project';
import {
buildProjectHomepageBanner,
buildProjectHomepageTimeline,
buildProjectScheduleOverview,
buildProjectTeamOverview,
getProjectHomepageExtensionModules
} from './homepage';
import { projectHomepageExtensionMock } from './mock';
defineOptions({ name: 'ProjectOverview' });
const { currentObjectId, currentProject } = useCurrentProject();
const { getLabel: getDirectionLabel } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE);
const { getLabel: getProjectTypeLabel } = useDict(RDMS_PROJECT_TYPE_DICT_CODE);
const pageLoading = ref(false);
const projectDetail = ref<Api.Project.Project | null>(null);
const settings = ref<Api.Project.ProjectSettings | null>(null);
const members = ref<Api.Project.ProjectMember[]>([]);
const latestActivityTime = ref('');
const timelineItems = computed(() => buildProjectHomepageTimeline(projectDetail.value, settings.value, members.value));
const scheduleOverview = computed(() => buildProjectScheduleOverview(projectDetail.value));
const teamOverview = computed(() => buildProjectTeamOverview(members.value));
const homepageBanner = computed(() =>
buildProjectHomepageBanner({
project: projectDetail.value,
settings: settings.value,
members: members.value,
latestActivityTime: latestActivityTime.value
})
);
const extensionModules = computed(() => getProjectHomepageExtensionModules(projectHomepageExtensionMock));
const directionLabel = computed(() => getDirectionLabel(homepageBanner.value.identity.directionCode, '--'));
const projectTypeLabel = computed(() => getProjectTypeLabel(homepageBanner.value.identity.projectType, '--'));
const bannerFacts = computed(() => [
{
label: '项目方向',
value: directionLabel.value,
fullWidth: false
},
{
label: '项目类型',
value: projectTypeLabel.value,
fullWidth: false
},
...homepageBanner.value.identity.facts
]);
const progressValue = computed(() => projectDetail.value?.progressRate ?? 0);
const bannerStatusClass = computed(() => {
const statusCode = homepageBanner.value.identity.statusCode;
return statusCode ? `project-homepage-banner--${statusCode}` : 'project-homepage-banner--default';
});
const bannerStatusWordClass = computed(() => {
const statusCode = homepageBanner.value.identity.statusCode;
return statusCode
? `project-homepage-banner__status-word--${statusCode}`
: 'project-homepage-banner__status-word--default';
});
async function loadOverviewData(objectId: string) {
pageLoading.value = true;
try {
const [projectResult, settingsResult, membersResult] = await Promise.all([
fetchGetProject(objectId),
fetchGetProjectSettings(objectId),
fetchGetProjectMembers(objectId)
]);
projectDetail.value = projectResult.error ? null : projectResult.data || null;
settings.value = settingsResult.error ? null : settingsResult.data || null;
members.value = membersResult.error ? [] : membersResult.data || [];
latestActivityTime.value = timelineItems.value[0]?.time || '';
} finally {
pageLoading.value = false;
}
}
watch(
() => currentObjectId.value,
async objectId => {
if (!objectId) {
projectDetail.value = null;
settings.value = null;
members.value = [];
latestActivityTime.value = '';
return;
}
await loadOverviewData(objectId);
},
{ immediate: true }
);
</script>
<template>
<div v-loading="pageLoading" class="project-homepage">
<section class="project-homepage-banner" :class="bannerStatusClass">
<div class="project-homepage-banner__identity">
<div class="project-homepage-banner__title-group">
<div class="project-homepage-banner__title-main min-w-0">
<div class="project-homepage-banner__title-row">
<h1 class="project-homepage-banner__title">
{{ homepageBanner.identity.name || currentProject?.projectName || '--' }}
</h1>
<span class="project-homepage-banner__status-word" :class="bannerStatusWordClass">
{{ homepageBanner.identity.statusLabel }}
</span>
</div>
<div class="project-homepage-banner__subtitle">
<span class="project-homepage-banner__code">编号 {{ homepageBanner.identity.code }}</span>
<p v-if="homepageBanner.identity.description" class="project-homepage-banner__description">
{{ homepageBanner.identity.description }}
</p>
</div>
</div>
</div>
<div class="project-homepage-banner__facts">
<div
v-for="item in bannerFacts"
:key="item.label"
class="project-homepage-banner__fact"
:class="{ 'project-homepage-banner__fact--full': item.fullWidth }"
>
<span class="project-homepage-banner__fact-label">{{ item.label }}</span>
<strong class="project-homepage-banner__fact-value">{{ item.value }}</strong>
</div>
</div>
</div>
<div class="project-homepage-banner__metrics">
<article v-for="item in homepageBanner.metrics" :key="item.label" class="project-homepage-banner__metric">
<span class="project-homepage-banner__metric-label">{{ item.label }}</span>
<strong class="project-homepage-banner__metric-value">{{ item.value }}</strong>
</article>
</div>
</section>
<section class="project-homepage-main">
<ElCard class="project-homepage-panel card-wrapper">
<template #header>
<div>
<h3 class="project-homepage-panel__title">项目动态时间线</h3>
<p class="project-homepage-panel__desc">
先展示项目创建状态动作实际日期和团队变化后续可替换为专用动态接口
</p>
</div>
</template>
<div v-if="timelineItems.length" class="project-homepage-timeline">
<article v-for="item in timelineItems" :key="item.key" class="project-homepage-timeline__item">
<div class="project-homepage-timeline__rail">
<span class="project-homepage-timeline__dot" :class="`project-homepage-timeline__dot--${item.tone}`" />
<span class="project-homepage-timeline__line" />
</div>
<div class="project-homepage-timeline__content">
<div class="project-homepage-timeline__meta">
<ElTag effect="plain" size="small">{{ item.tag }}</ElTag>
<span class="project-homepage-timeline__time">{{ item.time }}</span>
</div>
<p class="project-homepage-timeline__sentence">
<strong class="project-homepage-timeline__headline">{{ item.title }}</strong>
<span>{{ item.content }}</span>
</p>
</div>
</article>
</div>
<ElEmpty v-else description="当前暂无可展示的项目动态" :image-size="88" />
</ElCard>
<div class="project-homepage-main__aside">
<ElCard class="project-homepage-panel card-wrapper">
<template #header>
<div>
<h3 class="project-homepage-panel__title">计划进展概览</h3>
<p class="project-homepage-panel__desc">先看当前进度计划周期和实际执行日期是否已经闭环</p>
</div>
</template>
<div class="project-homepage-schedule">
<div class="project-homepage-schedule__progress">
<strong>{{ progressValue }}%</strong>
<ElProgress
:percentage="progressValue"
:stroke-width="8"
:show-text="false"
:color="progressValue >= 100 ? '#10b981' : progressValue >= 50 ? '#3b82f6' : '#6366f1'"
/>
</div>
<div class="project-homepage-summary-metrics">
<article
v-for="item in scheduleOverview.metrics"
:key="item.label"
class="project-homepage-summary-metrics__item"
>
<span class="project-homepage-summary-metrics__label">{{ item.label }}</span>
<strong class="project-homepage-summary-metrics__value">{{ item.value }}</strong>
</article>
</div>
<div class="project-homepage-schedule__dates">
<div v-for="item in scheduleOverview.dates" :key="item.label" class="project-homepage-schedule__date">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
</div>
</div>
</div>
</ElCard>
<ElCard class="project-homepage-panel card-wrapper">
<template #header>
<div>
<h3 class="project-homepage-panel__title">项目团队概览</h3>
<p class="project-homepage-panel__desc">承接当前成员规模负责人和角色结构和设置页团队维护分开表达</p>
</div>
</template>
<div class="project-homepage-team">
<div class="project-homepage-summary-metrics">
<article
v-for="item in teamOverview.metrics"
:key="item.label"
class="project-homepage-summary-metrics__item"
>
<span class="project-homepage-summary-metrics__label">{{ item.label }}</span>
<strong class="project-homepage-summary-metrics__value">{{ item.value }}</strong>
</article>
</div>
<div v-if="teamOverview.roles.length" class="project-homepage-team__roles">
<div v-for="item in teamOverview.roles" :key="item.label" class="project-homepage-team__role">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
</div>
</div>
<ElEmpty v-else description="当前暂无有效团队成员" :image-size="72" />
</div>
</ElCard>
</div>
</section>
<section class="project-homepage-extension">
<ElCard v-for="module in extensionModules" :key="module.key" class="project-homepage-panel card-wrapper">
<template #header>
<div>
<h3 class="project-homepage-panel__title">{{ module.title }}</h3>
<p class="project-homepage-panel__desc">{{ module.description }}</p>
</div>
</template>
<div class="project-homepage-extension__list">
<div v-for="item in module.items" :key="item" class="project-homepage-extension__item">
<span class="project-homepage-extension__dot" />
<span>{{ item }}</span>
</div>
</div>
</ElCard>
</section>
</div>
</template>
<style scoped>
.project-homepage {
display: flex;
flex-direction: column;
gap: 16px;
}
.project-homepage-banner {
display: grid;
grid-template-columns: minmax(0, 1.55fr) minmax(320px, 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%));
}
.project-homepage-banner--default {
border-color: rgb(226 232 240 / 92%);
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%));
}
.project-homepage-banner--pending,
.project-homepage-banner--archived {
border-color: rgb(203 213 225 / 92%);
background:
radial-gradient(circle at top left, rgb(100 116 139 / 14%), transparent 34%),
radial-gradient(circle at bottom right, rgb(148 163 184 / 10%), transparent 26%),
linear-gradient(135deg, rgb(248 250 252 / 99%), rgb(255 255 255 / 98%));
}
.project-homepage-banner--active,
.project-homepage-banner--completed {
border-color: rgb(167 243 208 / 88%);
background:
radial-gradient(circle at top left, rgb(5 150 105 / 16%), transparent 32%),
radial-gradient(circle at bottom right, rgb(16 185 129 / 14%), transparent 26%),
linear-gradient(135deg, rgb(236 253 245 / 99%), rgb(255 255 255 / 98%));
}
.project-homepage-banner--paused {
border-color: rgb(253 230 138 / 90%);
background:
radial-gradient(circle at top left, rgb(245 158 11 / 18%), transparent 32%),
radial-gradient(circle at bottom right, rgb(251 191 36 / 16%), transparent 24%),
linear-gradient(135deg, rgb(255 251 235 / 99%), rgb(255 255 255 / 98%));
}
.project-homepage-banner--cancelled {
border-color: rgb(254 205 211 / 92%);
background:
radial-gradient(circle at top left, rgb(244 63 94 / 16%), transparent 32%),
radial-gradient(circle at bottom right, rgb(251 113 133 / 14%), transparent 24%),
linear-gradient(135deg, rgb(255 241 242 / 99%), rgb(255 255 255 / 98%));
}
.project-homepage-banner__identity {
display: flex;
min-width: 0;
flex-direction: column;
gap: 16px;
}
.project-homepage-banner__title-group {
display: flex;
align-items: flex-start;
gap: 12px;
}
.project-homepage-banner__title-main {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
gap: 12px;
}
.project-homepage-banner__title-row {
display: flex;
min-width: 0;
align-items: baseline;
gap: 14px;
}
.project-homepage-banner__code {
margin: 0;
color: rgb(14 116 144 / 92%);
font-size: 13px;
font-weight: 600;
white-space: nowrap;
}
.project-homepage-banner__title {
margin: 0;
color: rgb(15 23 42 / 98%);
font-size: 34px;
line-height: 1.15;
letter-spacing: 0;
}
.project-homepage-banner__subtitle {
display: flex;
min-width: 0;
flex-wrap: wrap;
align-items: baseline;
gap: 10px 14px;
}
.project-homepage-banner__description {
margin: 0;
min-width: 0;
color: rgb(71 85 105 / 94%);
font-size: 14px;
line-height: 1.8;
}
.project-homepage-banner__status-word {
flex-shrink: 0;
font-size: 26px;
font-weight: 800;
line-height: 1;
letter-spacing: 0.18em;
text-transform: uppercase;
user-select: none;
}
.project-homepage-banner__status-word--default {
color: rgb(148 163 184 / 48%);
}
.project-homepage-banner__status-word--pending,
.project-homepage-banner__status-word--archived {
color: transparent;
background: linear-gradient(180deg, rgb(71 85 105 / 92%), rgb(148 163 184 / 64%));
background-clip: text;
text-shadow: 0 10px 24px rgb(100 116 139 / 14%);
-webkit-text-fill-color: transparent;
}
.project-homepage-banner__status-word--active,
.project-homepage-banner__status-word--completed {
color: transparent;
background: linear-gradient(180deg, rgb(5 150 105 / 94%), rgb(16 185 129 / 70%));
background-clip: text;
text-shadow: 0 10px 24px rgb(5 150 105 / 16%);
-webkit-text-fill-color: transparent;
}
.project-homepage-banner__status-word--paused {
color: transparent;
background: linear-gradient(180deg, rgb(217 119 6 / 94%), rgb(245 158 11 / 70%));
background-clip: text;
text-shadow: 0 10px 24px rgb(245 158 11 / 16%);
-webkit-text-fill-color: transparent;
}
.project-homepage-banner__status-word--cancelled {
color: transparent;
background: linear-gradient(180deg, rgb(244 63 94 / 94%), rgb(251 113 133 / 68%));
background-clip: text;
text-shadow: 0 10px 24px rgb(244 63 94 / 16%);
-webkit-text-fill-color: transparent;
}
.project-homepage-banner__facts {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.project-homepage-banner__fact {
display: flex;
min-height: 58px;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 14px 16px;
border: 1px solid rgb(226 232 240 / 88%);
border-radius: 18px;
background-color: rgb(255 255 255 / 78%);
}
.project-homepage-banner__fact--full {
grid-column: 1 / -1;
align-items: flex-start;
}
.project-homepage-banner__fact-label {
color: rgb(100 116 139 / 94%);
font-size: 13px;
white-space: nowrap;
}
.project-homepage-banner__fact-value {
color: rgb(15 23 42 / 96%);
font-size: 15px;
line-height: 1.6;
text-align: right;
}
.project-homepage-banner__fact--full .project-homepage-banner__fact-value {
max-width: 72%;
text-align: left;
}
.project-homepage-banner__metrics {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.project-homepage-banner__metric {
display: flex;
min-height: 112px;
flex-direction: column;
justify-content: center;
gap: 16px;
padding: 18px;
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%));
}
.project-homepage-banner__metric-label {
color: rgb(100 116 139 / 92%);
font-size: 12px;
}
.project-homepage-banner__metric-value {
color: rgb(15 23 42 / 98%);
font-size: 28px;
line-height: 1.1;
letter-spacing: 0;
word-break: break-word;
}
.project-homepage-main {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.project-homepage-main__aside {
display: flex;
min-width: 0;
flex-direction: column;
gap: 16px;
}
.project-homepage-panel {
overflow: hidden;
}
.project-homepage-panel__title {
margin: 0;
color: rgb(15 23 42 / 98%);
font-size: 16px;
font-weight: 700;
}
.project-homepage-panel__desc {
margin: 4px 0 0;
color: rgb(100 116 139 / 92%);
font-size: 13px;
line-height: 1.7;
}
.project-homepage-timeline {
display: flex;
flex-direction: column;
gap: 10px;
}
.project-homepage-timeline__item {
display: grid;
grid-template-columns: 20px minmax(0, 1fr);
gap: 12px;
}
.project-homepage-timeline__rail {
display: flex;
flex-direction: column;
align-items: center;
}
.project-homepage-timeline__dot {
width: 12px;
height: 12px;
margin-top: 6px;
border-radius: 999px;
box-shadow: 0 0 0 4px rgb(255 255 255 / 96%);
}
.project-homepage-timeline__dot--sky {
background-color: rgb(14 165 233 / 88%);
}
.project-homepage-timeline__dot--emerald {
background-color: rgb(5 150 105 / 88%);
}
.project-homepage-timeline__dot--amber {
background-color: rgb(217 119 6 / 88%);
}
.project-homepage-timeline__dot--rose {
background-color: rgb(225 29 72 / 88%);
}
.project-homepage-timeline__dot--slate {
background-color: rgb(100 116 139 / 88%);
}
.project-homepage-timeline__line {
flex: 1;
width: 2px;
min-height: 30px;
margin-top: 4px;
background: linear-gradient(180deg, rgb(203 213 225 / 96%), rgb(226 232 240 / 28%));
}
.project-homepage-timeline__item:last-child .project-homepage-timeline__line {
opacity: 0;
}
.project-homepage-timeline__content {
padding: 12px 14px;
border: 1px solid rgb(226 232 240 / 92%);
border-radius: 16px;
background-color: rgb(255 255 255 / 98%);
}
.project-homepage-timeline__meta {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.project-homepage-timeline__time {
color: rgb(100 116 139 / 90%);
font-size: 12px;
}
.project-homepage-timeline__sentence {
margin: 6px 0 0;
color: rgb(71 85 105 / 94%);
font-size: 13px;
line-height: 1.65;
}
.project-homepage-timeline__headline {
margin-right: 6px;
color: rgb(15 23 42 / 98%);
font-weight: 600;
}
.project-homepage-schedule,
.project-homepage-team {
display: flex;
flex-direction: column;
gap: 16px;
}
.project-homepage-schedule__progress {
display: flex;
flex-direction: column;
gap: 12px;
padding: 18px;
border-radius: 18px;
background: linear-gradient(180deg, rgb(248 250 252 / 98%), rgb(241 245 249 / 94%));
}
.project-homepage-schedule__progress strong {
color: rgb(15 23 42 / 98%);
font-size: 36px;
line-height: 1.1;
}
.project-homepage-summary-metrics {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.project-homepage-summary-metrics__item {
display: flex;
min-height: 100px;
flex-direction: column;
justify-content: center;
gap: 14px;
padding: 14px 16px;
border-radius: 18px;
background: linear-gradient(180deg, rgb(248 250 252 / 98%), rgb(241 245 249 / 94%));
}
.project-homepage-summary-metrics__label {
color: rgb(100 116 139 / 92%);
font-size: 12px;
}
.project-homepage-summary-metrics__value {
color: rgb(15 23 42 / 98%);
font-size: 22px;
line-height: 1.2;
word-break: break-word;
}
.project-homepage-schedule__dates,
.project-homepage-team__roles {
display: flex;
flex-direction: column;
gap: 10px;
}
.project-homepage-schedule__date,
.project-homepage-team__role {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 13px 14px;
border: 1px solid rgb(226 232 240 / 88%);
border-radius: 14px;
background-color: rgb(255 255 255 / 96%);
color: rgb(51 65 85 / 95%);
font-size: 14px;
}
.project-homepage-schedule__date strong,
.project-homepage-team__role strong {
color: rgb(15 23 42 / 98%);
font-size: 18px;
}
.project-homepage-extension {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
.project-homepage-extension__list {
display: flex;
flex-direction: column;
gap: 10px;
}
.project-homepage-extension__item {
display: flex;
align-items: center;
gap: 10px;
padding: 13px 14px;
border-radius: 16px;
background-color: rgb(248 250 252 / 96%);
color: rgb(51 65 85 / 95%);
font-size: 14px;
line-height: 1.7;
}
.project-homepage-extension__dot {
width: 8px;
height: 8px;
flex-shrink: 0;
border-radius: 999px;
background-color: rgb(14 116 144 / 88%);
}
@media (width <= 1280px) {
.project-homepage-banner,
.project-homepage-main,
.project-homepage-extension {
grid-template-columns: 1fr;
}
}
@media (width <= 768px) {
.project-homepage-banner {
padding: 18px;
}
.project-homepage-banner__title-row {
flex-wrap: wrap;
}
.project-homepage-banner__title {
font-size: 28px;
}
.project-homepage-banner__status-word {
font-size: 22px;
}
.project-homepage-banner__facts,
.project-homepage-banner__metrics,
.project-homepage-summary-metrics {
grid-template-columns: 1fr;
}
.project-homepage-banner__fact--full .project-homepage-banner__fact-value {
max-width: none;
}
}
</style>