ss-index-components.js 48 KB

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