ss-index-components.js 46 KB

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