ss-index-components.js 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263
  1. import { isNum, toStyleStr } from "./tools.js";
  2. import { eventBus, EVEN_VAR } from "./EventBus.js";
  3. // import { debounce } from "../lib/tools.js";
  4. // 首页组件的名字
  5. const winName = {
  6. launch: "launch",
  7. Notice: "Notice",
  8. Statistics: "Statistics",
  9. TodoList: "TodoList",
  10. UrgingList: "UrgingList",
  11. UserInfo: "UserInfo",
  12. };
  13. // 尺寸窗口名字
  14. const size2Win = (name) => `${name}-${document.body.clientWidth}`;
  15. // 加载尺寸
  16. const loadWinSize = function (name) {
  17. const val = localStorage.getItem(size2Win(name));
  18. if (val) {
  19. try {
  20. return JSON.parse(val) || {};
  21. } catch (err) {
  22. return {};
  23. }
  24. } else {
  25. return {};
  26. }
  27. };
  28. const saveWinSize = function (name, obj) {
  29. localStorage.setItem(size2Win(name), JSON.stringify(obj));
  30. };
  31. // 全局头部
  32. export const GlobalHeader = {
  33. name: 'GlobalHeader',
  34. props: {
  35. menuData: {
  36. type: Array,
  37. required: true
  38. },
  39. iconItems: {
  40. type: Array,
  41. required: true
  42. }
  43. },
  44. setup(props) {
  45. //原值为'/newUI/skin/easy/images/logo/full-logo.png' Ben(20251205)
  46. const fullLogo = Vue.ref('/skin/easy/images/logo/full-logo.png'); // 需要指定完整路径
  47. const convertMenuData = (menuData) => {
  48. return menuData.map(item => ({
  49. id: item.id, //菜单id
  50. pid: item.pid, //父菜单id
  51. label: item.desc, //菜单名称
  52. component: item.url, //菜单链接
  53. js: item.js, //菜单要执行的js
  54. class: item.icon, // 如果icon为空则使用默认class
  55. itemType: item.type, //菜单 1:菜单项 2:菜单组
  56. }));
  57. };
  58. const menuItemsNew = Vue.ref(convertMenuData(props.menuData));
  59. Vue.watchEffect(() => {
  60. if (props.menuData && props.menuData.length) {
  61. menuItemsNew.value = convertMenuData(props.menuData);
  62. console.log('Header menu items updated:', menuItemsNew.value);
  63. }
  64. });
  65. const menuItems = Vue.ref([]);
  66. const breadCrumbs = Vue.ref([
  67. { label: '首页', component: '/index.html' },
  68. ]);
  69. const iconItems = Vue.ref(props.iconItems);
  70. //功能: 顶部工具栏图标跟随菜单模式(collapse/fixed/expand)by xu 20251222
  71. const globalMenuMode = Vue.ref(eventBus.getState('globalMenuModeChange') || 'collapse');
  72. //功能: icon-base 图标名调整:全局顶部菜单模式使用粗体 icon-autoTxt-bold / icon-fixTxt-bold / icon-fix-bold by xu 20251224
  73. const menuMode2IconClass = {
  74. collapse: 'icon-autoTxt-bold',
  75. fixed: 'icon-fix-bold',
  76. expand: 'icon-fixTxt-bold',
  77. };
  78. const globalMenuModeSubscriber = eventBus.subscribe('globalMenuModeChange', (mode) => {
  79. globalMenuMode.value = mode || 'collapse';
  80. });
  81. Vue.onUnmounted(() => {
  82. globalMenuModeSubscriber?.unSubscribe?.();
  83. });
  84. const resolveHeaderIconClass = (icon) => {
  85. if (!icon) {
  86. return '';
  87. }
  88. if (icon.name === 'qiehuan') {
  89. //功能: 顶部菜单模式切换图标直接使用 icon-autoTxt-bold / icon-fixTxt-bold / icon-fix-bold by xu 20251225
  90. const baseClass = icon.class || '';
  91. const iconClass = menuMode2IconClass[globalMenuMode.value] || menuMode2IconClass.collapse;
  92. return `${baseClass} ${iconClass}`.trim();
  93. }
  94. return icon.class || '';
  95. };
  96. const onClickMenuItemsNew = (item) => {
  97. console.log(item.component, item.js)
  98. if (item.component && item.js) {
  99. eval(item.js)
  100. } else if(item.component){
  101. eventBus.publish(EVEN_VAR.currentPage, item.component);
  102. } else {
  103. eval(item.js)
  104. }
  105. };
  106. const onClickMenuItem = (item) => {
  107. if (item.type == 'page') {
  108. eventBus.publish(EVEN_VAR.currentPage, item.component);
  109. breadCrumbs.value[0] = item
  110. } else if (item.type == 'dialog') {
  111. SS.openDialog({
  112. headerTitle: item.label,
  113. src: '.'+item.component,
  114. height: item.height,
  115. width:item.width
  116. });
  117. }
  118. };
  119. const breadCrumbClick = (item) => {
  120. eventBus.publish(EVEN_VAR.currentPage, item.component);
  121. breadCrumbs.value[0] = item
  122. }
  123. const gotoSearch = () => {
  124. eventBus.publish(EVEN_VAR.showGlobalSearchDialog);
  125. };
  126. const SsIcon = Vue.resolveComponent('ss-icon');
  127. const SsGolbalMenuIcon = Vue.resolveComponent('ss-golbal-menu-icon');
  128. // 引入搜索框
  129. const SsSearch = Vue.resolveComponent('ss-search');
  130. // 处理面包屑导航
  131. const renderBreadcrumbs = () => {
  132. const breadcrumbElements = [Vue.h(SsIcon, { class: "home-icon", name: "home", type: "common", size: "22px" })];
  133. // 遍历面包屑数组,添加面包屑和分隔符
  134. breadCrumbs.value.forEach((crumb, index) => {
  135. // 添加面包屑链接
  136. breadcrumbElements.push(
  137. Vue.h('span', { onClick: () => breadCrumbClick(crumb) }, crumb.label)
  138. );
  139. // 除了最后一个元素,每个面包屑后添加分隔符图标
  140. if (index < breadCrumbs.value.length - 1) {
  141. breadcrumbElements.push(
  142. Vue.h(SsIcon, { class: "split-icon", size: "12px", name: "double-arrow-right", type: "common" })
  143. );
  144. }
  145. });
  146. return breadcrumbElements;
  147. };
  148. return () => Vue.h('div', { class: 'block-self flex-between-center global-header-container' }, [
  149. Vue.h('div', { class: 'icon-area flex-start-center' }, [
  150. Vue.h('div', { class: 'logo', onClick: () => console.log('Home clicked') }, [
  151. Vue.h('div', { class: 'img' }, [
  152. Vue.h('img', { src: fullLogo.value })
  153. ]),
  154. Vue.h('div', { class: 'menu', onClick: Vue.withModifiers(() => { }, ['stop']) },
  155. // menuItems.value.map(item =>
  156. // Vue.h('div', { onClick: () => onClickMenuItem(item) },[
  157. // Vue.h(SsGolbalMenuIcon, { class: item.class || '' }),
  158. // item.label
  159. // ])
  160. // )
  161. menuItemsNew.value.map(item =>
  162. Vue.h('div', { onClick: () => onClickMenuItemsNew(item) },[
  163. Vue.h(SsGolbalMenuIcon, { class: item.class || '' }),
  164. item.label
  165. ])
  166. )
  167. )
  168. ]),
  169. // Vue.h('div', { class: 'bread-crumb', style: { height: "100% !important" } }, [
  170. // Vue.h('div', { class: 'content' }, renderBreadcrumbs())
  171. // ])
  172. ]),
  173. Vue.h('div', { class: 'menu-area' },
  174. Vue.h('div', { class: 'search-area' }, [
  175. Vue.h(SsSearch, { theme: 'dark', placeholder: '跨对象搜索', onClick: gotoSearch })
  176. ]),
  177. iconItems.value.map(icon =>
  178. Vue.h('div', { class: 'icon-item', onClick: icon.action, style: { display: icon.condition ? (icon.condition() ? 'block' : 'none') : 'block' } }, [
  179. //功能: 顶部工具栏图标改为 ss-icon + icon-base class by xu 20251222
  180. Vue.h(SsIcon, { class: resolveHeaderIconClass(icon) })
  181. ])
  182. )
  183. )
  184. ]);
  185. }
  186. };
  187. // 全局菜单
  188. export const GlobalMenu = {
  189. name: 'GlobalMenu',
  190. props: {
  191. menuItems: {
  192. type: Array,
  193. required: true
  194. },
  195. initialSize: {
  196. type: String,
  197. default: 'min'
  198. },
  199. // onMenuClick: {
  200. // type: Function,
  201. // required: true,
  202. // default: () => {}
  203. // }
  204. },
  205. setup(props) {
  206. const menuItemsNew = Vue.ref([]);
  207. const activeItem = Vue.ref(''); // 默认选中第一个菜单项
  208. // 将后台返回的菜单数据转换为树结构
  209. const convertMenuDataToTree = (menuData) => {
  210. // 先转换格式
  211. const formattedData = menuData.map(item => ({
  212. id: item.id,
  213. pid: item.pid,
  214. name: item.desc,
  215. component: item.url,
  216. js: item.js,
  217. // v3.0 优先使用 item.icon,没有才用 menu-base-icon + 默认图标 by xu 20251215
  218. class: item.icon || (item.type == 1 ? 'menu-base-icon icon-folder-close' : 'menu-base-icon icon-home'),
  219. itemType: item.type,
  220. children: []
  221. }));
  222. // 创建一个映射表,方便查找
  223. const map = {};
  224. formattedData.forEach(item => {
  225. map[item.id] = item;
  226. });
  227. // 构建树结构
  228. const treeData = [];
  229. formattedData.forEach(item => {
  230. if (item.pid && map[item.pid]) {
  231. // 如果有父节点,就放到父节点的children中
  232. if (!map[item.pid].children) {
  233. map[item.pid].children = [];
  234. }
  235. map[item.pid].children.push(item);
  236. } else {
  237. // 没有父节点就是顶层节点
  238. treeData.push(item);
  239. }
  240. });
  241. return treeData;
  242. };
  243. // 添加 watchEffect 来监听 props.menuItems 的变化
  244. Vue.watchEffect(() => {
  245. if (props.menuItems && props.menuItems.length) {
  246. menuItemsNew.value = convertMenuDataToTree(props.menuItems);
  247. // 初始化 activeItem
  248. const firstItem = menuItemsNew.value[0];
  249. if(firstItem?.component){
  250. // 首页有URL,直接设置为首页
  251. activeItem.value = firstItem.name;
  252. eventBus.publish(EVEN_VAR.currentPage, firstItem.component);
  253. } else if(firstItem?.children && firstItem.children.length > 0){
  254. // 首页没有URL,但有子菜单,设置为第一个子菜单
  255. activeItem.value = firstItem.children[0].name;
  256. eventBus.publish(EVEN_VAR.currentPage, firstItem.children[0].component);
  257. } else {
  258. activeItem.value = '';
  259. }
  260. console.log('Menu items updated:', menuItemsNew.value);
  261. }
  262. });
  263. // v3.0 使用 ss-icon 组件替代 SsNavIcon by xu 20251215
  264. const SsIcon = Vue.resolveComponent('ss-icon');
  265. // 移动状态控制逻辑到组件内
  266. const leftSideTypeDict = {
  267. min: { key: "min", icon: "arrow-double-right" },
  268. max: { key: "max", icon: "arrow-double-left" },
  269. };
  270. const leftSideType = Vue.ref(leftSideTypeDict[props.initialSize]);
  271. let waitLeftSideChangeTimer = null;
  272. // 控制子菜单展开状态
  273. const expandedMenus = Vue.ref(new Set());
  274. // 点击子菜单不高亮父菜单 by xu 20251211
  275. const shouldHighlightMenu = (item) => {
  276. if (!item) {
  277. return false;
  278. }
  279. const hasChildren = Array.isArray(item.children) && item.children.length > 0;
  280. return !hasChildren && activeItem.value === item.name;
  281. };
  282. // ===== 三种菜单模式管理 =====
  283. const menuModeDict = {
  284. collapse: { key: "collapse", label: "收起", width: "60px" },
  285. fixed: { key: "fixed", label: "固定", width: "60px" },
  286. expand: { key: "expand", label: "展开", width: "230px" }
  287. };
  288. // 当前菜单模式(默认收起)
  289. const currentMenuMode = Vue.ref(menuModeDict.collapse);
  290. //功能: 同步当前菜单模式到顶部工具栏(qiehuan 图标)by xu 20251222
  291. eventBus.publish('globalMenuModeChange', currentMenuMode.value.key);
  292. // 切换菜单模式(循环:收起 → 固定 → 展开 → 收起)by xu 20251219
  293. const toggleMenuMode = () => {
  294. const modeOrder = ['collapse', 'fixed', 'expand'];
  295. const currentIndex = modeOrder.indexOf(currentMenuMode.value.key);
  296. const nextIndex = (currentIndex + 1) % modeOrder.length;
  297. const nextMode = menuModeDict[modeOrder[nextIndex]];
  298. currentMenuMode.value = nextMode;
  299. eventBus.publish('globalMenuModeChange', nextMode.key);
  300. // 更新 CSS 变量
  301. updateLayoutWidth(nextMode.width);
  302. };
  303. // v3.0 通过事件总线监听菜单模式切换 by xu 20251219
  304. Vue.onMounted(() => {
  305. eventBus.subscribe('toggleGlobalMenuMode', toggleMenuMode);
  306. });
  307. Vue.onUnmounted(() => {
  308. eventBus.unsubscribe('toggleGlobalMenuMode', toggleMenuMode);
  309. });
  310. // 更新布局宽度
  311. const updateLayoutWidth = (width) => {
  312. const layoutContainer = document.querySelector('.layout-container');
  313. if (layoutContainer) {
  314. layoutContainer.style.setProperty('--left-side-width', width);
  315. }
  316. };
  317. const onMenuClick = (item) => {
  318. console.log(item)
  319. console.log(activeItem.value)
  320. // 功能说明:切换菜单时强制退出系统编辑态(避免“编辑态残留”导致左侧加号/标记异常) by xu 20260123
  321. try {
  322. eventBus.publish(EVEN_VAR.systemEditModelChange, { mode: "read", source: "menu" });
  323. } catch (_) {
  324. try { eventBus.publish(EVEN_VAR.systemEditModelChange, "read"); } catch (_) {}
  325. }
  326. if(item.component && item.js){
  327. eval(item.js)
  328. } else if(item.component){
  329. eventBus.publish(EVEN_VAR.currentPage, item.component);
  330. }else{
  331. eval(item.js)
  332. }
  333. }
  334. const toggleLeftSideType = () => {
  335. leftSideType.value =
  336. leftSideType.value.key === 'min' ? leftSideTypeDict.max : leftSideTypeDict.min;
  337. };
  338. const doChangeLeftSideType2Max = () => {
  339. // 只有在收起模式下才响应鼠标悬停
  340. if (currentMenuMode.value.key === 'collapse' && leftSideType.value.key === 'min') {
  341. // 即刻展开,不需要延迟
  342. clearTimeout(waitLeftSideChangeTimer);
  343. leftSideType.value = leftSideTypeDict.max;
  344. }
  345. };
  346. const onLeaveLeftMenuArea = () => {
  347. // 只有在收起模式下才响应鼠标离开
  348. if (currentMenuMode.value.key === 'collapse') {
  349. if (waitLeftSideChangeTimer) {
  350. clearTimeout(waitLeftSideChangeTimer);
  351. waitLeftSideChangeTimer = null;
  352. }
  353. leftSideType.value = leftSideTypeDict.min;
  354. }
  355. };
  356. return () => Vue.h('div', { class: 'left-side' }, [
  357. Vue.h('div', {
  358. class: 'left-side-container',
  359. 'data-mode': currentMenuMode.value.key, // 添加模式标识
  360. size: leftSideType.value.key
  361. }, [
  362. // ===== 固定区域:首页 =====
  363. Vue.h('div', { class: 'fixed-top' }, [
  364. menuItemsNew.value.length > 0 ? Vue.h('div', {
  365. class: ['menu-item', 'level-1', { active: shouldHighlightMenu(menuItemsNew.value[0]) }],
  366. onClick: () => {
  367. const firstItem = menuItemsNew.value[0];
  368. activeItem.value = firstItem.name;
  369. onMenuClick(firstItem);
  370. }
  371. }, [
  372. Vue.h('div', { class: 'menu-item-content' }, [
  373. // v3.0 使用 ss-icon + 双 class (图标类 + menu-icon 组类) by xu 20251215
  374. Vue.h(SsIcon, { class: 'menu-base-icon icon-home'}),
  375. Vue.h('div', { class: 'menu-item-label' }, menuItemsNew.value[0]?.name || '首页')
  376. ])
  377. ]) : null
  378. ]),
  379. // ===== 可滚动区域:其他菜单项 =====
  380. Vue.h('div', {
  381. class: 'scrollable-content',
  382. onMouseenter: doChangeLeftSideType2Max,
  383. onMousemove: doChangeLeftSideType2Max,
  384. onMouseleave: onLeaveLeftMenuArea
  385. }, [
  386. // 菜单项列表(跳过首页)
  387. ...menuItemsNew.value.slice(1).map(icon => [
  388. // 父菜单项
  389. Vue.h('div', {
  390. class: ['menu-item', 'level-1', { active: shouldHighlightMenu(icon) }],
  391. onClick: () => {
  392. if (icon.children && icon.children.length > 0) {
  393. // 有子菜单:仅切换展开/收起,不设置 activeItem(不高亮)
  394. if (expandedMenus.value.has(icon.name)) {
  395. expandedMenus.value.delete(icon.name);
  396. } else {
  397. expandedMenus.value.add(icon.name);
  398. }
  399. } else {
  400. // 无子菜单:设置 activeItem 并导航
  401. activeItem.value = icon.name;
  402. onMenuClick(icon);
  403. }
  404. }
  405. }, [
  406. Vue.h('div', { class: 'menu-item-content' }, [
  407. // v3.0 只显示原图标,不需要保底图标 by xu 20251219
  408. Vue.h(SsIcon, {
  409. class: icon.class + ' menu-icon'
  410. }),
  411. Vue.h('div', { class: 'menu-item-label' }, icon.name || ''),
  412. // 有子菜单时显示小圆点(在一级菜单右上角)
  413. icon.children && icon.children.length > 0 ?
  414. Vue.h('div', { class: 'has-children-dot' }) : null
  415. ])
  416. ]),
  417. // 子菜单项(与父菜单项同级)
  418. ...(icon.children && expandedMenus.value.has(icon.name) ?
  419. icon.children.map(child =>
  420. Vue.h('div', {
  421. class: ['menu-item', 'level-2', { active: activeItem.value === child.name }],
  422. onClick: (e) => {
  423. activeItem.value = child.name;
  424. e.stopPropagation();
  425. onMenuClick(child);
  426. }
  427. }, [
  428. Vue.h('div', { class: 'menu-item-content' }, [
  429. // v3.0 使用 ss-icon + 双 class by xu 20251215
  430. Vue.h(SsIcon, {
  431. class: (child.class || '') + ' menu-icon'
  432. }),
  433. Vue.h('div', { class: 'menu-item-label' }, child.name)
  434. ])
  435. ])
  436. ) : []
  437. )
  438. ]).flat()
  439. ]),
  440. // 功能说明:系统编辑模式(sys-mode="edit")下显示左侧菜单底部“+”入口(由 CSS 变量 --menu-add-btn-display 控制显隐) by xu 20260123
  441. Vue.h('div', {
  442. class: ['menu-item', 'add-menu-btn'],
  443. onClick: () => {
  444. try {
  445. eventBus.publish(EVEN_VAR.showProjectDialog);
  446. } catch (_) {
  447. }
  448. }
  449. }, [
  450. Vue.h('div', { class: 'add' }, [
  451. Vue.h(SsIcon, { class: 'menu-base-icon icon-add' })
  452. ])
  453. ])
  454. // v3.0 移除底部切换按钮,功能移至 header 右侧图标 by xu 20251219
  455. ])
  456. ]);
  457. }
  458. };
  459. // 基础组件 首页组件头部
  460. export const HeaderContainer = {
  461. name: 'HeaderContainer',
  462. props: {
  463. title: String,
  464. icon: String,
  465. },
  466. emits: ['setting', 'refresh'],
  467. setup(props, { emit }) {
  468. const onSetting = () => {
  469. emit('setting');
  470. };
  471. const onRefresh = () => {
  472. emit('refresh');
  473. };
  474. return {
  475. props,
  476. onSetting,
  477. onRefresh
  478. };
  479. },
  480. render() {
  481. const SsIcon = Vue.resolveComponent('ss-icon');
  482. return Vue.h('div', { class: 'edit-box-header-container' }, [
  483. Vue.h('div', { class: ['title', { visibility: !!(this.props.icon || this.props.title) }] }, [
  484. this.props.icon ? Vue.h('div', { class: 'icon', onClick: this.onRefresh }, [
  485. Vue.h(SsIcon, { class: 'normal', name: this.props.icon, size: '22px' }),
  486. Vue.h(SsIcon, { class: 'hover', name: 'refresh', size: '22px' })
  487. ]) : null,
  488. Vue.h('div', this.props.title)
  489. ]),
  490. Vue.h('div', { class: 'handle-bar' }, [
  491. Vue.h('div', { class: 'left-bar' }), // Assuming left-bar is empty
  492. Vue.h('div', { class: 'setting', onClick: this.onSetting }, [
  493. Vue.h(SsIcon, { name: 'setting', size: '22px' })
  494. ])
  495. ])
  496. ]);
  497. }
  498. };
  499. // 基础组件 首页组件边框
  500. export const EditBox = {
  501. name: 'EditBox',
  502. setup(props, { emit, slots }) {
  503. const mousePos = Vue.reactive({
  504. rightCenter: "right-center",
  505. rightBottom: "right-bottom",
  506. bottomCenter: "bottom-center",
  507. });
  508. const curActionMousePos = Vue.ref("");
  509. const editBoxContainer = Vue.ref(null);
  510. const onMousemove = (e) => {
  511. if (e.buttons === 1 && curActionMousePos.value) {
  512. const dom = editBoxContainer.value;
  513. if (dom) {
  514. const zoom = Number(document.body.style.zoom || 1);
  515. const winRect = dom.getBoundingClientRect();
  516. const { x: xSource, y: ySource } = e;
  517. const x = xSource / zoom;
  518. const y = ySource / zoom;
  519. const val = {
  520. left: winRect.left,
  521. top: winRect.top,
  522. width: winRect.width,
  523. height: winRect.height,
  524. };
  525. const toHObj = (source) => ({
  526. ...source,
  527. height: Math.abs(y - source.top),
  528. top: y >= 0 ? source.top : source.top + (y - source.top),
  529. });
  530. const toWObj = (source) => ({
  531. ...source,
  532. width: Math.abs(x - source.left),
  533. left: x >= 0 ? source.left : source.left + (x - source.left),
  534. });
  535. if (curActionMousePos.value === mousePos.bottomCenter) {
  536. emit("size", toHObj(val));
  537. } else if (curActionMousePos.value === mousePos.rightCenter) {
  538. emit("size", toWObj(val));
  539. } else if (curActionMousePos.value === mousePos.rightBottom) {
  540. emit("size", toWObj(toHObj(val)));
  541. }
  542. }
  543. }
  544. };
  545. const onMouseup = () => {
  546. curActionMousePos.value = "";
  547. };
  548. const onMouseDown = (pos) => {
  549. console.log(pos)
  550. curActionMousePos.value = pos;
  551. };
  552. Vue.onMounted(() => {
  553. document.body.addEventListener("mousemove", onMousemove);
  554. document.body.addEventListener("mouseup", onMouseup);
  555. });
  556. Vue.onUnmounted(() => {
  557. document.body.removeEventListener("mousemove", onMousemove);
  558. document.body.removeEventListener("mouseup", onMouseup);
  559. });
  560. const SsIcon = Vue.resolveComponent('ss-icon');
  561. return () => Vue.h('div', { class: 'edit-box-container', ref: editBoxContainer }, [
  562. Vue.h('div', { class: ['edit-tools', { active: !!curActionMousePos.value }] }, [
  563. Vue.h('div', { class: 'close' }, [
  564. Vue.h(SsIcon, { name: "close-mark-fill", size: "36px" })
  565. ]),
  566. Vue.h('div', { class: 'right-center', onMousedown: () => onMouseDown(mousePos.rightCenter) }, [
  567. Vue.h('div', { class: 'icon' }, [
  568. Vue.h(SsIcon, { name: "resize", size: "30px" })
  569. ])
  570. ]),
  571. Vue.h('div', { class: 'right-bottom', onMousedown: () => onMouseDown(mousePos.rightBottom) }, [
  572. Vue.h('div', { class: 'icon' }, [
  573. Vue.h(SsIcon, { name: "resize", size: "30px" })
  574. ])
  575. ]),
  576. Vue.h('div', { class: 'bottom-center', onMousedown: () => onMouseDown(mousePos.bottomCenter) }, [
  577. Vue.h('div', { class: 'icon' }, [
  578. Vue.h(SsIcon, { name: "resize", size: "30px" })
  579. ])
  580. ])
  581. ]),
  582. Vue.h('div', { class: 'content-area' }, slots.default ? slots.default() : [])
  583. ]);
  584. }
  585. };
  586. // 基础组件 头像
  587. export const Avatar = {
  588. name: 'Avatar',
  589. props: {
  590. url: {
  591. type: String,
  592. required: true
  593. },
  594. size: {
  595. type: [String, Number],
  596. default: 50
  597. },
  598. unit: {
  599. type: String,
  600. default: 'px'
  601. },
  602. shape: {
  603. validator: function (value) {
  604. return ['circle', 'round'].includes(value);
  605. },
  606. default: 'circle'
  607. }
  608. },
  609. setup(props) {
  610. // 计算属性,转换尺寸并添加单位
  611. const style = Vue.computed(() => {
  612. const addUnit = (n) => isNum(n) ? `${n}${props.unit}` : (n || '');
  613. const styleObj = {
  614. width: addUnit(props.size),
  615. height: addUnit(props.size),
  616. };
  617. return toStyleStr(styleObj);
  618. });
  619. // 返回渲染函数所需要的数据
  620. return {
  621. props,
  622. style
  623. };
  624. },
  625. render() {
  626. return Vue.h('img', {
  627. src: this.props.url,
  628. style: this.style.value,
  629. class: ['avatar-container', this.props.shape]
  630. });
  631. }
  632. };
  633. // 基础组件 文件夹或者文件的组件
  634. export const FolderContainer = {
  635. name: 'FolderContainer',
  636. props: {
  637. list: {
  638. type: Array,
  639. required: true,
  640. },
  641. draggable: {
  642. type: Boolean,
  643. default: false,
  644. },
  645. },
  646. setup(props, { emit }) {
  647. const toggleFolder = (index, isFolder) => {
  648. const item = props.list[index];
  649. if (isFolder) {
  650. item.open = !item.open;
  651. }
  652. };
  653. const onClickItem = (item) => {
  654. emit('click', item);
  655. };
  656. const onItemStartDrag = (e, item) => {
  657. e.dataTransfer.setData("text/plain", JSON.stringify(item));
  658. console.log("开始拖拽的对象是=>", { item });
  659. };
  660. const onItemDrop = (e, item, pre) => {
  661. var data = e.dataTransfer.getData("text/plain");
  662. try {
  663. const obj = JSON.parse(data);
  664. item.children.push(obj);
  665. console.log("目标对象=>", { e, item, pre, obj });
  666. } catch (err) {
  667. console.log("拖拽的节点不正确", { item, err });
  668. }
  669. };
  670. return {
  671. toggleFolder,
  672. onClickItem,
  673. onItemStartDrag,
  674. onItemDrop,
  675. };
  676. },
  677. render() {
  678. const SsIcon = Vue.resolveComponent('ss-icon');
  679. return Vue.h('div', { class: 'folder-container' }, this.list.map((groupItem, i) => {
  680. return Vue.h('div', { class: 'group-item', key: i }, [
  681. Vue.h('div', {
  682. class: ['group-title', { active: !!groupItem.curActionMousePos }],
  683. onDrop: $event => this.onItemDrop($event, groupItem),
  684. onDragover: $event => $event.preventDefault(),
  685. }, [
  686. groupItem.type === 'folder' ? Vue.h('div', {
  687. class: 'folder',
  688. onClick: $event => this.toggleFolder(i, groupItem.type === 'folder'),
  689. 'data-num': groupItem.children.length,
  690. }, [
  691. groupItem.open ? Vue.h(SsIcon, { name: 'folder-expand-fill' }) : Vue.h(SsIcon, { name: 'folder-collapse-fill' })
  692. ]) : groupItem.type === 'file' ? Vue.h(SsIcon, { name: 'file' }) : null,
  693. Vue.h('div', groupItem.title)
  694. ]),
  695. groupItem.children.length > 0 && groupItem.open ? Vue.h('ul', {
  696. class: 'group-childs',
  697. onDrop: $event => this.onItemDrop($event, groupItem),
  698. onDragover: $event => $event.preventDefault(),
  699. }, groupItem.children.map((item, j) => {
  700. return Vue.h('li', {
  701. key: j,
  702. draggable: this.draggable,
  703. onDragstart: $event => this.onItemStartDrag($event, item),
  704. onDrop: $event => this.onItemDrop($event, groupItem, item),
  705. onDragover: $event => $event.preventDefault(),
  706. onClick: () => this.onClickItem(item),
  707. }, [
  708. Vue.h('div', item.title),
  709. Vue.h('div', item.time)
  710. ]);
  711. })) : null
  712. ]);
  713. }));
  714. }
  715. };
  716. // 个人卡片
  717. export const UserInfo = {
  718. name: 'UserInfo',
  719. props: {
  720. obj: {
  721. type: Object,
  722. default: () => ({})
  723. }
  724. },
  725. setup(props, { emit }) {
  726. const winInfo = Vue.reactive(loadWinSize(winName.UserInfo));
  727. const onSetting = () => {
  728. console.log("点击了设置按钮");
  729. };
  730. const onRefresh = () => {
  731. console.log("点击了刷新按钮");
  732. };
  733. const onSizeChange = (rect) => {
  734. const style = {};
  735. for (const key in rect) {
  736. style[key] = rect[key] + 'px';
  737. }
  738. saveWinSize(winName.UserInfo, style);
  739. Object.assign(winInfo, style);
  740. };
  741. return {
  742. winInfo,
  743. onSetting,
  744. onRefresh,
  745. onSizeChange,
  746. props
  747. };
  748. },
  749. render() {
  750. const SsIcon = Vue.resolveComponent('ss-icon');
  751. return Vue.h('div', { class: 'user-info-container can-resize-box', style: this.winInfo },
  752. Vue.h(EditBox, {
  753. onSize: this.onSizeChange
  754. }, {
  755. default: () => [
  756. Vue.h('div', { class: 'header' },
  757. Vue.h(HeaderContainer, {
  758. onSetting: this.onSetting,
  759. onRefresh: this.onRefresh
  760. })
  761. ),
  762. Vue.h('div', { class: 'body' }, [
  763. Vue.h('div', { class: 'user-info', style: "margin-bottom: 18px" }, [
  764. Vue.h('div', { class: 'avatar' },
  765. Vue.h(Avatar, {
  766. url: '../images/example/user-avatar.png',
  767. size: '90px'
  768. })
  769. ),
  770. Vue.h('div', { class: 'info' }, [
  771. Vue.h('p', this.props.obj.name + ',上午好!'),
  772. Vue.h('p', '最近登录时间:2024年/08/08 13:03'),
  773. Vue.h('p', '明天有暴雨,记得出门带伞噢!')
  774. ])
  775. ]),
  776. Vue.h('div', { class: 'progress-bar' }, [
  777. Vue.h('div', { class: 'progress' }, [
  778. Vue.h('div', { class: 'line', style: 'width: 60%' }, [
  779. Vue.h('div', '60%(课时)')
  780. ])
  781. ])
  782. ]),
  783. Vue.h('div', { class: 'other-info' }, [
  784. Vue.h('div', {}, [
  785. Vue.h(SsIcon, { name: "userGroup", size: "22px" }),
  786. Vue.h('div', {}, "《关于公司全面预算规划会议》"),
  787. ]),
  788. Vue.h('div', {}, [
  789. Vue.h(SsIcon, { name: "card", type: "common", size: "22px" }),
  790. Vue.h('div', {}, "一卡通余额:**************"),
  791. ]),
  792. Vue.h('div', {}, [
  793. Vue.h(SsIcon, { name: "site", type: "common", size: "22px" }),
  794. Vue.h('div', {}, "个人网站:xxx.xxx.xxx"),
  795. ]),
  796. ])
  797. ])
  798. ]
  799. })
  800. );
  801. }
  802. };
  803. // 待办
  804. export const TodoList = {
  805. name: 'TodoList',
  806. setup() {
  807. const todoGroupList = Vue.reactive([
  808. {
  809. title: "草稿",
  810. type: "folder",
  811. open: false,
  812. children: [
  813. { title: "被退回", time: "08/10 18:12" },
  814. { title: "项目立项申请", time: "08/10 18:12" },
  815. { title: "报销申请", time: "08/10 18:12" },
  816. { title: "资产领用申请", time: "08/10 18:12" },
  817. ],
  818. },
  819. {
  820. title: "被退回",
  821. type: "folder",
  822. open: true,
  823. children: [
  824. { title: "被退回", time: "08/10 18:12" },
  825. { title: "项目立项申请", time: "08/10 18:12" },
  826. { title: "报销申请", time: "08/10 18:12" },
  827. { title: "资产领用申请", time: "08/10 18:12" },
  828. ],
  829. },
  830. {
  831. title: "2020-高薪技术业项目申报",
  832. type: "file",
  833. children: [],
  834. },
  835. ]);
  836. const winInfo = Vue.ref(loadWinSize(winName.TodoList));
  837. const onSetting = () => {
  838. console.log("Clicked on settings button");
  839. };
  840. const onRefresh = () => {
  841. console.log("Clicked on refresh button");
  842. };
  843. const onSizeChange = (rect) => {
  844. console.log(rect)
  845. const style = {};
  846. for (const key in rect) {
  847. style[key] = rect[key] + "px";
  848. }
  849. saveWinSize(winName.TodoList, style);
  850. winInfo.value = style;
  851. };
  852. const onItemClick = (e) => {
  853. console.log("===>>>", e);
  854. };
  855. Vue.onMounted(() => {
  856. // console.log("TodoList component is mounted");
  857. });
  858. return () => Vue.h('div', { class: 'todo-list-container can-resize-box', style: winInfo.value },
  859. Vue.h(EditBox, { onSize: onSizeChange }, {
  860. default: () => [
  861. Vue.h('div', { class: 'header' }, [
  862. Vue.h(HeaderContainer, {
  863. title: "待办",
  864. icon: "todo",
  865. onSetting,
  866. onRefresh
  867. })
  868. ]),
  869. Vue.h('div', { class: 'body' }, [
  870. Vue.h(FolderContainer, { list: todoGroupList, onClick: onItemClick })
  871. ])
  872. ]
  873. })
  874. )
  875. }
  876. };
  877. // 催办
  878. export const UrgingList = {
  879. name: 'UrgingList',
  880. setup() {
  881. const todoGroupList = Vue.reactive([
  882. {
  883. title: "项目",
  884. type: "folder",
  885. open: true,
  886. children: [
  887. { title: "《高新技术企业认定》项目报销", time: "08/10 18:12" },
  888. { title: "日常办公报销", time: "08/10 18:12" },
  889. { title: "固定资产维修费用报销", time: "08/10 18:12" },
  890. ],
  891. },
  892. {
  893. title: "报销",
  894. type: "folder",
  895. open: true,
  896. children: [
  897. {
  898. title: "2020-企业-技术改造专项资金项目-立项申请",
  899. time: "08/10 18:12",
  900. },
  901. ],
  902. },
  903. {
  904. title: "2020-高薪技术企业项目申报",
  905. type: "file",
  906. children: [],
  907. },
  908. {
  909. title: "2020-高薪技术企业项目申报-技术改造专项资金项目-立项申请",
  910. type: "file",
  911. children: [],
  912. },
  913. ]);
  914. const winInfo = Vue.ref(loadWinSize(winName.UrgingList));
  915. const onSetting = () => {
  916. console.log("Clicked on settings button");
  917. };
  918. const onRefresh = () => {
  919. console.log("Clicked on refresh button");
  920. };
  921. const onSizeChange = (rect) => {
  922. const style = {};
  923. for (const key in rect) {
  924. style[key] = rect[key] + "px";
  925. }
  926. saveWinSize(winName.UrgingList, style);
  927. winInfo.value = style;
  928. };
  929. return () =>
  930. Vue.h('div', { class: 'todo-list-container can-resize-box', style: winInfo.value }, Vue.h(EditBox, { onSize: onSizeChange }, {
  931. default: () => [
  932. Vue.h('div', { class: 'header' }, [
  933. Vue.h(HeaderContainer, {
  934. title: "催办",
  935. icon: "alarm-clock",
  936. onSetting,
  937. onRefresh
  938. })
  939. ]),
  940. Vue.h('div', { class: 'body' }, [
  941. Vue.h(FolderContainer, { list: todoGroupList })
  942. ])
  943. ]
  944. })
  945. )
  946. }
  947. };
  948. // 公示公告
  949. export const Notice = {
  950. name: 'Notice',
  951. setup() {
  952. const list = Vue.reactive([
  953. { title: "关于2022年度中层干部的任免公告", time: "10/08 08:30" },
  954. { title: "2022年重组团队从心出发", time: "10/08 08:30" },
  955. { title: "关于单位进入粤港澳创新创业大赛决赛", time: "10/08 08:30" },
  956. { title: "关于2022年度中层干部的任免公告", time: "10/08 08:30" },
  957. ]);
  958. const winInfo = Vue.ref(loadWinSize(winName.Notice));
  959. const onSetting = () => {
  960. console.log("Clicked on settings button");
  961. };
  962. const onRefresh = () => {
  963. console.log("Clicked on refresh button");
  964. };
  965. const onSizeChange = (rect) => {
  966. const style = {};
  967. for (const key in rect) {
  968. style[key] = rect[key] + "px";
  969. }
  970. saveWinSize(winName.Notice, style);
  971. winInfo.value = style;
  972. };
  973. const SsIcon = Vue.resolveComponent('ss-icon');
  974. return () => Vue.h('div', { class: 'notice-list-container can-resize-box', style: winInfo.value }, [
  975. Vue.h(EditBox, { onSize: onSizeChange }, {
  976. default: () => [
  977. Vue.h('div', { class: 'header' }, [
  978. Vue.h(HeaderContainer, {
  979. title: "公示公告",
  980. icon: "notice",
  981. onSetting: onSetting,
  982. onRefresh: onRefresh
  983. })
  984. ]),
  985. Vue.h('div', { class: 'body' }, list.map((item, i) =>
  986. Vue.h('div', { key: i }, [
  987. Vue.h('div', [
  988. Vue.h(SsIcon, { name: "file", size: "22px" }),
  989. Vue.h('span', item.title)
  990. ]),
  991. Vue.h('div', item.time)
  992. ])
  993. ))
  994. ]
  995. })
  996. ]);
  997. }
  998. };
  999. // 快捷发起
  1000. export const Launch = {
  1001. name: 'Launch',
  1002. setup() {
  1003. const winInfo = Vue.ref(loadWinSize(winName.launch));
  1004. const popupInfo = Vue.reactive({
  1005. status: false,
  1006. height: 100,
  1007. width: 100,
  1008. left: 0,
  1009. top: 0,
  1010. });
  1011. const popupStyle = Vue.computed(() => {
  1012. const { width, height, left, top } = popupInfo;
  1013. return {
  1014. left: `${left - (130 - width) / 2}px`,
  1015. top: `${top + height}px`,
  1016. };
  1017. });
  1018. const items = Vue.reactive([
  1019. { name: "qingjia", text: "请假" },
  1020. {
  1021. name: "shoufukuan",
  1022. text: "收付款",
  1023. hasPopup: true,
  1024. popupContent: [
  1025. { text: "收款" },
  1026. { text: "付款" }
  1027. ]
  1028. },
  1029. {
  1030. name: "kaoqin",
  1031. text: "考勤",
  1032. hasPopup: true,
  1033. popupContent: [
  1034. { text: "2" },
  1035. { text: "3" }
  1036. ]
  1037. }
  1038. ]);
  1039. const onSetting = () => console.log("Clicked on settings button");
  1040. const onRefresh = () => console.log("Clicked on refresh button");
  1041. const onMouseMove = (e, item) => {
  1042. console.log("鼠标进入了",item.text)
  1043. showPopupDialog(e, item)
  1044. };
  1045. const onMouseleave = (e,item) => {
  1046. console.log("鼠标离开了",item.text)
  1047. popupInfo.status = false;
  1048. };
  1049. const showPopupDialog = (e, item) => {
  1050. const dom = e.target.closest('.item');
  1051. if (dom) {
  1052. const rect = dom.getBoundingClientRect();
  1053. popupInfo.height = rect.height;
  1054. popupInfo.left = rect.left;
  1055. popupInfo.top = rect.top;
  1056. popupInfo.width = rect.width;
  1057. setTimeout(() => {
  1058. popupInfo.status = true;
  1059. }, 600)
  1060. }
  1061. };
  1062. const onSizeChange = (rect) => {
  1063. const style = {};
  1064. for (const key in rect) style[key] = rect[key] + "px";
  1065. saveWinSize(winName.launch, style);
  1066. winInfo.value = style;
  1067. };
  1068. // Vue.onMounted(() => console.log("LaunchContainer component is mounted"));
  1069. const SsIcon = Vue.resolveComponent('ss-icon');
  1070. return () => Vue.h('div', { class: 'launch-container can-resize-box', style: winInfo.value }, [
  1071. Vue.h(EditBox, { onSize: onSizeChange }, {
  1072. default: () => [
  1073. Vue.h('div', { class: 'header' }, [
  1074. Vue.h(HeaderContainer, { title: "快捷发起", icon: "lightning", onSetting, onRefresh })
  1075. ]),
  1076. Vue.h('div', { class: 'body' }, items.map(item =>
  1077. Vue.h('div', { class: 'item', onMousemove: e => onMouseMove(e, item), onMouseleave: e => onMouseleave(e, item) }, [
  1078. Vue.h(SsIcon, {
  1079. class: item.hasPopup && item.popupContent.length > 0 ? "mark-down" : "",
  1080. name: item.name,
  1081. size: "36px"
  1082. }),
  1083. Vue.h('div', { class: 'text' }, item.text),
  1084. item.hasPopup && popupInfo.status ? Vue.h('div', { class: 'popup', style: popupStyle.value }, item.popupContent.map(content =>
  1085. Vue.h('div', content.text)
  1086. )) : null
  1087. ])
  1088. ))
  1089. ]
  1090. })
  1091. ])
  1092. }
  1093. };
  1094. // 项目实时统计图
  1095. export const Statistics = {
  1096. name: 'Statistics',
  1097. setup() {
  1098. const winInfo = Vue.ref(loadWinSize(winName.Statistics));
  1099. const chartInstance = Vue.ref(null);
  1100. const option = {
  1101. tooltip: {
  1102. trigger: "axis",
  1103. axisPointer: {
  1104. type: "cross",
  1105. crossStyle: {
  1106. color: "#999",
  1107. },
  1108. },
  1109. },
  1110. legend: {
  1111. data: ["项目金额", "成本"]
  1112. },
  1113. xAxis: [{
  1114. type: "category",
  1115. data: ["2月", "3月", "4���", "5月", "6月", "7月", "8月", "9月", "10月", "11月"],
  1116. axisPointer: {
  1117. type: "shadow"
  1118. },
  1119. }],
  1120. yAxis: [{
  1121. type: "value",
  1122. name: "单位:万(RMB)",
  1123. min: 0,
  1124. max: 1000,
  1125. interval: 200,
  1126. axisLabel: {
  1127. formatter: "{value} "
  1128. },
  1129. }, {
  1130. type: "value",
  1131. name: "成本",
  1132. min: 0,
  1133. max: 1000,
  1134. interval: 200,
  1135. axisLabel: {
  1136. formatter: "{value}"
  1137. },
  1138. }],
  1139. series: [{
  1140. name: "项目金额",
  1141. type: "bar",
  1142. tooltip: {
  1143. valueFormatter: function (value) {
  1144. return value + " ml";
  1145. }
  1146. },
  1147. data: [250, 850, 500, 650, 450, 850, 780, 450, 700, 850]
  1148. }, {
  1149. name: "成本",
  1150. type: "line",
  1151. yAxisIndex: 1,
  1152. tooltip: {
  1153. valueFormatter: function (value) {
  1154. return value;
  1155. }
  1156. },
  1157. data: [400, 390, 580, 590, 430, 420, 430, 550, 230, 250]
  1158. }]
  1159. }
  1160. // 初始化echants
  1161. const initChart = () => {
  1162. const chartDom = document.getElementById("statistics-chart");
  1163. if (!chartDom) return;
  1164. chartInstance.value = echarts.init(chartDom);
  1165. chartInstance.value.setOption(option);
  1166. };
  1167. const onSetting = () => console.log("Clicked on settings button");
  1168. const onRefresh = () => {
  1169. console.log("Clicked on refresh button");
  1170. if (chartInstance.value) {
  1171. chartInstance.value.clear();
  1172. initChart(); // Reinitialize the chart to reflect any new data or settings
  1173. }
  1174. };
  1175. const onSizeChange = (rect) => {
  1176. const style = {};
  1177. for (const key in rect) style[key] = rect[key] + "px";
  1178. saveWinSize(winName.Statistics, style);
  1179. winInfo.value = style;
  1180. if (chartInstance.value) {
  1181. chartInstance.value.resize();
  1182. }
  1183. };
  1184. Vue.onMounted(() => {
  1185. // console.log("StatisticsContainer component is mounted");
  1186. initChart(); // Initialize the chart when component is mounted
  1187. });
  1188. // return { winInfo, onSetting, onRefresh, onSizeChange };
  1189. return () => Vue.h('div', { class: 'statistics-container can-resize-box', style: winInfo.value }, [
  1190. Vue.h(EditBox, { onSize: onSizeChange }, {
  1191. default: () => [
  1192. Vue.h('div', { class: 'header' }, [
  1193. Vue.h(HeaderContainer, { title: "项目实时统计图", icon: "layer", onSetting, onRefresh })
  1194. ]),
  1195. Vue.h('div', { class: 'body' }, [
  1196. Vue.h('div', { id: 'statistics-chart', class: 'chart-container' })
  1197. ])
  1198. ]
  1199. })
  1200. ]);
  1201. },
  1202. };