ss-index-components.js 48 KB

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