ss-index-components.js 46 KB

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