mirror of
				https://github.com/soybeanjs/soybean-admin.git
				synced 2025-11-04 07:43:42 +08:00 
			
		
		
		
	Compare commits
	
		
			79 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					ec792fa749 | ||
| 
						 | 
					232f56fd62 | ||
| 
						 | 
					6010f51801 | ||
| 
						 | 
					9755c31361 | ||
| 
						 | 
					0b9982bdc6 | ||
| 
						 | 
					1d14269557 | ||
| 
						 | 
					5cfb606042 | ||
| 
						 | 
					e471e9140e | ||
| 
						 | 
					b2c919b673 | ||
| 
						 | 
					8dc17e62f1 | ||
| 
						 | 
					60115572a8 | ||
| 
						 | 
					56ad0df974 | ||
| 
						 | 
					6cbf5705bb | ||
| 
						 | 
					87a66a4236 | ||
| 
						 | 
					3549c4dbd5 | ||
| 
						 | 
					24c6df528b | ||
| 
						 | 
					dac5075be9 | ||
| 
						 | 
					a8d1e5d266 | ||
| 
						 | 
					345aa2932f | ||
| 
						 | 
					017440c1e6 | ||
| 
						 | 
					d5a3a25d3d | ||
| 
						 | 
					d567c057a8 | ||
| 
						 | 
					91fae50260 | ||
| 
						 | 
					61fa4b7f3b | ||
| 
						 | 
					ef7acc626f | ||
| 
						 | 
					2a0c9f1b41 | ||
| 
						 | 
					e18d39724a | ||
| 
						 | 
					5be864a80b | ||
| 
						 | 
					fd087f59fa | ||
| 
						 | 
					b041fdd864 | ||
| 
						 | 
					9fa951aa06 | ||
| 
						 | 
					12b25e0d58 | ||
| 
						 | 
					e33f944a74 | ||
| 
						 | 
					3d72f954ed | ||
| 
						 | 
					1213531bef | ||
| 
						 | 
					805c338141 | ||
| 
						 | 
					3c0a52825d | ||
| 
						 | 
					100e0ea55d | ||
| 
						 | 
					257f1183fc | ||
| 
						 | 
					d73111116a | ||
| 
						 | 
					29a2a5c66a | ||
| 
						 | 
					4005763c00 | ||
| 
						 | 
					a55b4dc073 | ||
| 
						 | 
					be8f915a0c | ||
| 
						 | 
					8d7f91dccf | ||
| 
						 | 
					33ade53904 | ||
| 
						 | 
					358e129765 | ||
| 
						 | 
					9ea56c9b82 | ||
| 
						 | 
					923eb98a5c | ||
| 
						 | 
					87adc35f2e | ||
| 
						 | 
					ee4341457a | ||
| 
						 | 
					8a7cd5934b | ||
| 
						 | 
					c962f7b2c5 | ||
| 
						 | 
					8cc5177cda | ||
| 
						 | 
					3a343eea33 | ||
| 
						 | 
					f83eefbc3e | ||
| 
						 | 
					936b834e62 | ||
| 
						 | 
					c965140b87 | ||
| 
						 | 
					32b8f99071 | ||
| 
						 | 
					abaaa4a068 | ||
| 
						 | 
					b4e125300e | ||
| 
						 | 
					50a5cba088 | ||
| 
						 | 
					d6c8142bb4 | ||
| 
						 | 
					8146858b96 | ||
| 
						 | 
					8b8a2083bb | ||
| 
						 | 
					6207292d81 | ||
| 
						 | 
					b4e5c6d990 | ||
| 
						 | 
					b6ac3106ce | ||
| 
						 | 
					d37ce04606 | ||
| 
						 | 
					8439a60070 | ||
| 
						 | 
					f238fcbd47 | ||
| 
						 | 
					8ba71a0857 | ||
| 
						 | 
					e89b86ce56 | ||
| 
						 | 
					03dd64c543 | ||
| 
						 | 
					aeb6369005 | ||
| 
						 | 
					133196f337 | ||
| 
						 | 
					41191d54fb | ||
| 
						 | 
					5cb1cebd88 | ||
| 
						 | 
					a5c4b4e3b7 | 
							
								
								
									
										19
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@@ -4,15 +4,28 @@
 | 
			
		||||
    "source.organizeImports": "never"
 | 
			
		||||
  },
 | 
			
		||||
  "editor.formatOnSave": false,
 | 
			
		||||
  "eslint.validate": ["html", "css", "scss", "json", "jsonc"],
 | 
			
		||||
  "eslint.validate": [
 | 
			
		||||
    "html",
 | 
			
		||||
    "css",
 | 
			
		||||
    "scss",
 | 
			
		||||
    "json",
 | 
			
		||||
    "jsonc",
 | 
			
		||||
    "javascript",
 | 
			
		||||
    "javascriptreact",
 | 
			
		||||
    "typescript",
 | 
			
		||||
    "typescriptreact",
 | 
			
		||||
    "vue"
 | 
			
		||||
  ],
 | 
			
		||||
  "i18n-ally.displayLanguage": "zh-cn",
 | 
			
		||||
  "i18n-ally.enabledParsers": ["ts"],
 | 
			
		||||
  "i18n-ally.enabledFrameworks": ["vue"],
 | 
			
		||||
  "i18n-ally.editor.preferEditor": true,
 | 
			
		||||
  "i18n-ally.keystyle": "nested",
 | 
			
		||||
  "i18n-ally.localesPaths": ["src/locales/langs"],
 | 
			
		||||
  "i18n-ally.parsers.typescript.compilerOptions": {
 | 
			
		||||
    "moduleResolution": "node"
 | 
			
		||||
  },
 | 
			
		||||
  "prettier.enable": false,
 | 
			
		||||
  "typescript.tsdk": "node_modules/typescript/lib",
 | 
			
		||||
  "unocss.root": ["./"],
 | 
			
		||||
  "vue.server.hybridMode": true
 | 
			
		||||
  "unocss.root": ["./"]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										281
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										281
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,6 +1,287 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## [v2.0.0](https://github.com/soybeanjs/soybean-admin/compare/v1.3.15...v2.0.0) (2025-11-02)
 | 
			
		||||
 | 
			
		||||
###    🚨 Breaking Changes
 | 
			
		||||
 | 
			
		||||
- **hooks**: refactor useTable and enhance type definitions  -  by @soybeanjs [<samp>(8cc51)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8cc5177c)
 | 
			
		||||
- **projects**: optimize layout mode, split horizontal mix component into two layouts, and rename the component.  -  by **Azir** [<samp>(b6ac3)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b6ac3106)
 | 
			
		||||
- **request**: remove cancelRequest method and related logic from request instances  -  by @soybeanjs [<samp>(b4e12)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b4e12530)
 | 
			
		||||
 | 
			
		||||
###    🚀 Features
 | 
			
		||||
 | 
			
		||||
- **components**:
 | 
			
		||||
  - add the IconTooltip component.  -  by **Azir-11** [<samp>(a55b4)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a55b4dc0)
 | 
			
		||||
  - replace NTooltip with IconTooltip and optimize the layout of related components.  -  by **Azir-11** [<samp>(40057)</samp>](https://github.com/soybeanjs/soybean-admin/commit/4005763c)
 | 
			
		||||
- **global-tab**:
 | 
			
		||||
  - add support for switching tabs with right mouse button click  -  by @soybeanjs [<samp>(b2c91)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b2c919b6)
 | 
			
		||||
- **hooks**:
 | 
			
		||||
  - add scrollX computation for total table width in useNaiveTable  -  by @Lruihao [<samp>(358e1)</samp>](https://github.com/soybeanjs/soybean-admin/commit/358e1297)
 | 
			
		||||
- **packages**:
 | 
			
		||||
  - materials support slider-tab. closed #823  -  by @CyberShen in https://github.com/soybeanjs/soybean-admin/issues/823 [<samp>(61fa4)</samp>](https://github.com/soybeanjs/soybean-admin/commit/61fa4b7f)
 | 
			
		||||
- **projects**:
 | 
			
		||||
  - refactor theme drawer with tabbed layout for better UX.  -  by **Azir** [<samp>(8ba71)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8ba71a08)
 | 
			
		||||
  - Add current time display option for watermark  -  by @wenyuanw in https://github.com/soybeanjs/soybean-admin/issues/772 [<samp>(f238f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/f238fcbd)
 | 
			
		||||
  - add 'vertical-hybrid-header-first' layout mode  -  by @wenyuanw [<samp>(b4e5c)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b4e5c6d9)
 | 
			
		||||
  - add prompt information for scrolling mode and tab bar caching.  -  by **Azir-11** [<samp>(29a2a)</samp>](https://github.com/soybeanjs/soybean-admin/commit/29a2a5c6)
 | 
			
		||||
  - support theme preset function.  -  by **Azir-11** [<samp>(257f1)</samp>](https://github.com/soybeanjs/soybean-admin/commit/257f1183)
 | 
			
		||||
  - modify the default value of the reset cache policy to 'refresh'.  -  by **Azir-11** [<samp>(3c0a5)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3c0a5282)
 | 
			
		||||
  - optimize tabs cache cleaning strategy. close #820.  -  by **Azir-11** in https://github.com/soybeanjs/soybean-admin/issues/820 [<samp>(ef7ac)</samp>](https://github.com/soybeanjs/soybean-admin/commit/ef7acc62)
 | 
			
		||||
  - support closing tabs with middle mouse button click  -  by @wenyuanw [<samp>(a8d1e)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a8d1e5d2)
 | 
			
		||||
  - support set global redius  -  by **CyberShen123** [<samp>(24c6d)</samp>](https://github.com/soybeanjs/soybean-admin/commit/24c6df52)
 | 
			
		||||
  - support set global redius  -  by **CyberShen123** [<samp>(3549c)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3549c4db)
 | 
			
		||||
  - compatible with the new Echarts API and optimized styles.  -  by **Azir-11** [<samp>(9755c)</samp>](https://github.com/soybeanjs/soybean-admin/commit/9755c313)
 | 
			
		||||
- **styles**:
 | 
			
		||||
  - add text-autospace property to improve text layout  -  by @wenyuanw [<samp>(345aa)</samp>](https://github.com/soybeanjs/soybean-admin/commit/345aa293)
 | 
			
		||||
 | 
			
		||||
###    🐞 Bug Fixes
 | 
			
		||||
 | 
			
		||||
- **hooks**:
 | 
			
		||||
  - correct chart rendering logic in useEcharts  -  by @soybeanjs [<samp>(8a7cd)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8a7cd593)
 | 
			
		||||
- **layout**:
 | 
			
		||||
  - fix getSiderWidth  -  by @soybeanjs [<samp>(e471e)</samp>](https://github.com/soybeanjs/soybean-admin/commit/e471e914)
 | 
			
		||||
- **packages**:
 | 
			
		||||
  - fix the parsing logic for stored data to ensure correct return of boolean values  -  by @Lruihao [<samp>(9ea56)</samp>](https://github.com/soybeanjs/soybean-admin/commit/9ea56c9b)
 | 
			
		||||
  - axios: fix json response. fixed #815  -  by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/815 [<samp>(fd087)</samp>](https://github.com/soybeanjs/soybean-admin/commit/fd087f59)
 | 
			
		||||
  - axios: fix json response. fixed #815  -  by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/815 [<samp>(5be86)</samp>](https://github.com/soybeanjs/soybean-admin/commit/5be864a8)
 | 
			
		||||
- **projects**:
 | 
			
		||||
  - Fix i18n-ally not working when setting moduleResolution to bundler. fixed #780  -  by @xiaobao0505 in https://github.com/soybeanjs/soybean-admin/issues/780 [<samp>(41191)</samp>](https://github.com/soybeanjs/soybean-admin/commit/41191d54)
 | 
			
		||||
  - adjust legend position in line chart options.  -  by **Azir-11** [<samp>(0b998)</samp>](https://github.com/soybeanjs/soybean-admin/commit/0b9982bd)
 | 
			
		||||
- **readme**:
 | 
			
		||||
  - update GitHub stars and forks links for gitee  -  by @soybeanjs [<samp>(923eb)</samp>](https://github.com/soybeanjs/soybean-admin/commit/923eb98a)
 | 
			
		||||
- **scripts**:
 | 
			
		||||
  - update command to use 'npm-check-updates' instead of 'ncu'  -  by @soybeanjs [<samp>(8dc17)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8dc17e62)
 | 
			
		||||
- **styles**:
 | 
			
		||||
  - show light color scrollbar while dark mode is on  -  by **whyang** [<samp>(dac50)</samp>](https://github.com/soybeanjs/soybean-admin/commit/dac5075b)
 | 
			
		||||
- **table**:
 | 
			
		||||
  - add type annotations for records in useTable hook  -  by @soybeanjs [<samp>(32b8f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/32b8f990)
 | 
			
		||||
- **types**:
 | 
			
		||||
  - fix proxy types  -  by @soybeanjs [<samp>(3d72f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3d72f954)
 | 
			
		||||
  - fix proxy types  -  by @soybeanjs [<samp>(12b25)</samp>](https://github.com/soybeanjs/soybean-admin/commit/12b25e0d)
 | 
			
		||||
  - fix ts type error  -  by @soybeanjs [<samp>(d5a3a)</samp>](https://github.com/soybeanjs/soybean-admin/commit/d5a3a25d)
 | 
			
		||||
 | 
			
		||||
###    🛠 Optimizations
 | 
			
		||||
 | 
			
		||||
- **hooks**:
 | 
			
		||||
  - optimize useEcharts  -  by @soybeanjs [<samp>(936b8)</samp>](https://github.com/soybeanjs/soybean-admin/commit/936b834e)
 | 
			
		||||
- **packages**:
 | 
			
		||||
  - remove ofetch package  -  by @soybeanjs [<samp>(abaaa)</samp>](https://github.com/soybeanjs/soybean-admin/commit/abaaa4a0)
 | 
			
		||||
- **projects**:
 | 
			
		||||
  - improve theme drawer responsive width for mobile devices  -  by @wenyuanw [<samp>(8439a)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8439a600)
 | 
			
		||||
  - improve robustness of second-level menu key logic  -  by @wenyuanw [<samp>(8b8a2)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8b8a2083)
 | 
			
		||||
  - optimize theme drawer width  -  by @soybeanjs [<samp>(81468)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8146858b)
 | 
			
		||||
  - optimize api type file  -  by @soybeanjs [<samp>(3a343)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3a343eea)
 | 
			
		||||
  - optimize radius settings  -  by @soybeanjs [<samp>(87a66)</samp>](https://github.com/soybeanjs/soybean-admin/commit/87a66a42)
 | 
			
		||||
- **request**:
 | 
			
		||||
  - enhance request options and response handling with generic types  -  by @soybeanjs [<samp>(50a5c)</samp>](https://github.com/soybeanjs/soybean-admin/commit/50a5cba0)
 | 
			
		||||
- **typings**:
 | 
			
		||||
  - update component typings  -  by @soybeanjs [<samp>(1d142)</samp>](https://github.com/soybeanjs/soybean-admin/commit/1d142695)
 | 
			
		||||
 | 
			
		||||
###    💅 Refactors
 | 
			
		||||
 | 
			
		||||
- **hooks**:
 | 
			
		||||
  - optimize useContext and update useMixMenuContext  -  by @soybeanjs [<samp>(c9651)</samp>](https://github.com/soybeanjs/soybean-admin/commit/c965140b)
 | 
			
		||||
  - streamline column visibility handling in useTable and table components  -  by @soybeanjs [<samp>(ee434)</samp>](https://github.com/soybeanjs/soybean-admin/commit/ee434145)
 | 
			
		||||
  - remove useSignal hook and update exports  -  by @soybeanjs [<samp>(87adc)</samp>](https://github.com/soybeanjs/soybean-admin/commit/87adc35f)
 | 
			
		||||
- **menu**:
 | 
			
		||||
  - optimize the margin on the menu  -  by **NicholasLD** [<samp>(d7311)</samp>](https://github.com/soybeanjs/soybean-admin/commit/d7311111)
 | 
			
		||||
- **projects**:
 | 
			
		||||
  - remove unnecessary logic in onRouteSwitchWhenLoggedIn  -  by @wenyuanw [<samp>(d6c81)</samp>](https://github.com/soybeanjs/soybean-admin/commit/d6c8142b)
 | 
			
		||||
- **request**:
 | 
			
		||||
  - unify response transformation methods and deprecate transformBackendResponse  -  by @soybeanjs [<samp>(f83ee)</samp>](https://github.com/soybeanjs/soybean-admin/commit/f83eefbc)
 | 
			
		||||
- **types**:
 | 
			
		||||
  - move Auth and Route namespaces to separate files and clean up api.d.ts  -  by **Azir** [<samp>(d37ce)</samp>](https://github.com/soybeanjs/soybean-admin/commit/d37ce046)
 | 
			
		||||
 | 
			
		||||
###    📖 Documentation
 | 
			
		||||
 | 
			
		||||
- **projects**:
 | 
			
		||||
  - add github trendshift info.  -  by **恕瑞玛的皇帝** [<samp>(e18d3)</samp>](https://github.com/soybeanjs/soybean-admin/commit/e18d3972)
 | 
			
		||||
  - add github trendshift info.  -  by **恕瑞玛的皇帝** [<samp>(2a0c9)</samp>](https://github.com/soybeanjs/soybean-admin/commit/2a0c9f1b)
 | 
			
		||||
  - add contribution leaderboard  -  by @wenyuanw [<samp>(01744)</samp>](https://github.com/soybeanjs/soybean-admin/commit/017440c1)
 | 
			
		||||
 | 
			
		||||
###    🏡 Chore
 | 
			
		||||
 | 
			
		||||
- **deps**:
 | 
			
		||||
  - update NodeJS and pnpm version requirements in package.json and documentation  -  by **Junior25306** [<samp>(a5c4b)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a5c4b4e3)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(5cb1c)</samp>](https://github.com/soybeanjs/soybean-admin/commit/5cb1cebd)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(aeb63)</samp>](https://github.com/soybeanjs/soybean-admin/commit/aeb63690)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(e89b8)</samp>](https://github.com/soybeanjs/soybean-admin/commit/e89b86ce)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(c962f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/c962f7b2)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(12135)</samp>](https://github.com/soybeanjs/soybean-admin/commit/1213531b)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(e33f9)</samp>](https://github.com/soybeanjs/soybean-admin/commit/e33f944a)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(9fa95)</samp>](https://github.com/soybeanjs/soybean-admin/commit/9fa951aa)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(b041f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b041fdd8)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(d567c)</samp>](https://github.com/soybeanjs/soybean-admin/commit/d567c057)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(6cbf5)</samp>](https://github.com/soybeanjs/soybean-admin/commit/6cbf5705)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(6010f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/6010f518)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(232f5)</samp>](https://github.com/soybeanjs/soybean-admin/commit/232f56fd)
 | 
			
		||||
- **other**:
 | 
			
		||||
  - update the ESLint validation configuration to support more file types.  -  by **Azir-11** [<samp>(8d7f9)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8d7f91dc)
 | 
			
		||||
  - update the ESLint validation configuration to support more file types.  -  by **Azir-11** [<samp>(be8f9)</samp>](https://github.com/soybeanjs/soybean-admin/commit/be8f915a)
 | 
			
		||||
- **packages**:
 | 
			
		||||
  - update Vite version to 7 in package.json and documentation.  -  by **Azir** [<samp>(03dd6)</samp>](https://github.com/soybeanjs/soybean-admin/commit/03dd64c5)
 | 
			
		||||
  - add picomatch to fix scripts  -  by @soybeanjs [<samp>(805c3)</samp>](https://github.com/soybeanjs/soybean-admin/commit/805c3381)
 | 
			
		||||
- **readme**:
 | 
			
		||||
  - remove DartNode sponsorship badge from README files  -  by @soybeanjs [<samp>(33ade)</samp>](https://github.com/soybeanjs/soybean-admin/commit/33ade539)
 | 
			
		||||
- **vscode**:
 | 
			
		||||
  - remove unused vue.server.hybridMode setting from .vscode/settings.json  -  by @soybeanjs [<samp>(13319)</samp>](https://github.com/soybeanjs/soybean-admin/commit/133196f3)
 | 
			
		||||
 | 
			
		||||
###    🎨 Styles
 | 
			
		||||
 | 
			
		||||
- **projects**: format code.  -  by **Azir-11** [<samp>(100e0)</samp>](https://github.com/soybeanjs/soybean-admin/commit/100e0ea5)
 | 
			
		||||
 | 
			
		||||
###    ❤️ Contributors
 | 
			
		||||
 | 
			
		||||
[](https://github.com/soybeanjs)  [](https://github.com/wenyuanw)  [](https://github.com/CyberShen)  [](https://github.com/Lruihao)  [](https://github.com/xiaobao0505)  
 | 
			
		||||
[Azir-11](mailto:2075125282@qq.com), [CyberShen123](mailto:s.lijun@qq.com), [whyang](mailto:whyang9701@gmail.com), [HongxuanG](mailto:1359774872@qq.com), [NicholasLD](mailto:878639947@qq.com), [Junior25306](mailto:dayu429@qq.com)
 | 
			
		||||
 | 
			
		||||
## [v2.0.0-beta.2](https://github.com/soybeanjs/soybean-admin/compare/v2.0.0-beta.1...v2.0.0-beta.2) (2025-10-27)
 | 
			
		||||
 | 
			
		||||
###    🚀 Features
 | 
			
		||||
 | 
			
		||||
- **global-tab**: add support for switching tabs with right mouse button click  -  by @soybeanjs [<samp>(b2c91)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b2c919b6)
 | 
			
		||||
 | 
			
		||||
###    🐞 Bug Fixes
 | 
			
		||||
 | 
			
		||||
- **layout**: fix getSiderWidth  -  by @soybeanjs [<samp>(e471e)</samp>](https://github.com/soybeanjs/soybean-admin/commit/e471e914)
 | 
			
		||||
- **packages**: axios: fix json response. fixed #815  -  by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/815 [<samp>(fd087)</samp>](https://github.com/soybeanjs/soybean-admin/commit/fd087f59)
 | 
			
		||||
- **readme**: update GitHub stars and forks links for gitee  -  by @soybeanjs [<samp>(923eb)</samp>](https://github.com/soybeanjs/soybean-admin/commit/923eb98a)
 | 
			
		||||
- **scripts**: update command to use 'npm-check-updates' instead of 'ncu'  -  by @soybeanjs [<samp>(8dc17)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8dc17e62)
 | 
			
		||||
- **types**: fix proxy types  -  by @soybeanjs [<samp>(12b25)</samp>](https://github.com/soybeanjs/soybean-admin/commit/12b25e0d)
 | 
			
		||||
 | 
			
		||||
###    📖 Documentation
 | 
			
		||||
 | 
			
		||||
- **projects**:
 | 
			
		||||
  - add github trendshift info.  -  by **恕瑞玛的皇帝** [<samp>(e18d3)</samp>](https://github.com/soybeanjs/soybean-admin/commit/e18d3972)
 | 
			
		||||
  - add contribution leaderboard  -  by @wenyuanw [<samp>(01744)</samp>](https://github.com/soybeanjs/soybean-admin/commit/017440c1)
 | 
			
		||||
 | 
			
		||||
###    🏡 Chore
 | 
			
		||||
 | 
			
		||||
- **deps**:
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(e33f9)</samp>](https://github.com/soybeanjs/soybean-admin/commit/e33f944a)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(9fa95)</samp>](https://github.com/soybeanjs/soybean-admin/commit/9fa951aa)
 | 
			
		||||
- **other**:
 | 
			
		||||
  - update the ESLint validation configuration to support more file types.  -  by **Azir-11** [<samp>(8d7f9)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8d7f91dc)
 | 
			
		||||
- **readme**:
 | 
			
		||||
  - remove DartNode sponsorship badge from README files  -  by @soybeanjs [<samp>(33ade)</samp>](https://github.com/soybeanjs/soybean-admin/commit/33ade539)
 | 
			
		||||
 | 
			
		||||
###    ❤️ Contributors
 | 
			
		||||
 | 
			
		||||
[](https://github.com/soybeanjs)  [](https://github.com/wenyuanw)  
 | 
			
		||||
[恕瑞玛的皇帝](mailto:2075125282@qq.com)
 | 
			
		||||
 | 
			
		||||
## [v2.0.0-beta.1](https://github.com/soybeanjs/soybean-admin/compare/v1.3.15...v2.0.0-beta.1) (2025-10-25)
 | 
			
		||||
 | 
			
		||||
###    🚨 Breaking Changes
 | 
			
		||||
 | 
			
		||||
- **hooks**: refactor useTable and enhance type definitions  -  by @soybeanjs [<samp>(8cc51)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8cc5177)
 | 
			
		||||
- **projects**: optimize layout mode, split horizontal mix component into two layouts, and rename the component.  -  by **Azir** [<samp>(b6ac3)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b6ac310)
 | 
			
		||||
- **request**: remove cancelRequest method and related logic from request instances  -  by @soybeanjs [<samp>(b4e12)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b4e1253)
 | 
			
		||||
 | 
			
		||||
###    🚀 Features
 | 
			
		||||
 | 
			
		||||
- **components**:
 | 
			
		||||
  - add the IconTooltip component.  -  by **Azir-11** [<samp>(a55b4)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a55b4dc)
 | 
			
		||||
  - replace NTooltip with IconTooltip and optimize the layout of related components.  -  by **Azir-11** [<samp>(40057)</samp>](https://github.com/soybeanjs/soybean-admin/commit/4005763)
 | 
			
		||||
- **hooks**:
 | 
			
		||||
  - add scrollX computation for total table width in useNaiveTable  -  by @Lruihao [<samp>(358e1)</samp>](https://github.com/soybeanjs/soybean-admin/commit/358e129)
 | 
			
		||||
- **packages**:
 | 
			
		||||
  - materials support slider-tab. closed #823  -  by @CyberShen in https://github.com/soybeanjs/soybean-admin/issues/823 [<samp>(61fa4)</samp>](https://github.com/soybeanjs/soybean-admin/commit/61fa4b7)
 | 
			
		||||
- **projects**:
 | 
			
		||||
  - refactor theme drawer with tabbed layout for better UX.  -  by **Azir** [<samp>(8ba71)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8ba71a0)
 | 
			
		||||
  - Add current time display option for watermark  -  by @wenyuanw in https://github.com/soybeanjs/soybean-admin/issues/772 [<samp>(f238f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/f238fcb)
 | 
			
		||||
  - add 'vertical-hybrid-header-first' layout mode  -  by @wenyuanw [<samp>(b4e5c)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b4e5c6d)
 | 
			
		||||
  - add prompt information for scrolling mode and tab bar caching.  -  by **Azir-11** [<samp>(29a2a)</samp>](https://github.com/soybeanjs/soybean-admin/commit/29a2a5c)
 | 
			
		||||
  - support theme preset function.  -  by **Azir-11** [<samp>(257f1)</samp>](https://github.com/soybeanjs/soybean-admin/commit/257f118)
 | 
			
		||||
  - modify the default value of the reset cache policy to 'refresh'.  -  by **Azir-11** [<samp>(3c0a5)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3c0a528)
 | 
			
		||||
  - optimize tabs cache cleaning strategy. close #820.  -  by **Azir-11** in https://github.com/soybeanjs/soybean-admin/issues/820 [<samp>(ef7ac)</samp>](https://github.com/soybeanjs/soybean-admin/commit/ef7acc6)
 | 
			
		||||
  - support closing tabs with middle mouse button click  -  by @wenyuanw [<samp>(a8d1e)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a8d1e5d)
 | 
			
		||||
  - support set global redius  -  by **CyberShen123** [<samp>(24c6d)</samp>](https://github.com/soybeanjs/soybean-admin/commit/24c6df5)
 | 
			
		||||
  - support set global redius  -  by **CyberShen123** [<samp>(3549c)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3549c4d)
 | 
			
		||||
- **styles**:
 | 
			
		||||
  - add text-autospace property to improve text layout  -  by @wenyuanw [<samp>(345aa)</samp>](https://github.com/soybeanjs/soybean-admin/commit/345aa29)
 | 
			
		||||
 | 
			
		||||
###    🐞 Bug Fixes
 | 
			
		||||
 | 
			
		||||
- **hooks**:
 | 
			
		||||
  - correct chart rendering logic in useEcharts  -  by @soybeanjs [<samp>(8a7cd)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8a7cd59)
 | 
			
		||||
- **packages**:
 | 
			
		||||
  - fix the parsing logic for stored data to ensure correct return of boolean values  -  by @Lruihao [<samp>(9ea56)</samp>](https://github.com/soybeanjs/soybean-admin/commit/9ea56c9)
 | 
			
		||||
  - axios: fix json response. fixed #815  -  by @soybeanjs in https://github.com/soybeanjs/soybean-admin/issues/815 [<samp>(5be86)</samp>](https://github.com/soybeanjs/soybean-admin/commit/5be864a)
 | 
			
		||||
- **projects**:
 | 
			
		||||
  - Fix i18n-ally not working when setting moduleResolution to bundler. fixed #780  -  by @xiaobao0505 in https://github.com/soybeanjs/soybean-admin/issues/780 [<samp>(41191)</samp>](https://github.com/soybeanjs/soybean-admin/commit/41191d5)
 | 
			
		||||
- **styles**:
 | 
			
		||||
  - show light color scrollbar while dark mode is on  -  by **whyang** [<samp>(dac50)</samp>](https://github.com/soybeanjs/soybean-admin/commit/dac5075)
 | 
			
		||||
- **table**:
 | 
			
		||||
  - add type annotations for records in useTable hook  -  by @soybeanjs [<samp>(32b8f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/32b8f99)
 | 
			
		||||
- **types**:
 | 
			
		||||
  - fix proxy types  -  by @soybeanjs [<samp>(3d72f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3d72f95)
 | 
			
		||||
  - fix ts type error  -  by @soybeanjs [<samp>(d5a3a)</samp>](https://github.com/soybeanjs/soybean-admin/commit/d5a3a25)
 | 
			
		||||
 | 
			
		||||
###    🛠 Optimizations
 | 
			
		||||
 | 
			
		||||
- **hooks**:
 | 
			
		||||
  - optimize useEcharts  -  by @soybeanjs [<samp>(936b8)</samp>](https://github.com/soybeanjs/soybean-admin/commit/936b834)
 | 
			
		||||
- **packages**:
 | 
			
		||||
  - remove ofetch package  -  by @soybeanjs [<samp>(abaaa)</samp>](https://github.com/soybeanjs/soybean-admin/commit/abaaa4a)
 | 
			
		||||
- **projects**:
 | 
			
		||||
  - improve theme drawer responsive width for mobile devices  -  by @wenyuanw [<samp>(8439a)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8439a60)
 | 
			
		||||
  - improve robustness of second-level menu key logic  -  by @wenyuanw [<samp>(8b8a2)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8b8a208)
 | 
			
		||||
  - optimize theme drawer width  -  by @soybeanjs [<samp>(81468)</samp>](https://github.com/soybeanjs/soybean-admin/commit/8146858)
 | 
			
		||||
  - optimize api type file  -  by @soybeanjs [<samp>(3a343)</samp>](https://github.com/soybeanjs/soybean-admin/commit/3a343ee)
 | 
			
		||||
  - optimize radius settings  -  by @soybeanjs [<samp>(87a66)</samp>](https://github.com/soybeanjs/soybean-admin/commit/87a66a4)
 | 
			
		||||
- **request**:
 | 
			
		||||
  - enhance request options and response handling with generic types  -  by @soybeanjs [<samp>(50a5c)</samp>](https://github.com/soybeanjs/soybean-admin/commit/50a5cba)
 | 
			
		||||
 | 
			
		||||
###    💅 Refactors
 | 
			
		||||
 | 
			
		||||
- **hooks**:
 | 
			
		||||
  - optimize useContext and update useMixMenuContext  -  by @soybeanjs [<samp>(c9651)</samp>](https://github.com/soybeanjs/soybean-admin/commit/c965140)
 | 
			
		||||
  - streamline column visibility handling in useTable and table components  -  by @soybeanjs [<samp>(ee434)</samp>](https://github.com/soybeanjs/soybean-admin/commit/ee43414)
 | 
			
		||||
  - remove useSignal hook and update exports  -  by @soybeanjs [<samp>(87adc)</samp>](https://github.com/soybeanjs/soybean-admin/commit/87adc35)
 | 
			
		||||
- **menu**:
 | 
			
		||||
  - optimize the margin on the menu  -  by **NicholasLD** [<samp>(d7311)</samp>](https://github.com/soybeanjs/soybean-admin/commit/d731111)
 | 
			
		||||
- **projects**:
 | 
			
		||||
  - remove unnecessary logic in onRouteSwitchWhenLoggedIn  -  by @wenyuanw [<samp>(d6c81)</samp>](https://github.com/soybeanjs/soybean-admin/commit/d6c8142)
 | 
			
		||||
- **request**:
 | 
			
		||||
  - unify response transformation methods and deprecate transformBackendResponse  -  by @soybeanjs [<samp>(f83ee)</samp>](https://github.com/soybeanjs/soybean-admin/commit/f83eefb)
 | 
			
		||||
- **types**:
 | 
			
		||||
  - move Auth and Route namespaces to separate files and clean up api.d.ts  -  by **Azir** [<samp>(d37ce)</samp>](https://github.com/soybeanjs/soybean-admin/commit/d37ce04)
 | 
			
		||||
 | 
			
		||||
###    📖 Documentation
 | 
			
		||||
 | 
			
		||||
- **projects**: add github trendshift info.  -  by **恕瑞玛的皇帝** [<samp>(2a0c9)</samp>](https://github.com/soybeanjs/soybean-admin/commit/2a0c9f1)
 | 
			
		||||
 | 
			
		||||
###    🏡 Chore
 | 
			
		||||
 | 
			
		||||
- **deps**:
 | 
			
		||||
  - update NodeJS and pnpm version requirements in package.json and documentation  -  by **Junior25306** [<samp>(a5c4b)</samp>](https://github.com/soybeanjs/soybean-admin/commit/a5c4b4e)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(5cb1c)</samp>](https://github.com/soybeanjs/soybean-admin/commit/5cb1ceb)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(aeb63)</samp>](https://github.com/soybeanjs/soybean-admin/commit/aeb6369)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(e89b8)</samp>](https://github.com/soybeanjs/soybean-admin/commit/e89b86c)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(c962f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/c962f7b)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(12135)</samp>](https://github.com/soybeanjs/soybean-admin/commit/1213531)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(b041f)</samp>](https://github.com/soybeanjs/soybean-admin/commit/b041fdd)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(d567c)</samp>](https://github.com/soybeanjs/soybean-admin/commit/d567c05)
 | 
			
		||||
  - update deps  -  by @soybeanjs [<samp>(6cbf5)</samp>](https://github.com/soybeanjs/soybean-admin/commit/6cbf570)
 | 
			
		||||
- **other**:
 | 
			
		||||
  - update the ESLint validation configuration to support more file types.  -  by **Azir-11** [<samp>(be8f9)</samp>](https://github.com/soybeanjs/soybean-admin/commit/be8f915)
 | 
			
		||||
- **packages**:
 | 
			
		||||
  - update Vite version to 7 in package.json and documentation.  -  by **Azir** [<samp>(03dd6)</samp>](https://github.com/soybeanjs/soybean-admin/commit/03dd64c)
 | 
			
		||||
  - add picomatch to fix scripts  -  by @soybeanjs [<samp>(805c3)</samp>](https://github.com/soybeanjs/soybean-admin/commit/805c338)
 | 
			
		||||
- **vscode**:
 | 
			
		||||
  - remove unused vue.server.hybridMode setting from .vscode/settings.json  -  by @soybeanjs [<samp>(13319)</samp>](https://github.com/soybeanjs/soybean-admin/commit/133196f)
 | 
			
		||||
 | 
			
		||||
###    🎨 Styles
 | 
			
		||||
 | 
			
		||||
- **projects**: format code.  -  by **Azir-11** [<samp>(100e0)</samp>](https://github.com/soybeanjs/soybean-admin/commit/100e0ea)
 | 
			
		||||
 | 
			
		||||
###    ❤️ Contributors
 | 
			
		||||
 | 
			
		||||
[](https://github.com/soybeanjs)  [](https://github.com/wenyuanw)  [](https://github.com/CyberShen)  [](https://github.com/Lruihao)  [](https://github.com/xiaobao0505)  
 | 
			
		||||
[CyberShen123](mailto:s.lijun@qq.com), [whyang](mailto:whyang9701@gmail.com), [HongxuanG](mailto:1359774872@qq.com), [Azir-11](mailto:2075125282@qq.com), [NicholasLD](mailto:878639947@qq.com), [Junior25306](mailto:dayu429@qq.com)
 | 
			
		||||
 | 
			
		||||
## [v1.3.15](https://github.com/soybeanjs/soybean-admin/compare/v1.3.14...v1.3.15) (2025-06-24)
 | 
			
		||||
 | 
			
		||||
###    🚀 Features
 | 
			
		||||
 
 | 
			
		||||
@@ -7,14 +7,15 @@
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
[](./LICENSE)
 | 
			
		||||
[](https://github.com/soybeanjs/soybean-admin)
 | 
			
		||||
[](https://github.com/soybeanjs/soybean-admin)
 | 
			
		||||
[](https://github.com/soybeanjs/soybean-admin)
 | 
			
		||||
[](https://github.com/soybeanjs/soybean-admin)
 | 
			
		||||
[](https://gitee.com/honghuangdc/soybean-admin)
 | 
			
		||||
[](https://gitcode.com/soybeanjs/soybean-admin)
 | 
			
		||||
[](https://dartnode.com "Powered by DartNode - Free VPS for Open Source")
 | 
			
		||||
 | 
			
		||||
<a href="https://hellogithub.com/repository/1298f27d5fe54959a16cf9686516ddb3" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=1298f27d5fe54959a16cf9686516ddb3&claim_uid=IiDXWmP4TEntjbV" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
 | 
			
		||||
 | 
			
		||||
<div style="display: flex; gap: 12px; align-items: center;">
 | 
			
		||||
  <a href="https://trendshift.io/repositories/7963" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7963" alt="soybeanjs%2Fsoybean-admin | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
 | 
			
		||||
  <a href="https://hellogithub.com/repository/1298f27d5fe54959a16cf9686516ddb3" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=1298f27d5fe54959a16cf9686516ddb3&claim_uid=IiDXWmP4TEntjbV" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
> [!NOTE]
 | 
			
		||||
> If you think `SoybeanAdmin` is helpful to you, or you like our project, please give us a ⭐️ on GitHub. Your support is the driving force for us to continue to improve and add new features! Thank you for your support!
 | 
			
		||||
@@ -27,12 +28,12 @@
 | 
			
		||||
 | 
			
		||||
## Introduction
 | 
			
		||||
 | 
			
		||||
[`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) is a clean, elegant, beautiful and powerful admin template, based on the latest front-end technology stack, including Vue3, Vite6, TypeScript, Pinia and UnoCSS. It has built-in rich theme configuration and components, strict code specifications, and an automated file routing system. In addition, it also uses the online mock data solution based on ApiFox. `SoybeanAdmin` provides you with a one-stop admin solution, no additional configuration, and out of the box. It is also a best practice for learning cutting-edge technologies quickly.
 | 
			
		||||
[`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) is a clean, elegant, beautiful and powerful admin template, based on the latest front-end technology stack, including Vue3, Vite7, TypeScript, Pinia and UnoCSS. It has built-in rich theme configuration and components, strict code specifications, and an automated file routing system. In addition, it also uses the online mock data solution based on ApiFox. `SoybeanAdmin` provides you with a one-stop admin solution, no additional configuration, and out of the box. It is also a best practice for learning cutting-edge technologies quickly.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Features
 | 
			
		||||
 | 
			
		||||
- **Cutting-edge technology application**: using the latest popular technology stack such as Vue3, Vite6, TypeScript, Pinia and UnoCSS.
 | 
			
		||||
- **Cutting-edge technology application**: using the latest popular technology stack such as Vue3, Vite7, TypeScript, Pinia and UnoCSS.
 | 
			
		||||
- **Clear project architecture**: using pnpm monorepo architecture, clear structure, elegant and easy to understand.
 | 
			
		||||
- **Strict code specifications**: follow the [SoybeanJS specification](https://docs.soybeanjs.cn/standard), integrate eslint, prettier and simple-git-hooks to ensure the code is standardized.
 | 
			
		||||
- **TypeScript**: support strict type checking to improve code maintainability.
 | 
			
		||||
@@ -99,8 +100,8 @@
 | 
			
		||||
Make sure your environment meets the following requirements:
 | 
			
		||||
 | 
			
		||||
- **git**: you need git to clone and manage project versions.
 | 
			
		||||
- **NodeJS**: >=18.12.0, recommended 18.19.0 or higher.
 | 
			
		||||
- **pnpm**: >= 8.7.0, recommended 8.14.0 or higher.
 | 
			
		||||
- **NodeJS**: >=20.19.0, recommended 20.19.0 or higher.
 | 
			
		||||
- **pnpm**: >= 10.5.0, recommended 10.5.0 or higher.
 | 
			
		||||
 | 
			
		||||
**Clone Project**
 | 
			
		||||
 | 
			
		||||
@@ -179,6 +180,14 @@ Thanks the following people for their contributions. If you want to contribute t
 | 
			
		||||
  <img src="https://contrib.rocks/image?repo=soybeanjs/soybean-admin" />
 | 
			
		||||
</a>
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
Here are the most active contributors from the past year. Thank you all for your support, which has enabled the project's continued development.
 | 
			
		||||
 | 
			
		||||
<a href="https://openomy.com/soybeanjs/soybean-admin" target="_blank" style="display: block; width: 100%;" align="center">
 | 
			
		||||
  <img src="https://www.openomy.com/svg?repo=soybeanjs/soybean-admin&chart=list&latestMonth=12" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
 | 
			
		||||
</a>
 | 
			
		||||
 | 
			
		||||
## Communication
 | 
			
		||||
 | 
			
		||||
`SoybeanAdmin` is a completely open source and free project, helping developers to develop medium and large-scale management systems more conveniently. It also provides WeChat and QQ communication groups. If you have any questions, please feel free to ask in the group.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								README.md
									
									
									
									
									
								
							@@ -7,13 +7,15 @@
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
[](./LICENSE)
 | 
			
		||||
[](https://github.com/soybeanjs/soybean-admin)
 | 
			
		||||
[](https://github.com/soybeanjs/soybean-admin)
 | 
			
		||||
[](https://github.com/soybeanjs/soybean-admin)
 | 
			
		||||
[](https://github.com/soybeanjs/soybean-admin)
 | 
			
		||||
[](https://gitee.com/honghuangdc/soybean-admin)
 | 
			
		||||
[](https://gitcode.com/soybeanjs/soybean-admin)
 | 
			
		||||
[](https://dartnode.com "Powered by DartNode - Free VPS for Open Source")
 | 
			
		||||
 | 
			
		||||
<a href="https://hellogithub.com/repository/1298f27d5fe54959a16cf9686516ddb3" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=1298f27d5fe54959a16cf9686516ddb3&claim_uid=IiDXWmP4TEntjbV" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
 | 
			
		||||
<div style="display: flex; gap: 12px; align-items: center;">
 | 
			
		||||
  <a href="https://trendshift.io/repositories/7963" target="_blank"><img src="https://trendshift.io/api/badge/repositories/7963" alt="soybeanjs%2Fsoybean-admin | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
 | 
			
		||||
  <a href="https://hellogithub.com/repository/1298f27d5fe54959a16cf9686516ddb3" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=1298f27d5fe54959a16cf9686516ddb3&claim_uid=IiDXWmP4TEntjbV" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
> [!NOTE]
 | 
			
		||||
> 如果您觉得 `SoybeanAdmin`对您有所帮助,或者您喜欢我们的项目,请在 GitHub 上给我们一个 ⭐️。您的支持是我们持续改进和增加新功能的动力!感谢您的支持!
 | 
			
		||||
@@ -26,11 +28,11 @@
 | 
			
		||||
 | 
			
		||||
## 简介
 | 
			
		||||
 | 
			
		||||
[`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) 是一个清新优雅、高颜值且功能强大的后台管理模板,基于最新的前端技术栈,包括 Vue3, Vite6, TypeScript, Pinia 和 UnoCSS。它内置了丰富的主题配置和组件,代码规范严谨,实现了自动化的文件路由系统。此外,它还采用了基于 ApiFox 的在线Mock数据方案。`SoybeanAdmin` 为您提供了一站式的后台管理解决方案,无需额外配置,开箱即用。同样是一个快速学习前沿技术的最佳实践。
 | 
			
		||||
[`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) 是一个清新优雅、高颜值且功能强大的后台管理模板,基于最新的前端技术栈,包括 Vue3, Vite7, TypeScript, Pinia 和 UnoCSS。它内置了丰富的主题配置和组件,代码规范严谨,实现了自动化的文件路由系统。此外,它还采用了基于 ApiFox 的在线Mock数据方案。`SoybeanAdmin` 为您提供了一站式的后台管理解决方案,无需额外配置,开箱即用。同样是一个快速学习前沿技术的最佳实践。
 | 
			
		||||
 | 
			
		||||
## 特性
 | 
			
		||||
 | 
			
		||||
- **前沿技术应用**:采用 Vue3, Vite6, TypeScript, Pinia 和 UnoCSS 等最新流行的技术栈。
 | 
			
		||||
- **前沿技术应用**:采用 Vue3, Vite7, TypeScript, Pinia 和 UnoCSS 等最新流行的技术栈。
 | 
			
		||||
- **清晰的项目架构**:采用 pnpm monorepo 架构,结构清晰,优雅易懂。
 | 
			
		||||
- **严格的代码规范**:遵循 [SoybeanJS 规范](https://docs.soybeanjs.cn/zh/standard),集成了eslint, prettier 和 simple-git-hooks,保证代码的规范性。
 | 
			
		||||
- **TypeScript**: 支持严格的类型检查,提高代码的可维护性。
 | 
			
		||||
@@ -124,8 +126,8 @@
 | 
			
		||||
确保你的环境满足以下要求:
 | 
			
		||||
 | 
			
		||||
- **git**: 你需要git来克隆和管理项目版本。
 | 
			
		||||
- **NodeJS**: >=18.12.0,推荐 18.19.0 或更高。
 | 
			
		||||
- **pnpm**: >= 8.7.0,推荐 8.14.0 或更高。
 | 
			
		||||
- **NodeJS**: >=20.19.0,推荐 20.19.0 或更高。
 | 
			
		||||
- **pnpm**: >= 10.5.0,推荐 10.5.0 或更高。
 | 
			
		||||
 | 
			
		||||
**克隆项目**
 | 
			
		||||
 | 
			
		||||
@@ -206,6 +208,14 @@ pnpm build
 | 
			
		||||
  <img src="https://contrib.rocks/image?repo=soybeanjs/soybean-admin" />
 | 
			
		||||
</a>
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
以下是近一年中活跃度较高的贡献者,感谢各位的支持,让项目得以持续发展。
 | 
			
		||||
 | 
			
		||||
<a href="https://openomy.com/soybeanjs/soybean-admin" target="_blank" style="display: block; width: 100%;" align="center">
 | 
			
		||||
  <img src="https://www.openomy.com/svg?repo=soybeanjs/soybean-admin&chart=list&latestMonth=12" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
 | 
			
		||||
</a>
 | 
			
		||||
 | 
			
		||||
## 交流
 | 
			
		||||
 | 
			
		||||
`SoybeanAdmin` 是完全开源免费的项目,在帮助开发者更方便地进行中大型管理系统开发,同时也提供微信和 QQ 交流群,使用问题欢迎在群内提问。
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import type { HttpProxy, ProxyOptions } from 'vite';
 | 
			
		||||
import type { ProxyOptions } from 'vite';
 | 
			
		||||
import { bgRed, bgYellow, green, lightBlue } from 'kolorist';
 | 
			
		||||
import { consola } from 'consola';
 | 
			
		||||
import { createServiceConfig } from '../../src/utils/service';
 | 
			
		||||
@@ -33,7 +33,7 @@ function createProxyItem(item: App.Service.ServiceConfigItem, enableLog: boolean
 | 
			
		||||
  proxy[item.proxyPattern] = {
 | 
			
		||||
    target: item.baseURL,
 | 
			
		||||
    changeOrigin: true,
 | 
			
		||||
    configure: (_proxy: HttpProxy.Server, options: ProxyOptions) => {
 | 
			
		||||
    configure: (_proxy, options) => {
 | 
			
		||||
      _proxy.on('proxyReq', (_proxyReq, req, _res) => {
 | 
			
		||||
        if (!enableLog) return;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										70
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										70
									
								
								package.json
									
									
									
									
									
								
							@@ -1,8 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "soybean-admin",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "version": "1.3.15",
 | 
			
		||||
  "description": "A fresh and elegant admin template, based on Vue3、Vite6、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite6、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。",
 | 
			
		||||
  "version": "2.0.0",
 | 
			
		||||
  "description": "A fresh and elegant admin template, based on Vue3、Vite7、TypeScript、NaiveUI and UnoCSS. 一个基于Vue3、Vite7、TypeScript、NaiveUI and UnoCSS的清新优雅的中后台模版。",
 | 
			
		||||
  "author": {
 | 
			
		||||
    "name": "Soybean",
 | 
			
		||||
    "email": "soybeanjs@outlook.com",
 | 
			
		||||
@@ -19,7 +19,7 @@
 | 
			
		||||
  "keywords": [
 | 
			
		||||
    "Vue3 admin ",
 | 
			
		||||
    "vue-admin-template",
 | 
			
		||||
    "Vite6",
 | 
			
		||||
    "Vite7",
 | 
			
		||||
    "TypeScript",
 | 
			
		||||
    "naive-ui",
 | 
			
		||||
    "naive-ui-admin",
 | 
			
		||||
@@ -27,8 +27,8 @@
 | 
			
		||||
    "UnoCSS"
 | 
			
		||||
  ],
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": ">=18.20.0",
 | 
			
		||||
    "pnpm": ">=8.7.0"
 | 
			
		||||
    "node": ">=20.19.0",
 | 
			
		||||
    "pnpm": ">=10.5.0"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "build": "vite build --mode prod",
 | 
			
		||||
@@ -54,53 +54,53 @@
 | 
			
		||||
    "@sa/hooks": "workspace:*",
 | 
			
		||||
    "@sa/materials": "workspace:*",
 | 
			
		||||
    "@sa/utils": "workspace:*",
 | 
			
		||||
    "@vueuse/core": "13.4.0",
 | 
			
		||||
    "@vueuse/core": "14.0.0",
 | 
			
		||||
    "clipboard": "2.0.11",
 | 
			
		||||
    "dayjs": "1.11.13",
 | 
			
		||||
    "dayjs": "1.11.19",
 | 
			
		||||
    "defu": "6.1.4",
 | 
			
		||||
    "echarts": "5.6.0",
 | 
			
		||||
    "echarts": "6.0.0",
 | 
			
		||||
    "json5": "2.2.3",
 | 
			
		||||
    "naive-ui": "2.42.0",
 | 
			
		||||
    "naive-ui": "2.43.1",
 | 
			
		||||
    "nprogress": "0.2.0",
 | 
			
		||||
    "pinia": "3.0.3",
 | 
			
		||||
    "tailwind-merge": "3.3.1",
 | 
			
		||||
    "vue": "3.5.17",
 | 
			
		||||
    "vue": "3.5.22",
 | 
			
		||||
    "vue-draggable-plus": "0.6.0",
 | 
			
		||||
    "vue-i18n": "11.1.7",
 | 
			
		||||
    "vue-router": "4.5.1"
 | 
			
		||||
    "vue-i18n": "11.1.12",
 | 
			
		||||
    "vue-router": "4.6.3"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@elegant-router/vue": "0.3.8",
 | 
			
		||||
    "@iconify/json": "2.2.352",
 | 
			
		||||
    "@iconify/json": "2.2.402",
 | 
			
		||||
    "@sa/scripts": "workspace:*",
 | 
			
		||||
    "@sa/uno-preset": "workspace:*",
 | 
			
		||||
    "@soybeanjs/eslint-config": "1.6.1",
 | 
			
		||||
    "@types/node": "24.0.3",
 | 
			
		||||
    "@soybeanjs/eslint-config": "1.7.1",
 | 
			
		||||
    "@types/node": "24.9.2",
 | 
			
		||||
    "@types/nprogress": "0.2.3",
 | 
			
		||||
    "@unocss/eslint-config": "66.2.3",
 | 
			
		||||
    "@unocss/preset-icons": "66.2.3",
 | 
			
		||||
    "@unocss/preset-uno": "66.2.3",
 | 
			
		||||
    "@unocss/transformer-directives": "66.2.3",
 | 
			
		||||
    "@unocss/transformer-variant-group": "66.2.3",
 | 
			
		||||
    "@unocss/vite": "66.2.3",
 | 
			
		||||
    "@vitejs/plugin-vue": "6.0.0",
 | 
			
		||||
    "@vitejs/plugin-vue-jsx": "5.0.0",
 | 
			
		||||
    "@unocss/eslint-config": "66.5.4",
 | 
			
		||||
    "@unocss/preset-icons": "66.5.4",
 | 
			
		||||
    "@unocss/preset-uno": "66.5.4",
 | 
			
		||||
    "@unocss/transformer-directives": "66.5.4",
 | 
			
		||||
    "@unocss/transformer-variant-group": "66.5.4",
 | 
			
		||||
    "@unocss/vite": "66.5.4",
 | 
			
		||||
    "@vitejs/plugin-vue": "6.0.1",
 | 
			
		||||
    "@vitejs/plugin-vue-jsx": "5.1.1",
 | 
			
		||||
    "consola": "3.4.2",
 | 
			
		||||
    "eslint": "9.29.0",
 | 
			
		||||
    "eslint-plugin-vue": "10.2.0",
 | 
			
		||||
    "eslint": "9.39.0",
 | 
			
		||||
    "eslint-plugin-vue": "10.5.1",
 | 
			
		||||
    "kolorist": "1.8.0",
 | 
			
		||||
    "sass": "1.89.2",
 | 
			
		||||
    "simple-git-hooks": "2.13.0",
 | 
			
		||||
    "tsx": "4.20.3",
 | 
			
		||||
    "typescript": "5.8.3",
 | 
			
		||||
    "unplugin-icons": "22.1.0",
 | 
			
		||||
    "unplugin-vue-components": "28.7.0",
 | 
			
		||||
    "vite": "7.0.0",
 | 
			
		||||
    "sass": "1.93.3",
 | 
			
		||||
    "simple-git-hooks": "2.13.1",
 | 
			
		||||
    "tsx": "4.20.6",
 | 
			
		||||
    "typescript": "5.9.3",
 | 
			
		||||
    "unplugin-icons": "22.5.0",
 | 
			
		||||
    "unplugin-vue-components": "30.0.0",
 | 
			
		||||
    "vite": "7.1.12",
 | 
			
		||||
    "vite-plugin-progress": "0.0.7",
 | 
			
		||||
    "vite-plugin-svg-icons": "2.0.1",
 | 
			
		||||
    "vite-plugin-vue-devtools": "7.7.7",
 | 
			
		||||
    "vue-eslint-parser": "10.1.4",
 | 
			
		||||
    "vue-tsc": "2.2.10"
 | 
			
		||||
    "vite-plugin-vue-devtools": "8.0.3",
 | 
			
		||||
    "vue-eslint-parser": "10.2.0",
 | 
			
		||||
    "vue-tsc": "3.1.2"
 | 
			
		||||
  },
 | 
			
		||||
  "simple-git-hooks": {
 | 
			
		||||
    "commit-msg": "pnpm sa git-commit-verify",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@sa/alova",
 | 
			
		||||
  "version": "1.3.15",
 | 
			
		||||
  "version": "2.0.0",
 | 
			
		||||
  "exports": {
 | 
			
		||||
    ".": "./src/index.ts",
 | 
			
		||||
    "./fetch": "./src/fetch.ts",
 | 
			
		||||
@@ -15,6 +15,6 @@
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@alova/mock": "2.0.17",
 | 
			
		||||
    "@sa/utils": "workspace:*",
 | 
			
		||||
    "alova": "3.3.3"
 | 
			
		||||
    "alova": "3.3.4"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@sa/axios",
 | 
			
		||||
  "version": "1.3.15",
 | 
			
		||||
  "version": "2.0.0",
 | 
			
		||||
  "exports": {
 | 
			
		||||
    ".": "./src/index.ts"
 | 
			
		||||
  },
 | 
			
		||||
@@ -11,7 +11,7 @@
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@sa/utils": "workspace:*",
 | 
			
		||||
    "axios": "1.10.0",
 | 
			
		||||
    "axios": "1.13.1",
 | 
			
		||||
    "axios-retry": "4.5.0",
 | 
			
		||||
    "qs": "6.14.0"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import type { AxiosResponse, CreateAxiosDefaults, InternalAxiosRequestConfig } f
 | 
			
		||||
import axiosRetry from 'axios-retry';
 | 
			
		||||
import { nanoid } from '@sa/utils';
 | 
			
		||||
import { createAxiosConfig, createDefaultOptions, createRetryOptions } from './options';
 | 
			
		||||
import { transformResponse } from './shared';
 | 
			
		||||
import { BACKEND_ERROR_CODE, REQUEST_ID_KEY } from './constant';
 | 
			
		||||
import type {
 | 
			
		||||
  CustomAxiosRequestConfig,
 | 
			
		||||
@@ -13,11 +14,12 @@ import type {
 | 
			
		||||
  ResponseType
 | 
			
		||||
} from './type';
 | 
			
		||||
 | 
			
		||||
function createCommonRequest<ResponseData = any>(
 | 
			
		||||
  axiosConfig?: CreateAxiosDefaults,
 | 
			
		||||
  options?: Partial<RequestOption<ResponseData>>
 | 
			
		||||
) {
 | 
			
		||||
  const opts = createDefaultOptions<ResponseData>(options);
 | 
			
		||||
function createCommonRequest<
 | 
			
		||||
  ResponseData,
 | 
			
		||||
  ApiData = ResponseData,
 | 
			
		||||
  State extends Record<string, unknown> = Record<string, unknown>
 | 
			
		||||
>(axiosConfig?: CreateAxiosDefaults, options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
 | 
			
		||||
  const opts = createDefaultOptions<ResponseData, ApiData, State>(options);
 | 
			
		||||
 | 
			
		||||
  const axiosConf = createAxiosConfig(axiosConfig);
 | 
			
		||||
  const instance = axios.create(axiosConf);
 | 
			
		||||
@@ -52,6 +54,8 @@ function createCommonRequest<ResponseData = any>(
 | 
			
		||||
    async response => {
 | 
			
		||||
      const responseType: ResponseType = (response.config?.responseType as ResponseType) || 'json';
 | 
			
		||||
 | 
			
		||||
      await transformResponse(response);
 | 
			
		||||
 | 
			
		||||
      if (responseType !== 'json' || opts.isBackendSuccess(response)) {
 | 
			
		||||
        return Promise.resolve(response);
 | 
			
		||||
      }
 | 
			
		||||
@@ -80,14 +84,6 @@ function createCommonRequest<ResponseData = any>(
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  function cancelRequest(requestId: string) {
 | 
			
		||||
    const abortController = abortControllerMap.get(requestId);
 | 
			
		||||
    if (abortController) {
 | 
			
		||||
      abortController.abort();
 | 
			
		||||
      abortControllerMap.delete(requestId);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function cancelAllRequest() {
 | 
			
		||||
    abortControllerMap.forEach(abortController => {
 | 
			
		||||
      abortController.abort();
 | 
			
		||||
@@ -98,7 +94,6 @@ function createCommonRequest<ResponseData = any>(
 | 
			
		||||
  return {
 | 
			
		||||
    instance,
 | 
			
		||||
    opts,
 | 
			
		||||
    cancelRequest,
 | 
			
		||||
    cancelAllRequest
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@@ -109,27 +104,27 @@ function createCommonRequest<ResponseData = any>(
 | 
			
		||||
 * @param axiosConfig axios config
 | 
			
		||||
 * @param options request options
 | 
			
		||||
 */
 | 
			
		||||
export function createRequest<ResponseData = any, State = Record<string, unknown>>(
 | 
			
		||||
export function createRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
 | 
			
		||||
  axiosConfig?: CreateAxiosDefaults,
 | 
			
		||||
  options?: Partial<RequestOption<ResponseData>>
 | 
			
		||||
  options?: Partial<RequestOption<ResponseData, ApiData, State>>
 | 
			
		||||
) {
 | 
			
		||||
  const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
 | 
			
		||||
  const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options);
 | 
			
		||||
 | 
			
		||||
  const request: RequestInstance<State> = async function request<T = any, R extends ResponseType = 'json'>(
 | 
			
		||||
    config: CustomAxiosRequestConfig
 | 
			
		||||
  ) {
 | 
			
		||||
  const request: RequestInstance<ApiData, State> = async function request<
 | 
			
		||||
    T extends ApiData = ApiData,
 | 
			
		||||
    R extends ResponseType = 'json'
 | 
			
		||||
  >(config: CustomAxiosRequestConfig) {
 | 
			
		||||
    const response: AxiosResponse<ResponseData> = await instance(config);
 | 
			
		||||
 | 
			
		||||
    const responseType = response.config?.responseType || 'json';
 | 
			
		||||
 | 
			
		||||
    if (responseType === 'json') {
 | 
			
		||||
      return opts.transformBackendResponse(response);
 | 
			
		||||
      return opts.transform(response);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return response.data as MappedType<R, T>;
 | 
			
		||||
  } as RequestInstance<State>;
 | 
			
		||||
  } as RequestInstance<ApiData, State>;
 | 
			
		||||
 | 
			
		||||
  request.cancelRequest = cancelRequest;
 | 
			
		||||
  request.cancelAllRequest = cancelAllRequest;
 | 
			
		||||
  request.state = {} as State;
 | 
			
		||||
 | 
			
		||||
@@ -144,14 +139,14 @@ export function createRequest<ResponseData = any, State = Record<string, unknown
 | 
			
		||||
 * @param axiosConfig axios config
 | 
			
		||||
 * @param options request options
 | 
			
		||||
 */
 | 
			
		||||
export function createFlatRequest<ResponseData = any, State = Record<string, unknown>>(
 | 
			
		||||
export function createFlatRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
 | 
			
		||||
  axiosConfig?: CreateAxiosDefaults,
 | 
			
		||||
  options?: Partial<RequestOption<ResponseData>>
 | 
			
		||||
  options?: Partial<RequestOption<ResponseData, ApiData, State>>
 | 
			
		||||
) {
 | 
			
		||||
  const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
 | 
			
		||||
  const { instance, opts, cancelAllRequest } = createCommonRequest<ResponseData, ApiData, State>(axiosConfig, options);
 | 
			
		||||
 | 
			
		||||
  const flatRequest: FlatRequestInstance<State, ResponseData> = async function flatRequest<
 | 
			
		||||
    T = any,
 | 
			
		||||
  const flatRequest: FlatRequestInstance<ResponseData, ApiData, State> = async function flatRequest<
 | 
			
		||||
    T extends ApiData = ApiData,
 | 
			
		||||
    R extends ResponseType = 'json'
 | 
			
		||||
  >(config: CustomAxiosRequestConfig) {
 | 
			
		||||
    try {
 | 
			
		||||
@@ -160,20 +155,21 @@ export function createFlatRequest<ResponseData = any, State = Record<string, unk
 | 
			
		||||
      const responseType = response.config?.responseType || 'json';
 | 
			
		||||
 | 
			
		||||
      if (responseType === 'json') {
 | 
			
		||||
        const data = opts.transformBackendResponse(response);
 | 
			
		||||
        const data = await opts.transform(response);
 | 
			
		||||
 | 
			
		||||
        return { data, error: null, response };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return { data: response.data as MappedType<R, T>, error: null };
 | 
			
		||||
      return { data: response.data as MappedType<R, T>, error: null, response };
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      return { data: null, error, response: (error as AxiosError<ResponseData>).response };
 | 
			
		||||
    }
 | 
			
		||||
  } as FlatRequestInstance<State, ResponseData>;
 | 
			
		||||
  } as FlatRequestInstance<ResponseData, ApiData, State>;
 | 
			
		||||
 | 
			
		||||
  flatRequest.cancelRequest = cancelRequest;
 | 
			
		||||
  flatRequest.cancelAllRequest = cancelAllRequest;
 | 
			
		||||
  flatRequest.state = {} as State;
 | 
			
		||||
  flatRequest.state = {
 | 
			
		||||
    ...opts.defaultState
 | 
			
		||||
  } as State;
 | 
			
		||||
 | 
			
		||||
  return flatRequest;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,15 +4,27 @@ import { stringify } from 'qs';
 | 
			
		||||
import { isHttpSuccess } from './shared';
 | 
			
		||||
import type { RequestOption } from './type';
 | 
			
		||||
 | 
			
		||||
export function createDefaultOptions<ResponseData = any>(options?: Partial<RequestOption<ResponseData>>) {
 | 
			
		||||
  const opts: RequestOption<ResponseData> = {
 | 
			
		||||
export function createDefaultOptions<
 | 
			
		||||
  ResponseData,
 | 
			
		||||
  ApiData = ResponseData,
 | 
			
		||||
  State extends Record<string, unknown> = Record<string, unknown>
 | 
			
		||||
>(options?: Partial<RequestOption<ResponseData, ApiData, State>>) {
 | 
			
		||||
  const opts: RequestOption<ResponseData, ApiData, State> = {
 | 
			
		||||
    defaultState: {} as State,
 | 
			
		||||
    transform: async response => response.data as unknown as ApiData,
 | 
			
		||||
    transformBackendResponse: async response => response.data as unknown as ApiData,
 | 
			
		||||
    onRequest: async config => config,
 | 
			
		||||
    isBackendSuccess: _response => true,
 | 
			
		||||
    onBackendFail: async () => {},
 | 
			
		||||
    transformBackendResponse: async response => response.data,
 | 
			
		||||
    onError: async () => {}
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if (options?.transform) {
 | 
			
		||||
    opts.transform = options.transform;
 | 
			
		||||
  } else {
 | 
			
		||||
    opts.transform = options?.transformBackendResponse || opts.transform;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Object.assign(opts, options);
 | 
			
		||||
 | 
			
		||||
  return opts;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import type { AxiosHeaderValue, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
 | 
			
		||||
import type { ResponseType } from './type';
 | 
			
		||||
 | 
			
		||||
export function getContentType(config: InternalAxiosRequestConfig) {
 | 
			
		||||
  const contentType: AxiosHeaderValue = config.headers?.['Content-Type'] || 'application/json';
 | 
			
		||||
@@ -26,3 +27,53 @@ export function isResponseJson(response: AxiosResponse) {
 | 
			
		||||
 | 
			
		||||
  return responseType === 'json' || responseType === undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function transformResponse(response: AxiosResponse) {
 | 
			
		||||
  const responseType: ResponseType = (response.config?.responseType as ResponseType) || 'json';
 | 
			
		||||
  if (responseType === 'json') return;
 | 
			
		||||
 | 
			
		||||
  const isJson = response.headers['content-type']?.includes('application/json');
 | 
			
		||||
  if (!isJson) return;
 | 
			
		||||
 | 
			
		||||
  if (responseType === 'blob') {
 | 
			
		||||
    await transformBlobToJson(response);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (responseType === 'arrayBuffer') {
 | 
			
		||||
    await transformArrayBufferToJson(response);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function transformBlobToJson(response: AxiosResponse) {
 | 
			
		||||
  try {
 | 
			
		||||
    let data = response.data;
 | 
			
		||||
 | 
			
		||||
    if (typeof data === 'string') {
 | 
			
		||||
      data = JSON.parse(data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (Object.prototype.toString.call(data) === '[object Blob]') {
 | 
			
		||||
      const json = await data.text();
 | 
			
		||||
      data = JSON.parse(json);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    response.data = data;
 | 
			
		||||
  } catch {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function transformArrayBufferToJson(response: AxiosResponse) {
 | 
			
		||||
  try {
 | 
			
		||||
    let data = response.data;
 | 
			
		||||
 | 
			
		||||
    if (typeof data === 'string') {
 | 
			
		||||
      data = JSON.parse(data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (Object.prototype.toString.call(data) === '[object ArrayBuffer]') {
 | 
			
		||||
      const json = new TextDecoder().decode(data);
 | 
			
		||||
      data = JSON.parse(json);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    response.data = data;
 | 
			
		||||
  } catch {}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,30 @@ export type ContentType =
 | 
			
		||||
  | 'application/x-www-form-urlencoded'
 | 
			
		||||
  | 'application/octet-stream';
 | 
			
		||||
 | 
			
		||||
export interface RequestOption<ResponseData = any> {
 | 
			
		||||
export type ResponseTransform<Input = any, Output = any> = (input: Input) => Output | Promise<Output>;
 | 
			
		||||
 | 
			
		||||
export interface RequestOption<
 | 
			
		||||
  ResponseData,
 | 
			
		||||
  ApiData = ResponseData,
 | 
			
		||||
  State extends Record<string, unknown> = Record<string, unknown>
 | 
			
		||||
> {
 | 
			
		||||
  /**
 | 
			
		||||
   * The default state
 | 
			
		||||
   */
 | 
			
		||||
  defaultState?: State;
 | 
			
		||||
  /**
 | 
			
		||||
   * transform the response data to the api data
 | 
			
		||||
   *
 | 
			
		||||
   * @param response Axios response
 | 
			
		||||
   */
 | 
			
		||||
  transform: ResponseTransform<AxiosResponse<ResponseData>, ApiData>;
 | 
			
		||||
  /**
 | 
			
		||||
   * transform the response data to the api data
 | 
			
		||||
   *
 | 
			
		||||
   * @deprecated use `transform` instead, will be removed in the next major version v3
 | 
			
		||||
   * @param response Axios response
 | 
			
		||||
   */
 | 
			
		||||
  transformBackendResponse: ResponseTransform<AxiosResponse<ResponseData>, ApiData>;
 | 
			
		||||
  /**
 | 
			
		||||
   * The hook before request
 | 
			
		||||
   *
 | 
			
		||||
@@ -35,12 +58,6 @@ export interface RequestOption<ResponseData = any> {
 | 
			
		||||
    response: AxiosResponse<ResponseData>,
 | 
			
		||||
    instance: AxiosInstance
 | 
			
		||||
  ) => Promise<AxiosResponse | null> | Promise<void>;
 | 
			
		||||
  /**
 | 
			
		||||
   * transform backend response when the responseType is json
 | 
			
		||||
   *
 | 
			
		||||
   * @param response Axios response
 | 
			
		||||
   */
 | 
			
		||||
  transformBackendResponse(response: AxiosResponse<ResponseData>): any | Promise<any>;
 | 
			
		||||
  /**
 | 
			
		||||
   * The hook to handle error
 | 
			
		||||
   *
 | 
			
		||||
@@ -68,15 +85,7 @@ export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<Axi
 | 
			
		||||
  responseType?: R;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface RequestInstanceCommon<T> {
 | 
			
		||||
  /**
 | 
			
		||||
   * cancel the request by request id
 | 
			
		||||
   *
 | 
			
		||||
   * if the request provide abort controller sign from config, it will not collect in the abort controller map
 | 
			
		||||
   *
 | 
			
		||||
   * @param requestId
 | 
			
		||||
   */
 | 
			
		||||
  cancelRequest: (requestId: string) => void;
 | 
			
		||||
export interface RequestInstanceCommon<State extends Record<string, unknown>> {
 | 
			
		||||
  /**
 | 
			
		||||
   * cancel all request
 | 
			
		||||
   *
 | 
			
		||||
@@ -84,32 +93,35 @@ export interface RequestInstanceCommon<T> {
 | 
			
		||||
   */
 | 
			
		||||
  cancelAllRequest: () => void;
 | 
			
		||||
  /** you can set custom state in the request instance */
 | 
			
		||||
  state: T;
 | 
			
		||||
  state: State;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** The request instance */
 | 
			
		||||
export interface RequestInstance<S = Record<string, unknown>> extends RequestInstanceCommon<S> {
 | 
			
		||||
  <T = any, R extends ResponseType = 'json'>(config: CustomAxiosRequestConfig<R>): Promise<MappedType<R, T>>;
 | 
			
		||||
export interface RequestInstance<ApiData, State extends Record<string, unknown>> extends RequestInstanceCommon<State> {
 | 
			
		||||
  <T extends ApiData = ApiData, R extends ResponseType = 'json'>(
 | 
			
		||||
    config: CustomAxiosRequestConfig<R>
 | 
			
		||||
  ): Promise<MappedType<R, T>>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type FlatResponseSuccessData<T = any, ResponseData = any> = {
 | 
			
		||||
  data: T;
 | 
			
		||||
export type FlatResponseSuccessData<ResponseData, ApiData> = {
 | 
			
		||||
  data: ApiData;
 | 
			
		||||
  error: null;
 | 
			
		||||
  response: AxiosResponse<ResponseData>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type FlatResponseFailData<ResponseData = any> = {
 | 
			
		||||
export type FlatResponseFailData<ResponseData> = {
 | 
			
		||||
  data: null;
 | 
			
		||||
  error: AxiosError<ResponseData>;
 | 
			
		||||
  response: AxiosResponse<ResponseData>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type FlatResponseData<T = any, ResponseData = any> =
 | 
			
		||||
  | FlatResponseSuccessData<T, ResponseData>
 | 
			
		||||
export type FlatResponseData<ResponseData, ApiData> =
 | 
			
		||||
  | FlatResponseSuccessData<ResponseData, ApiData>
 | 
			
		||||
  | FlatResponseFailData<ResponseData>;
 | 
			
		||||
 | 
			
		||||
export interface FlatRequestInstance<S = Record<string, unknown>, ResponseData = any> extends RequestInstanceCommon<S> {
 | 
			
		||||
  <T = any, R extends ResponseType = 'json'>(
 | 
			
		||||
export interface FlatRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
 | 
			
		||||
  extends RequestInstanceCommon<State> {
 | 
			
		||||
  <T extends ApiData = ApiData, R extends ResponseType = 'json'>(
 | 
			
		||||
    config: CustomAxiosRequestConfig<R>
 | 
			
		||||
  ): Promise<FlatResponseData<MappedType<R, T>, ResponseData>>;
 | 
			
		||||
  ): Promise<FlatResponseData<ResponseData, MappedType<R, T>>>;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@sa/color",
 | 
			
		||||
  "version": "1.3.15",
 | 
			
		||||
  "version": "2.0.0",
 | 
			
		||||
  "exports": {
 | 
			
		||||
    ".": "./src/index.ts"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@sa/hooks",
 | 
			
		||||
  "version": "1.3.15",
 | 
			
		||||
  "version": "2.0.0",
 | 
			
		||||
  "exports": {
 | 
			
		||||
    ".": "./src/index.ts"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,7 @@ import useLoading from './use-loading';
 | 
			
		||||
import useCountDown from './use-count-down';
 | 
			
		||||
import useContext from './use-context';
 | 
			
		||||
import useSvgIconRender from './use-svg-icon-render';
 | 
			
		||||
import useHookTable from './use-table';
 | 
			
		||||
import useTable from './use-table';
 | 
			
		||||
 | 
			
		||||
export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useHookTable };
 | 
			
		||||
 | 
			
		||||
export * from './use-signal';
 | 
			
		||||
export * from './use-table';
 | 
			
		||||
export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useTable };
 | 
			
		||||
export type * from './use-table';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import { inject, provide } from 'vue';
 | 
			
		||||
import type { InjectionKey } from 'vue';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Use context
 | 
			
		||||
@@ -12,7 +11,7 @@ import type { InjectionKey } from 'vue';
 | 
			
		||||
 *   import { ref } from 'vue';
 | 
			
		||||
 *   import { useContext } from '@sa/hooks';
 | 
			
		||||
 *
 | 
			
		||||
 *   export const { setupStore, useStore } = useContext('demo', () => {
 | 
			
		||||
 *   export const [provideDemoContext, useDemoContext] = useContext('demo', () => {
 | 
			
		||||
 *     const count = ref(0);
 | 
			
		||||
 *
 | 
			
		||||
 *     function increment() {
 | 
			
		||||
@@ -35,10 +34,10 @@ import type { InjectionKey } from 'vue';
 | 
			
		||||
 *     <div>A</div>
 | 
			
		||||
 *   </template>
 | 
			
		||||
 *   <script setup lang="ts">
 | 
			
		||||
 *   import { setupStore } from './context';
 | 
			
		||||
 *   import { provideDemoContext } from './context';
 | 
			
		||||
 *
 | 
			
		||||
 *   setupStore();
 | 
			
		||||
 *   // const { increment } = setupStore(); // also can control the store in the parent component
 | 
			
		||||
 *   provideDemoContext();
 | 
			
		||||
 *   // const { increment } = provideDemoContext(); // also can control the store in the parent component
 | 
			
		||||
 *   </script>
 | 
			
		||||
 *   ``` // B.vue
 | 
			
		||||
 *   ```vue
 | 
			
		||||
@@ -46,9 +45,9 @@ import type { InjectionKey } from 'vue';
 | 
			
		||||
 *    <div>B</div>
 | 
			
		||||
 *   </template>
 | 
			
		||||
 *   <script setup lang="ts">
 | 
			
		||||
 *   import { useStore } from './context';
 | 
			
		||||
 *   import { useDemoContext } from './context';
 | 
			
		||||
 *
 | 
			
		||||
 *   const { count, increment } = useStore();
 | 
			
		||||
 *   const { count, increment } = useDemoContext();
 | 
			
		||||
 *   </script>
 | 
			
		||||
 *   ```;
 | 
			
		||||
 *
 | 
			
		||||
@@ -57,40 +56,41 @@ import type { InjectionKey } from 'vue';
 | 
			
		||||
 * @param contextName Context name
 | 
			
		||||
 * @param fn Context function
 | 
			
		||||
 */
 | 
			
		||||
export default function useContext<T extends (...args: any[]) => any>(contextName: string, fn: T) {
 | 
			
		||||
  type Context = ReturnType<T>;
 | 
			
		||||
export default function useContext<Arguments extends Array<any>, T>(
 | 
			
		||||
  contextName: string,
 | 
			
		||||
  composable: (...args: Arguments) => T
 | 
			
		||||
) {
 | 
			
		||||
  const key = Symbol(contextName);
 | 
			
		||||
 | 
			
		||||
  const { useProvide, useInject: useStore } = createContext<Context>(contextName);
 | 
			
		||||
  /**
 | 
			
		||||
   * Injects the context value.
 | 
			
		||||
   *
 | 
			
		||||
   * @param consumerName - The name of the component that is consuming the context. If provided, the component must be
 | 
			
		||||
   *   used within the context provider.
 | 
			
		||||
   * @param defaultValue - The default value to return if the context is not provided.
 | 
			
		||||
   * @returns The context value.
 | 
			
		||||
   */
 | 
			
		||||
  const useInject = <N extends string | null | undefined = undefined>(
 | 
			
		||||
    consumerName?: N,
 | 
			
		||||
    defaultValue?: T
 | 
			
		||||
  ): N extends null | undefined ? T | null : T => {
 | 
			
		||||
    const value = inject(key, defaultValue);
 | 
			
		||||
 | 
			
		||||
  function setupStore(...args: Parameters<T>) {
 | 
			
		||||
    const context: Context = fn(...args);
 | 
			
		||||
    return useProvide(context);
 | 
			
		||||
  }
 | 
			
		||||
    if (consumerName && !value) {
 | 
			
		||||
      throw new Error(`\`${consumerName}\` must be used within \`${contextName}\``);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    /** Setup store in the parent component */
 | 
			
		||||
    setupStore,
 | 
			
		||||
    /** Use store in the child component */
 | 
			
		||||
    useStore
 | 
			
		||||
    // @ts-expect-error - we want to return null if the value is undefined or null
 | 
			
		||||
    return value || null;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Create context */
 | 
			
		||||
function createContext<T>(contextName: string) {
 | 
			
		||||
  const injectKey: InjectionKey<T> = Symbol(contextName);
 | 
			
		||||
  const useProvide = (...args: Arguments) => {
 | 
			
		||||
    const value = composable(...args);
 | 
			
		||||
 | 
			
		||||
  function useProvide(context: T) {
 | 
			
		||||
    provide(injectKey, context);
 | 
			
		||||
    provide(key, value);
 | 
			
		||||
 | 
			
		||||
    return context;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function useInject() {
 | 
			
		||||
    return inject(injectKey) as T;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    useProvide,
 | 
			
		||||
    useInject
 | 
			
		||||
    return value;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return [useProvide, useInject] as const;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,31 +6,31 @@ import type {
 | 
			
		||||
  CreateAxiosDefaults,
 | 
			
		||||
  CustomAxiosRequestConfig,
 | 
			
		||||
  MappedType,
 | 
			
		||||
  RequestInstanceCommon,
 | 
			
		||||
  RequestOption,
 | 
			
		||||
  ResponseType
 | 
			
		||||
} from '@sa/axios';
 | 
			
		||||
import useLoading from './use-loading';
 | 
			
		||||
 | 
			
		||||
export type HookRequestInstanceResponseSuccessData<T = any> = {
 | 
			
		||||
  data: Ref<T>;
 | 
			
		||||
export type HookRequestInstanceResponseSuccessData<ApiData> = {
 | 
			
		||||
  data: Ref<ApiData>;
 | 
			
		||||
  error: Ref<null>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type HookRequestInstanceResponseFailData<ResponseData = any> = {
 | 
			
		||||
export type HookRequestInstanceResponseFailData<ResponseData> = {
 | 
			
		||||
  data: Ref<null>;
 | 
			
		||||
  error: Ref<AxiosError<ResponseData>>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type HookRequestInstanceResponseData<T = any, ResponseData = any> = {
 | 
			
		||||
export type HookRequestInstanceResponseData<ResponseData, ApiData> = {
 | 
			
		||||
  loading: Ref<boolean>;
 | 
			
		||||
} & (HookRequestInstanceResponseSuccessData<T> | HookRequestInstanceResponseFailData<ResponseData>);
 | 
			
		||||
} & (HookRequestInstanceResponseSuccessData<ApiData> | HookRequestInstanceResponseFailData<ResponseData>);
 | 
			
		||||
 | 
			
		||||
export interface HookRequestInstance<ResponseData = any> {
 | 
			
		||||
  <T = any, R extends ResponseType = 'json'>(
 | 
			
		||||
export interface HookRequestInstance<ResponseData, ApiData, State extends Record<string, unknown>>
 | 
			
		||||
  extends RequestInstanceCommon<State> {
 | 
			
		||||
  <T extends ApiData = ApiData, R extends ResponseType = 'json'>(
 | 
			
		||||
    config: CustomAxiosRequestConfig
 | 
			
		||||
  ): HookRequestInstanceResponseData<MappedType<R, T>, ResponseData>;
 | 
			
		||||
  cancelRequest: (requestId: string) => void;
 | 
			
		||||
  cancelAllRequest: () => void;
 | 
			
		||||
  ): HookRequestInstanceResponseData<ResponseData, MappedType<R, T>>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -39,25 +39,26 @@ export interface HookRequestInstance<ResponseData = any> {
 | 
			
		||||
 * @param axiosConfig
 | 
			
		||||
 * @param options
 | 
			
		||||
 */
 | 
			
		||||
export default function createHookRequest<ResponseData = any>(
 | 
			
		||||
export default function createHookRequest<ResponseData, ApiData, State extends Record<string, unknown>>(
 | 
			
		||||
  axiosConfig?: CreateAxiosDefaults,
 | 
			
		||||
  options?: Partial<RequestOption<ResponseData>>
 | 
			
		||||
  options?: Partial<RequestOption<ResponseData, ApiData, State>>
 | 
			
		||||
) {
 | 
			
		||||
  const request = createFlatRequest<ResponseData>(axiosConfig, options);
 | 
			
		||||
  const request = createFlatRequest<ResponseData, ApiData, State>(axiosConfig, options);
 | 
			
		||||
 | 
			
		||||
  const hookRequest: HookRequestInstance<ResponseData> = function hookRequest<T = any, R extends ResponseType = 'json'>(
 | 
			
		||||
    config: CustomAxiosRequestConfig
 | 
			
		||||
  ) {
 | 
			
		||||
  const hookRequest: HookRequestInstance<ResponseData, ApiData, State> = function hookRequest<
 | 
			
		||||
    T extends ApiData = ApiData,
 | 
			
		||||
    R extends ResponseType = 'json'
 | 
			
		||||
  >(config: CustomAxiosRequestConfig) {
 | 
			
		||||
    const { loading, startLoading, endLoading } = useLoading();
 | 
			
		||||
 | 
			
		||||
    const data = ref<MappedType<R, T> | null>(null) as Ref<MappedType<R, T>>;
 | 
			
		||||
    const error = ref<AxiosError<ResponseData> | null>(null) as Ref<AxiosError<ResponseData> | null>;
 | 
			
		||||
    const data = ref(null) as Ref<MappedType<R, T>>;
 | 
			
		||||
    const error = ref(null) as Ref<AxiosError<ResponseData> | null>;
 | 
			
		||||
 | 
			
		||||
    startLoading();
 | 
			
		||||
 | 
			
		||||
    request(config).then(res => {
 | 
			
		||||
      if (res.data) {
 | 
			
		||||
        data.value = res.data;
 | 
			
		||||
        data.value = res.data as MappedType<R, T>;
 | 
			
		||||
      } else {
 | 
			
		||||
        error.value = res.error;
 | 
			
		||||
      }
 | 
			
		||||
@@ -70,9 +71,8 @@ export default function createHookRequest<ResponseData = any>(
 | 
			
		||||
      data,
 | 
			
		||||
      error
 | 
			
		||||
    };
 | 
			
		||||
  } as HookRequestInstance<ResponseData>;
 | 
			
		||||
  } as HookRequestInstance<ResponseData, ApiData, State>;
 | 
			
		||||
 | 
			
		||||
  hookRequest.cancelRequest = request.cancelRequest;
 | 
			
		||||
  hookRequest.cancelAllRequest = request.cancelAllRequest;
 | 
			
		||||
 | 
			
		||||
  return hookRequest;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,144 +0,0 @@
 | 
			
		||||
import { computed, ref, shallowRef, triggerRef } from 'vue';
 | 
			
		||||
import type {
 | 
			
		||||
  ComputedGetter,
 | 
			
		||||
  DebuggerOptions,
 | 
			
		||||
  Ref,
 | 
			
		||||
  ShallowRef,
 | 
			
		||||
  WritableComputedOptions,
 | 
			
		||||
  WritableComputedRef
 | 
			
		||||
} from 'vue';
 | 
			
		||||
 | 
			
		||||
type Updater<T> = (value: T) => T;
 | 
			
		||||
type Mutator<T> = (value: T) => void;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Signal is a reactive value that can be set, updated or mutated
 | 
			
		||||
 *
 | 
			
		||||
 * @example
 | 
			
		||||
 *   ```ts
 | 
			
		||||
 *   const count = useSignal(0);
 | 
			
		||||
 *
 | 
			
		||||
 *   // `watchEffect`
 | 
			
		||||
 *   watchEffect(() => {
 | 
			
		||||
 *   console.log(count());
 | 
			
		||||
 *   });
 | 
			
		||||
 *
 | 
			
		||||
 *   // watch
 | 
			
		||||
 *   watch(count, value => {
 | 
			
		||||
 *   console.log(value);
 | 
			
		||||
 *   });
 | 
			
		||||
 *
 | 
			
		||||
 *   // useComputed
 | 
			
		||||
 *   const double = useComputed(() => count() * 2);
 | 
			
		||||
 *   const writeableDouble = useComputed({
 | 
			
		||||
 *   get: () => count() * 2,
 | 
			
		||||
 *   set: value => count.set(value / 2)
 | 
			
		||||
 *   });
 | 
			
		||||
 *   ```
 | 
			
		||||
 */
 | 
			
		||||
export interface Signal<T> {
 | 
			
		||||
  (): Readonly<T>;
 | 
			
		||||
  /**
 | 
			
		||||
   * Set the value of the signal
 | 
			
		||||
   *
 | 
			
		||||
   * It recommend use `set` for primitive values
 | 
			
		||||
   *
 | 
			
		||||
   * @param value
 | 
			
		||||
   */
 | 
			
		||||
  set(value: T): void;
 | 
			
		||||
  /**
 | 
			
		||||
   * Update the value of the signal using an updater function
 | 
			
		||||
   *
 | 
			
		||||
   * It recommend use `update` for non-primitive values, only the first level of the object will be reactive.
 | 
			
		||||
   *
 | 
			
		||||
   * @param updater
 | 
			
		||||
   */
 | 
			
		||||
  update(updater: Updater<T>): void;
 | 
			
		||||
  /**
 | 
			
		||||
   * Mutate the value of the signal using a mutator function
 | 
			
		||||
   *
 | 
			
		||||
   * this action will call `triggerRef`, so the value will be tracked on `watchEffect`.
 | 
			
		||||
   *
 | 
			
		||||
   * It recommend use `mutate` for non-primitive values, all levels of the object will be reactive.
 | 
			
		||||
   *
 | 
			
		||||
   * @param mutator
 | 
			
		||||
   */
 | 
			
		||||
  mutate(mutator: Mutator<T>): void;
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the reference of the signal
 | 
			
		||||
   *
 | 
			
		||||
   * Sometimes it can be useful to make `v-model` work with the signal
 | 
			
		||||
   *
 | 
			
		||||
   * ```vue
 | 
			
		||||
   * <template>
 | 
			
		||||
   *   <input v-model="model.count" />
 | 
			
		||||
   * </template>;
 | 
			
		||||
   *
 | 
			
		||||
   * <script setup lang="ts">
 | 
			
		||||
   *  const state = useSignal({ count: 0 }, { useRef: true });
 | 
			
		||||
   *
 | 
			
		||||
   *  const model = state.getRef();
 | 
			
		||||
   * </script>
 | 
			
		||||
   * ```
 | 
			
		||||
   */
 | 
			
		||||
  getRef(): Readonly<ShallowRef<Readonly<T>>>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ReadonlySignal<T> {
 | 
			
		||||
  (): Readonly<T>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SignalOptions {
 | 
			
		||||
  /**
 | 
			
		||||
   * Whether to use `ref` to store the value
 | 
			
		||||
   *
 | 
			
		||||
   * @default false use `sharedRef` to store the value
 | 
			
		||||
   */
 | 
			
		||||
  useRef?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useSignal<T>(initialValue: T, options?: SignalOptions): Signal<T> {
 | 
			
		||||
  const { useRef } = options || {};
 | 
			
		||||
 | 
			
		||||
  const state = useRef ? (ref(initialValue) as Ref<T>) : shallowRef(initialValue);
 | 
			
		||||
 | 
			
		||||
  return createSignal(state);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useComputed<T>(getter: ComputedGetter<T>, debugOptions?: DebuggerOptions): ReadonlySignal<T>;
 | 
			
		||||
export function useComputed<T>(options: WritableComputedOptions<T>, debugOptions?: DebuggerOptions): Signal<T>;
 | 
			
		||||
export function useComputed<T>(
 | 
			
		||||
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
 | 
			
		||||
  debugOptions?: DebuggerOptions
 | 
			
		||||
) {
 | 
			
		||||
  const isGetter = typeof getterOrOptions === 'function';
 | 
			
		||||
 | 
			
		||||
  const computedValue = computed(getterOrOptions as any, debugOptions);
 | 
			
		||||
 | 
			
		||||
  if (isGetter) {
 | 
			
		||||
    return () => computedValue.value as ReadonlySignal<T>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return createSignal(computedValue);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createSignal<T>(state: ShallowRef<T> | WritableComputedRef<T>): Signal<T> {
 | 
			
		||||
  const signal = () => state.value;
 | 
			
		||||
 | 
			
		||||
  signal.set = (value: T) => {
 | 
			
		||||
    state.value = value;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  signal.update = (updater: Updater<T>) => {
 | 
			
		||||
    state.value = updater(state.value);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  signal.mutate = (mutator: Mutator<T>) => {
 | 
			
		||||
    mutator(state.value);
 | 
			
		||||
    triggerRef(state);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  signal.getRef = () => state as Readonly<ShallowRef<Readonly<T>>>;
 | 
			
		||||
 | 
			
		||||
  return signal;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,12 +1,20 @@
 | 
			
		||||
import { computed, reactive, ref } from 'vue';
 | 
			
		||||
import { computed, ref } from 'vue';
 | 
			
		||||
import type { Ref, VNodeChild } from 'vue';
 | 
			
		||||
import { jsonClone } from '@sa/utils';
 | 
			
		||||
import useBoolean from './use-boolean';
 | 
			
		||||
import useLoading from './use-loading';
 | 
			
		||||
 | 
			
		||||
export type MaybePromise<T> = T | Promise<T>;
 | 
			
		||||
export interface PaginationData<T> {
 | 
			
		||||
  data: T[];
 | 
			
		||||
  pageNum: number;
 | 
			
		||||
  pageSize: number;
 | 
			
		||||
  total: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type ApiFn = (args: any) => Promise<unknown>;
 | 
			
		||||
type GetApiData<ApiData, Pagination extends boolean> = Pagination extends true ? PaginationData<ApiData> : ApiData[];
 | 
			
		||||
 | 
			
		||||
type Transform<ResponseData, ApiData, Pagination extends boolean> = (
 | 
			
		||||
  response: ResponseData
 | 
			
		||||
) => GetApiData<ApiData, Pagination>;
 | 
			
		||||
 | 
			
		||||
export type TableColumnCheckTitle = string | ((...args: any) => VNodeChild);
 | 
			
		||||
 | 
			
		||||
@@ -14,76 +22,64 @@ export type TableColumnCheck = {
 | 
			
		||||
  key: string;
 | 
			
		||||
  title: TableColumnCheckTitle;
 | 
			
		||||
  checked: boolean;
 | 
			
		||||
  visible: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type TableDataWithIndex<T> = T & { index: number };
 | 
			
		||||
 | 
			
		||||
export type TransformedData<T> = {
 | 
			
		||||
  data: TableDataWithIndex<T>[];
 | 
			
		||||
  pageNum: number;
 | 
			
		||||
  pageSize: number;
 | 
			
		||||
  total: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type Transformer<T, Response> = (response: Response) => TransformedData<T>;
 | 
			
		||||
 | 
			
		||||
export type TableConfig<A extends ApiFn, T, C> = {
 | 
			
		||||
  /** api function to get table data */
 | 
			
		||||
  apiFn: A;
 | 
			
		||||
  /** api params */
 | 
			
		||||
  apiParams?: Parameters<A>[0];
 | 
			
		||||
  /** transform api response to table data */
 | 
			
		||||
  transformer: Transformer<T, Awaited<ReturnType<A>>>;
 | 
			
		||||
  /** columns factory */
 | 
			
		||||
  columns: () => C[];
 | 
			
		||||
export interface UseTableOptions<ResponseData, ApiData, Column, Pagination extends boolean> {
 | 
			
		||||
  /**
 | 
			
		||||
   * api function to get table data
 | 
			
		||||
   */
 | 
			
		||||
  api: () => Promise<ResponseData>;
 | 
			
		||||
  /**
 | 
			
		||||
   * whether to enable pagination
 | 
			
		||||
   */
 | 
			
		||||
  pagination?: Pagination;
 | 
			
		||||
  /**
 | 
			
		||||
   * transform api response to table data
 | 
			
		||||
   */
 | 
			
		||||
  transform: Transform<ResponseData, ApiData, Pagination>;
 | 
			
		||||
  /**
 | 
			
		||||
   * columns factory
 | 
			
		||||
   */
 | 
			
		||||
  columns: () => Column[];
 | 
			
		||||
  /**
 | 
			
		||||
   * get column checks
 | 
			
		||||
   *
 | 
			
		||||
   * @param columns
 | 
			
		||||
   */
 | 
			
		||||
  getColumnChecks: (columns: C[]) => TableColumnCheck[];
 | 
			
		||||
  getColumnChecks: (columns: Column[]) => TableColumnCheck[];
 | 
			
		||||
  /**
 | 
			
		||||
   * get columns
 | 
			
		||||
   *
 | 
			
		||||
   * @param columns
 | 
			
		||||
   */
 | 
			
		||||
  getColumns: (columns: C[], checks: TableColumnCheck[]) => C[];
 | 
			
		||||
  getColumns: (columns: Column[], checks: TableColumnCheck[]) => Column[];
 | 
			
		||||
  /**
 | 
			
		||||
   * callback when response fetched
 | 
			
		||||
   *
 | 
			
		||||
   * @param transformed transformed data
 | 
			
		||||
   */
 | 
			
		||||
  onFetched?: (transformed: TransformedData<T>) => MaybePromise<void>;
 | 
			
		||||
  onFetched?: (data: GetApiData<ApiData, Pagination>) => void | Promise<void>;
 | 
			
		||||
  /**
 | 
			
		||||
   * whether to get data immediately
 | 
			
		||||
   *
 | 
			
		||||
   * @default true
 | 
			
		||||
   */
 | 
			
		||||
  immediate?: boolean;
 | 
			
		||||
};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) {
 | 
			
		||||
export default function useTable<ResponseData, ApiData, Column, Pagination extends boolean>(
 | 
			
		||||
  options: UseTableOptions<ResponseData, ApiData, Column, Pagination>
 | 
			
		||||
) {
 | 
			
		||||
  const { loading, startLoading, endLoading } = useLoading();
 | 
			
		||||
  const { bool: empty, setBool: setEmpty } = useBoolean();
 | 
			
		||||
 | 
			
		||||
  const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config;
 | 
			
		||||
  const { api, pagination, transform, columns, getColumnChecks, getColumns, onFetched, immediate = true } = options;
 | 
			
		||||
 | 
			
		||||
  const searchParams: NonNullable<Parameters<A>[0]> = reactive(jsonClone({ ...apiParams }));
 | 
			
		||||
  const data = ref([]) as Ref<ApiData[]>;
 | 
			
		||||
 | 
			
		||||
  const allColumns = ref(config.columns()) as Ref<C[]>;
 | 
			
		||||
  const columnChecks = ref(getColumnChecks(columns())) as Ref<TableColumnCheck[]>;
 | 
			
		||||
 | 
			
		||||
  const data: Ref<TableDataWithIndex<T>[]> = ref([]);
 | 
			
		||||
 | 
			
		||||
  const columnChecks: Ref<TableColumnCheck[]> = ref(getColumnChecks(config.columns()));
 | 
			
		||||
 | 
			
		||||
  const columns = computed(() => getColumns(allColumns.value, columnChecks.value));
 | 
			
		||||
  const $columns = computed(() => getColumns(columns(), columnChecks.value));
 | 
			
		||||
 | 
			
		||||
  function reloadColumns() {
 | 
			
		||||
    allColumns.value = config.columns();
 | 
			
		||||
 | 
			
		||||
    const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked]));
 | 
			
		||||
 | 
			
		||||
    const defaultChecks = getColumnChecks(allColumns.value);
 | 
			
		||||
    const defaultChecks = getColumnChecks(columns());
 | 
			
		||||
 | 
			
		||||
    columnChecks.value = defaultChecks.map(col => ({
 | 
			
		||||
      ...col,
 | 
			
		||||
@@ -92,47 +88,21 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function getData() {
 | 
			
		||||
    startLoading();
 | 
			
		||||
    try {
 | 
			
		||||
      startLoading();
 | 
			
		||||
 | 
			
		||||
    const formattedParams = formatSearchParams(searchParams);
 | 
			
		||||
      const response = await api();
 | 
			
		||||
 | 
			
		||||
    const response = await apiFn(formattedParams);
 | 
			
		||||
      const transformed = transform(response);
 | 
			
		||||
 | 
			
		||||
    const transformed = transformer(response as Awaited<ReturnType<A>>);
 | 
			
		||||
      data.value = getTableData(transformed, pagination);
 | 
			
		||||
 | 
			
		||||
    data.value = transformed.data;
 | 
			
		||||
      setEmpty(data.value.length === 0);
 | 
			
		||||
 | 
			
		||||
    setEmpty(transformed.data.length === 0);
 | 
			
		||||
 | 
			
		||||
    await config.onFetched?.(transformed);
 | 
			
		||||
 | 
			
		||||
    endLoading();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function formatSearchParams(params: Record<string, unknown>) {
 | 
			
		||||
    const formattedParams: Record<string, unknown> = {};
 | 
			
		||||
 | 
			
		||||
    Object.entries(params).forEach(([key, value]) => {
 | 
			
		||||
      if (value !== null && value !== undefined) {
 | 
			
		||||
        formattedParams[key] = value;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return formattedParams;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * update search params
 | 
			
		||||
   *
 | 
			
		||||
   * @param params
 | 
			
		||||
   */
 | 
			
		||||
  function updateSearchParams(params: Partial<Parameters<A>[0]>) {
 | 
			
		||||
    Object.assign(searchParams, params);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** reset search params */
 | 
			
		||||
  function resetSearchParams() {
 | 
			
		||||
    Object.assign(searchParams, jsonClone(apiParams));
 | 
			
		||||
      await onFetched?.(transformed);
 | 
			
		||||
    } finally {
 | 
			
		||||
      endLoading();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (immediate) {
 | 
			
		||||
@@ -143,12 +113,20 @@ export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<
 | 
			
		||||
    loading,
 | 
			
		||||
    empty,
 | 
			
		||||
    data,
 | 
			
		||||
    columns,
 | 
			
		||||
    columns: $columns,
 | 
			
		||||
    columnChecks,
 | 
			
		||||
    reloadColumns,
 | 
			
		||||
    getData,
 | 
			
		||||
    searchParams,
 | 
			
		||||
    updateSearchParams,
 | 
			
		||||
    resetSearchParams
 | 
			
		||||
    getData
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getTableData<ApiData, Pagination extends boolean>(
 | 
			
		||||
  data: GetApiData<ApiData, Pagination>,
 | 
			
		||||
  pagination?: Pagination
 | 
			
		||||
) {
 | 
			
		||||
  if (pagination) {
 | 
			
		||||
    return (data as PaginationData<ApiData>).data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return data as ApiData[];
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@sa/materials",
 | 
			
		||||
  "version": "1.3.15",
 | 
			
		||||
  "version": "2.0.0",
 | 
			
		||||
  "exports": {
 | 
			
		||||
    ".": "./src/index.ts"
 | 
			
		||||
  },
 | 
			
		||||
@@ -11,7 +11,7 @@
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@sa/utils": "workspace:*",
 | 
			
		||||
    "simplebar-vue": "2.4.1"
 | 
			
		||||
    "simplebar-vue": "2.4.2"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "typed-css-modules": "0.9.1"
 | 
			
		||||
 
 | 
			
		||||
@@ -127,7 +127,6 @@ function handleClickMask() {
 | 
			
		||||
          :class="[
 | 
			
		||||
            style['layout-header'],
 | 
			
		||||
            commonClass,
 | 
			
		||||
            headerClass,
 | 
			
		||||
            headerLeftGapClass,
 | 
			
		||||
            { 'absolute top-0 left-0 w-full': fixedHeaderAndTab }
 | 
			
		||||
          ]"
 | 
			
		||||
 
 | 
			
		||||
@@ -95,3 +95,27 @@
 | 
			
		||||
.chrome-tab_dark .chrome-tab-divider {
 | 
			
		||||
  background-color: rgba(255, 255, 255, 0.9);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.slider-tab {
 | 
			
		||||
  background-color: transparent;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  border-bottom: 2px solid transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.slider-tab_dark {
 | 
			
		||||
  background-color: transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.slider-tab:hover {
 | 
			
		||||
  color: var(--soy-primary-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.slider-tab_active {
 | 
			
		||||
  color: var(--soy-primary-color);
 | 
			
		||||
  background-color: var(--soy-primary-color-opacity1);
 | 
			
		||||
  border-bottom-color: var(--soy-primary-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.slider-tab_active_dark {
 | 
			
		||||
  background-color: var(--soy-primary-color-opacity2);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,10 @@ declare const styles: {
 | 
			
		||||
  readonly 'chrome-tab_dark': string;
 | 
			
		||||
  readonly 'chrome-tab-divider': string;
 | 
			
		||||
  readonly 'svg-close': string;
 | 
			
		||||
  readonly 'slider-tab': string;
 | 
			
		||||
  readonly 'slider-tab_active': string;
 | 
			
		||||
  readonly 'slider-tab_active_dark': string;
 | 
			
		||||
  readonly 'slider-tab_dark': string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default styles;
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import type { PageTabMode, PageTabProps } from '../../types';
 | 
			
		||||
import { ACTIVE_COLOR, createTabCssVars } from './shared';
 | 
			
		||||
import ChromeTab from './chrome-tab.vue';
 | 
			
		||||
import ButtonTab from './button-tab.vue';
 | 
			
		||||
import SliderTab from './slider-tab.vue';
 | 
			
		||||
import SvgClose from './svg-close.vue';
 | 
			
		||||
import style from './index.module.css';
 | 
			
		||||
 | 
			
		||||
@@ -26,7 +27,7 @@ interface Emits {
 | 
			
		||||
const emit = defineEmits<Emits>();
 | 
			
		||||
 | 
			
		||||
const activeTabComponent = computed(() => {
 | 
			
		||||
  const { mode, chromeClass, buttonClass } = props;
 | 
			
		||||
  const { mode, chromeClass, buttonClass, sliderClass } = props;
 | 
			
		||||
 | 
			
		||||
  const tabComponentMap = {
 | 
			
		||||
    chrome: {
 | 
			
		||||
@@ -36,6 +37,10 @@ const activeTabComponent = computed(() => {
 | 
			
		||||
    button: {
 | 
			
		||||
      component: ButtonTab,
 | 
			
		||||
      class: buttonClass
 | 
			
		||||
    },
 | 
			
		||||
    slider: {
 | 
			
		||||
      component: SliderTab,
 | 
			
		||||
      class: sliderClass
 | 
			
		||||
    }
 | 
			
		||||
  } satisfies Record<PageTabMode, { component: Component; class?: string }>;
 | 
			
		||||
 | 
			
		||||
@@ -45,7 +50,7 @@ const activeTabComponent = computed(() => {
 | 
			
		||||
const cssVars = computed(() => createTabCssVars(props.activeColor));
 | 
			
		||||
 | 
			
		||||
const bindProps = computed(() => {
 | 
			
		||||
  const { chromeClass: _chromeCls, buttonClass: _btnCls, ...rest } = props;
 | 
			
		||||
  const { chromeClass: _chromeCls, buttonClass: _btnCls, sliderClass: _sliderCls, ...rest } = props;
 | 
			
		||||
 | 
			
		||||
  return rest;
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										53
									
								
								packages/materials/src/libs/page-tab/slider-tab.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								packages/materials/src/libs/page-tab/slider-tab.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import type { PageTabProps } from '../../types';
 | 
			
		||||
import style from './index.module.css';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'SliderTab'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
defineProps<PageTabProps>();
 | 
			
		||||
 | 
			
		||||
type SlotFn = (props?: Record<string, unknown>) => any;
 | 
			
		||||
 | 
			
		||||
type Slots = {
 | 
			
		||||
  /**
 | 
			
		||||
   * Slot
 | 
			
		||||
   *
 | 
			
		||||
   * The center content of the tab
 | 
			
		||||
   */
 | 
			
		||||
  default?: SlotFn;
 | 
			
		||||
  /**
 | 
			
		||||
   * Slot
 | 
			
		||||
   *
 | 
			
		||||
   * The left content of the tab
 | 
			
		||||
   */
 | 
			
		||||
  prefix?: SlotFn;
 | 
			
		||||
  /**
 | 
			
		||||
   * Slot
 | 
			
		||||
   *
 | 
			
		||||
   * The right content of the tab
 | 
			
		||||
   */
 | 
			
		||||
  suffix?: SlotFn;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineSlots<Slots>();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-6px whitespace-nowrap px-12px py-4px"
 | 
			
		||||
    :class="[
 | 
			
		||||
      style['slider-tab'],
 | 
			
		||||
      { [style['slider-tab_dark']]: darkMode },
 | 
			
		||||
      { [style['slider-tab_active']]: active },
 | 
			
		||||
      { [style['slider-tab_active_dark']]: active && darkMode }
 | 
			
		||||
    ]"
 | 
			
		||||
  >
 | 
			
		||||
    <slot name="prefix"></slot>
 | 
			
		||||
    <slot></slot>
 | 
			
		||||
    <slot name="suffix"></slot>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
@@ -6,12 +6,6 @@ interface AdminLayoutHeaderConfig {
 | 
			
		||||
   * @default true
 | 
			
		||||
   */
 | 
			
		||||
  headerVisible?: boolean;
 | 
			
		||||
  /**
 | 
			
		||||
   * Header class
 | 
			
		||||
   *
 | 
			
		||||
   * @default ''
 | 
			
		||||
   */
 | 
			
		||||
  headerClass?: string;
 | 
			
		||||
  /**
 | 
			
		||||
   * Header height
 | 
			
		||||
   *
 | 
			
		||||
@@ -245,7 +239,7 @@ export type LayoutCssVars = {
 | 
			
		||||
 *
 | 
			
		||||
 * @default chrome
 | 
			
		||||
 */
 | 
			
		||||
export type PageTabMode = 'button' | 'chrome';
 | 
			
		||||
export type PageTabMode = 'button' | 'chrome' | 'slider';
 | 
			
		||||
 | 
			
		||||
export interface PageTabProps {
 | 
			
		||||
  /** Whether is dark mode */
 | 
			
		||||
@@ -268,6 +262,8 @@ export interface PageTabProps {
 | 
			
		||||
  buttonClass?: string;
 | 
			
		||||
  /** The class of the chrome tab */
 | 
			
		||||
  chromeClass?: string;
 | 
			
		||||
  /** The class of the title tab */
 | 
			
		||||
  sliderClass?: string;
 | 
			
		||||
  /** Whether the tab is active */
 | 
			
		||||
  active?: boolean;
 | 
			
		||||
  /** The color of the active tab */
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@sa/fetch",
 | 
			
		||||
  "version": "1.3.15",
 | 
			
		||||
  "exports": {
 | 
			
		||||
    ".": "./src/index.ts"
 | 
			
		||||
  },
 | 
			
		||||
  "typesVersions": {
 | 
			
		||||
    "*": {
 | 
			
		||||
      "*": ["./src/*"]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "ofetch": "1.4.1"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
import { ofetch } from 'ofetch';
 | 
			
		||||
import type { FetchOptions } from 'ofetch';
 | 
			
		||||
 | 
			
		||||
export function createRequest(options: FetchOptions) {
 | 
			
		||||
  const request = ofetch.create(options);
 | 
			
		||||
 | 
			
		||||
  return request;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default createRequest;
 | 
			
		||||
@@ -1,20 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "target": "ESNext",
 | 
			
		||||
    "jsx": "preserve",
 | 
			
		||||
    "lib": ["DOM", "ESNext"],
 | 
			
		||||
    "baseUrl": ".",
 | 
			
		||||
    "module": "ESNext",
 | 
			
		||||
    "moduleResolution": "node",
 | 
			
		||||
    "resolveJsonModule": true,
 | 
			
		||||
    "types": ["node"],
 | 
			
		||||
    "strict": true,
 | 
			
		||||
    "strictNullChecks": true,
 | 
			
		||||
    "noUnusedLocals": true,
 | 
			
		||||
    "allowSyntheticDefaultImports": true,
 | 
			
		||||
    "esModuleInterop": true,
 | 
			
		||||
    "forceConsistentCasingInFileNames": true
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["src/**/*"],
 | 
			
		||||
  "exclude": ["node_modules", "dist"]
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@sa/scripts",
 | 
			
		||||
  "version": "1.3.15",
 | 
			
		||||
  "version": "2.0.0",
 | 
			
		||||
  "bin": {
 | 
			
		||||
    "sa": "./bin.ts"
 | 
			
		||||
  },
 | 
			
		||||
@@ -13,15 +13,16 @@
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@soybeanjs/changelog": "0.3.24",
 | 
			
		||||
    "bumpp": "10.2.0",
 | 
			
		||||
    "c12": "3.0.4",
 | 
			
		||||
    "@soybeanjs/changelog": "0.3.25",
 | 
			
		||||
    "bumpp": "10.3.1",
 | 
			
		||||
    "c12": "3.3.1",
 | 
			
		||||
    "cac": "6.7.14",
 | 
			
		||||
    "consola": "3.4.2",
 | 
			
		||||
    "enquirer": "2.4.1",
 | 
			
		||||
    "execa": "9.6.0",
 | 
			
		||||
    "kolorist": "1.8.0",
 | 
			
		||||
    "npm-check-updates": "18.0.1",
 | 
			
		||||
    "rimraf": "6.0.1"
 | 
			
		||||
    "npm-check-updates": "19.1.2",
 | 
			
		||||
    "picomatch": "4.0.3",
 | 
			
		||||
    "rimraf": "6.1.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { execCommand } from '../shared';
 | 
			
		||||
 | 
			
		||||
export async function updatePkg(args: string[] = ['--deep', '-u']) {
 | 
			
		||||
  execCommand('npx', ['ncu', ...args], { stdio: 'inherit' });
 | 
			
		||||
  execCommand('npx', ['npm-check-updates', ...args], { stdio: 'inherit' });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@sa/uno-preset",
 | 
			
		||||
  "version": "1.3.15",
 | 
			
		||||
  "version": "2.0.0",
 | 
			
		||||
  "exports": {
 | 
			
		||||
    ".": "./src/index.ts"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@sa/utils",
 | 
			
		||||
  "version": "1.3.15",
 | 
			
		||||
  "version": "2.0.0",
 | 
			
		||||
  "exports": {
 | 
			
		||||
    ".": "./src/index.ts"
 | 
			
		||||
  },
 | 
			
		||||
@@ -14,7 +14,7 @@
 | 
			
		||||
    "crypto-js": "4.2.0",
 | 
			
		||||
    "klona": "2.0.6",
 | 
			
		||||
    "localforage": "1.10.0",
 | 
			
		||||
    "nanoid": "5.1.5"
 | 
			
		||||
    "nanoid": "5.1.6"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/crypto-js": "4.2.2"
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,8 @@ export function createStorage<T extends object>(type: StorageType, storagePrefix
 | 
			
		||||
          storageData = JSON.parse(json);
 | 
			
		||||
        } catch {}
 | 
			
		||||
 | 
			
		||||
        if (storageData) {
 | 
			
		||||
        // storageData may be `false` if it is boolean type
 | 
			
		||||
        if (storageData !== null) {
 | 
			
		||||
          return storageData as T[K];
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3635
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3635
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -4,7 +4,6 @@ import { NConfigProvider, darkTheme } from 'naive-ui';
 | 
			
		||||
import type { WatermarkProps } from 'naive-ui';
 | 
			
		||||
import { useAppStore } from './store/modules/app';
 | 
			
		||||
import { useThemeStore } from './store/modules/theme';
 | 
			
		||||
import { useAuthStore } from './store/modules/auth';
 | 
			
		||||
import { naiveDateLocales, naiveLocales } from './locales/naive';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
@@ -13,7 +12,6 @@ defineOptions({
 | 
			
		||||
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
const authStore = useAuthStore();
 | 
			
		||||
 | 
			
		||||
const naiveDarkTheme = computed(() => (themeStore.darkMode ? darkTheme : undefined));
 | 
			
		||||
 | 
			
		||||
@@ -26,13 +24,8 @@ const naiveDateLocale = computed(() => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const watermarkProps = computed<WatermarkProps>(() => {
 | 
			
		||||
  const content =
 | 
			
		||||
    themeStore.watermark.enableUserName && authStore.userInfo.userName
 | 
			
		||||
      ? authStore.userInfo.userName
 | 
			
		||||
      : themeStore.watermark.text;
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    content,
 | 
			
		||||
    content: themeStore.watermarkContent,
 | 
			
		||||
    cross: true,
 | 
			
		||||
    fullscreen: true,
 | 
			
		||||
    fontSize: 16,
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,12 @@ const columns = defineModel<NaiveUI.TableColumnCheck[]>('columns', {
 | 
			
		||||
      </NButton>
 | 
			
		||||
    </template>
 | 
			
		||||
    <VueDraggable v-model="columns" :animation="150" filter=".none_draggable">
 | 
			
		||||
      <div v-for="item in columns" :key="item.key" class="h-36px flex-y-center rd-4px hover:(bg-primary bg-opacity-20)">
 | 
			
		||||
      <div
 | 
			
		||||
        v-for="item in columns"
 | 
			
		||||
        :key="item.key"
 | 
			
		||||
        class="h-36px flex-y-center rd-4px hover:(bg-primary bg-opacity-20)"
 | 
			
		||||
        :class="{ hidden: !item.visible }"
 | 
			
		||||
      >
 | 
			
		||||
        <icon-mdi-drag class="mr-8px h-full cursor-move text-icon" />
 | 
			
		||||
        <NCheckbox v-model:checked="item.checked" class="none_draggable flex-1">
 | 
			
		||||
          <template v-if="typeof item.title === 'function'">
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								src/components/common/icon-tooltip.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/components/common/icon-tooltip.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, useSlots } from 'vue';
 | 
			
		||||
import type { PopoverPlacement } from 'naive-ui';
 | 
			
		||||
 | 
			
		||||
defineOptions({ name: 'IconTooltip' });
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  icon?: string;
 | 
			
		||||
  localIcon?: string;
 | 
			
		||||
  desc?: string;
 | 
			
		||||
  placement?: PopoverPlacement;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  icon: 'mdi-help-circle',
 | 
			
		||||
  localIcon: '',
 | 
			
		||||
  desc: '',
 | 
			
		||||
  placement: 'top'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const slots = useSlots();
 | 
			
		||||
const hasCustomTrigger = computed(() => Boolean(slots.trigger));
 | 
			
		||||
 | 
			
		||||
if (!hasCustomTrigger.value && !props.icon && !props.localIcon) {
 | 
			
		||||
  throw new Error('icon or localIcon is required when no custom trigger slot is provided');
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <NTooltip :placement="placement">
 | 
			
		||||
    <template #trigger>
 | 
			
		||||
      <slot name="trigger">
 | 
			
		||||
        <div class="cursor-pointer">
 | 
			
		||||
          <SvgIcon :icon="icon" :local-icon="localIcon" />
 | 
			
		||||
        </div>
 | 
			
		||||
      </slot>
 | 
			
		||||
    </template>
 | 
			
		||||
    <slot>
 | 
			
		||||
      <span>{{ desc }}</span>
 | 
			
		||||
    </slot>
 | 
			
		||||
  </NTooltip>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -5,9 +5,9 @@ export const GLOBAL_HEADER_MENU_ID = '__GLOBAL_HEADER_MENU__';
 | 
			
		||||
export const GLOBAL_SIDER_MENU_ID = '__GLOBAL_SIDER_MENU__';
 | 
			
		||||
 | 
			
		||||
export const themeSchemaRecord: Record<UnionKey.ThemeScheme, App.I18n.I18nKey> = {
 | 
			
		||||
  light: 'theme.themeSchema.light',
 | 
			
		||||
  dark: 'theme.themeSchema.dark',
 | 
			
		||||
  auto: 'theme.themeSchema.auto'
 | 
			
		||||
  light: 'theme.appearance.themeSchema.light',
 | 
			
		||||
  dark: 'theme.appearance.themeSchema.dark',
 | 
			
		||||
  auto: 'theme.appearance.themeSchema.auto'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const themeSchemaOptions = transformRecordToOption(themeSchemaRecord);
 | 
			
		||||
@@ -21,45 +21,51 @@ export const loginModuleRecord: Record<UnionKey.LoginModule, App.I18n.I18nKey> =
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const themeLayoutModeRecord: Record<UnionKey.ThemeLayoutMode, App.I18n.I18nKey> = {
 | 
			
		||||
  vertical: 'theme.layoutMode.vertical',
 | 
			
		||||
  'vertical-mix': 'theme.layoutMode.vertical-mix',
 | 
			
		||||
  horizontal: 'theme.layoutMode.horizontal',
 | 
			
		||||
  'horizontal-mix': 'theme.layoutMode.horizontal-mix'
 | 
			
		||||
  vertical: 'theme.layout.layoutMode.vertical',
 | 
			
		||||
  'vertical-mix': 'theme.layout.layoutMode.vertical-mix',
 | 
			
		||||
  'vertical-hybrid-header-first': 'theme.layout.layoutMode.vertical-hybrid-header-first',
 | 
			
		||||
  horizontal: 'theme.layout.layoutMode.horizontal',
 | 
			
		||||
  'top-hybrid-sidebar-first': 'theme.layout.layoutMode.top-hybrid-sidebar-first',
 | 
			
		||||
  'top-hybrid-header-first': 'theme.layout.layoutMode.top-hybrid-header-first'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const themeLayoutModeOptions = transformRecordToOption(themeLayoutModeRecord);
 | 
			
		||||
 | 
			
		||||
export const themeScrollModeRecord: Record<UnionKey.ThemeScrollMode, App.I18n.I18nKey> = {
 | 
			
		||||
  wrapper: 'theme.scrollMode.wrapper',
 | 
			
		||||
  content: 'theme.scrollMode.content'
 | 
			
		||||
  wrapper: 'theme.layout.content.scrollMode.wrapper',
 | 
			
		||||
  content: 'theme.layout.content.scrollMode.content'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const themeScrollModeOptions = transformRecordToOption(themeScrollModeRecord);
 | 
			
		||||
 | 
			
		||||
export const themeTabModeRecord: Record<UnionKey.ThemeTabMode, App.I18n.I18nKey> = {
 | 
			
		||||
  chrome: 'theme.tab.mode.chrome',
 | 
			
		||||
  button: 'theme.tab.mode.button'
 | 
			
		||||
  chrome: 'theme.layout.tab.mode.chrome',
 | 
			
		||||
  button: 'theme.layout.tab.mode.button',
 | 
			
		||||
  slider: 'theme.layout.tab.mode.slider'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const themeTabModeOptions = transformRecordToOption(themeTabModeRecord);
 | 
			
		||||
 | 
			
		||||
export const themePageAnimationModeRecord: Record<UnionKey.ThemePageAnimateMode, App.I18n.I18nKey> = {
 | 
			
		||||
  'fade-slide': 'theme.page.mode.fade-slide',
 | 
			
		||||
  fade: 'theme.page.mode.fade',
 | 
			
		||||
  'fade-bottom': 'theme.page.mode.fade-bottom',
 | 
			
		||||
  'fade-scale': 'theme.page.mode.fade-scale',
 | 
			
		||||
  'zoom-fade': 'theme.page.mode.zoom-fade',
 | 
			
		||||
  'zoom-out': 'theme.page.mode.zoom-out',
 | 
			
		||||
  none: 'theme.page.mode.none'
 | 
			
		||||
  'fade-slide': 'theme.layout.content.page.mode.fade-slide',
 | 
			
		||||
  fade: 'theme.layout.content.page.mode.fade',
 | 
			
		||||
  'fade-bottom': 'theme.layout.content.page.mode.fade-bottom',
 | 
			
		||||
  'fade-scale': 'theme.layout.content.page.mode.fade-scale',
 | 
			
		||||
  'zoom-fade': 'theme.layout.content.page.mode.zoom-fade',
 | 
			
		||||
  'zoom-out': 'theme.layout.content.page.mode.zoom-out',
 | 
			
		||||
  none: 'theme.layout.content.page.mode.none'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const themePageAnimationModeOptions = transformRecordToOption(themePageAnimationModeRecord);
 | 
			
		||||
 | 
			
		||||
export const resetCacheStrategyRecord: Record<UnionKey.ResetCacheStrategy, App.I18n.I18nKey> = {
 | 
			
		||||
  close: 'theme.resetCacheStrategy.close',
 | 
			
		||||
  refresh: 'theme.resetCacheStrategy.refresh'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const resetCacheStrategyOptions = transformRecordToOption(resetCacheStrategyRecord);
 | 
			
		||||
 | 
			
		||||
export const DARK_CLASS = 'dark';
 | 
			
		||||
 | 
			
		||||
export const watermarkTimeFormatOptions = [
 | 
			
		||||
  { label: 'YYYY-MM-DD HH:mm', value: 'YYYY-MM-DD HH:mm' },
 | 
			
		||||
  { label: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss' },
 | 
			
		||||
  { label: 'YYYY/MM/DD HH:mm', value: 'YYYY/MM/DD HH:mm' },
 | 
			
		||||
  { label: 'YYYY/MM/DD HH:mm:ss', value: 'YYYY/MM/DD HH:mm:ss' },
 | 
			
		||||
  { label: 'HH:mm', value: 'HH:mm' },
 | 
			
		||||
  { label: 'HH:mm:ss', value: 'HH:mm:ss' },
 | 
			
		||||
  { label: 'MM-DD HH:mm', value: 'MM-DD HH:mm' }
 | 
			
		||||
];
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { computed, effectScope, nextTick, onScopeDispose, ref, watch } from 'vue';
 | 
			
		||||
import { computed, effectScope, nextTick, onScopeDispose, shallowRef, watch } from 'vue';
 | 
			
		||||
import { useElementSize } from '@vueuse/core';
 | 
			
		||||
import * as echarts from 'echarts/core';
 | 
			
		||||
import { BarChart, GaugeChart, LineChart, PictorialBarChart, PieChart, RadarChart, ScatterChart } from 'echarts/charts';
 | 
			
		||||
@@ -86,11 +86,11 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
 | 
			
		||||
  const themeStore = useThemeStore();
 | 
			
		||||
  const darkMode = computed(() => themeStore.darkMode);
 | 
			
		||||
 | 
			
		||||
  const domRef = ref<HTMLElement | null>(null);
 | 
			
		||||
  const domRef = shallowRef<HTMLElement | null>(null);
 | 
			
		||||
  const initialSize = { width: 0, height: 0 };
 | 
			
		||||
  const { width, height } = useElementSize(domRef, initialSize);
 | 
			
		||||
 | 
			
		||||
  let chart: echarts.ECharts | null = null;
 | 
			
		||||
  const chart = shallowRef<echarts.ECharts | null>(null);
 | 
			
		||||
  const chartOptions: T = optionsFactory();
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
@@ -111,18 +111,9 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
 | 
			
		||||
    onDestroy
 | 
			
		||||
  } = hooks;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * whether can render chart
 | 
			
		||||
   *
 | 
			
		||||
   * when domRef is ready and initialSize is valid
 | 
			
		||||
   */
 | 
			
		||||
  function canRender() {
 | 
			
		||||
    return domRef.value && initialSize.width > 0 && initialSize.height > 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** is chart rendered */
 | 
			
		||||
  function isRendered() {
 | 
			
		||||
    return Boolean(domRef.value && chart);
 | 
			
		||||
    return Boolean(domRef.value && chart.value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@@ -131,59 +122,59 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
 | 
			
		||||
   * @param callback callback function
 | 
			
		||||
   */
 | 
			
		||||
  async function updateOptions(callback: (opts: T, optsFactory: () => T) => ECOption = () => chartOptions) {
 | 
			
		||||
    if (!isRendered()) return;
 | 
			
		||||
 | 
			
		||||
    const updatedOpts = callback(chartOptions, optionsFactory);
 | 
			
		||||
 | 
			
		||||
    Object.assign(chartOptions, updatedOpts);
 | 
			
		||||
 | 
			
		||||
    await nextTick();
 | 
			
		||||
 | 
			
		||||
    if (!isRendered()) return;
 | 
			
		||||
 | 
			
		||||
    if (isRendered()) {
 | 
			
		||||
      chart?.clear();
 | 
			
		||||
      chart.value?.clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    chart?.setOption({ ...updatedOpts, backgroundColor: 'transparent' });
 | 
			
		||||
    chart.value?.setOption({ ...updatedOpts, backgroundColor: 'transparent' });
 | 
			
		||||
 | 
			
		||||
    await onUpdated?.(chart!);
 | 
			
		||||
    await onUpdated?.(chart.value!);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function setOptions(options: T) {
 | 
			
		||||
    chart?.setOption(options);
 | 
			
		||||
    chart.value?.setOption(options);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** render chart */
 | 
			
		||||
  async function render() {
 | 
			
		||||
    if (!isRendered()) {
 | 
			
		||||
      const chartTheme = darkMode.value ? 'dark' : 'light';
 | 
			
		||||
    if (isRendered()) return;
 | 
			
		||||
 | 
			
		||||
      await nextTick();
 | 
			
		||||
    const chartTheme = darkMode.value ? 'dark' : 'light';
 | 
			
		||||
 | 
			
		||||
      chart = echarts.init(domRef.value, chartTheme);
 | 
			
		||||
    chart.value = echarts.init(domRef.value, chartTheme);
 | 
			
		||||
 | 
			
		||||
      chart.setOption({ ...chartOptions, backgroundColor: 'transparent' });
 | 
			
		||||
    chart.value?.setOption({ ...chartOptions, backgroundColor: 'transparent' });
 | 
			
		||||
 | 
			
		||||
      await onRender?.(chart);
 | 
			
		||||
    }
 | 
			
		||||
    await onRender?.(chart.value!);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** resize chart */
 | 
			
		||||
  function resize() {
 | 
			
		||||
    chart?.resize();
 | 
			
		||||
    chart.value?.resize();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** destroy chart */
 | 
			
		||||
  async function destroy() {
 | 
			
		||||
    if (!chart) return;
 | 
			
		||||
    if (!chart.value) return;
 | 
			
		||||
 | 
			
		||||
    await onDestroy?.(chart);
 | 
			
		||||
    chart?.dispose();
 | 
			
		||||
    chart = null;
 | 
			
		||||
    await onDestroy?.(chart.value);
 | 
			
		||||
    chart.value?.dispose();
 | 
			
		||||
    chart.value = null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** change chart theme */
 | 
			
		||||
  async function changeTheme() {
 | 
			
		||||
    await destroy();
 | 
			
		||||
    await render();
 | 
			
		||||
    await onUpdated?.(chart!);
 | 
			
		||||
    await onUpdated?.(chart.value!);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@@ -196,30 +187,29 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
 | 
			
		||||
    initialSize.width = w;
 | 
			
		||||
    initialSize.height = h;
 | 
			
		||||
 | 
			
		||||
    // size is abnormal, destroy chart
 | 
			
		||||
    if (!canRender()) {
 | 
			
		||||
      await destroy();
 | 
			
		||||
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // resize chart
 | 
			
		||||
    if (isRendered()) {
 | 
			
		||||
      resize();
 | 
			
		||||
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // render chart
 | 
			
		||||
    await render();
 | 
			
		||||
 | 
			
		||||
    if (chart) {
 | 
			
		||||
      await onUpdated?.(chart);
 | 
			
		||||
    if (chart.value) {
 | 
			
		||||
      await onUpdated?.(chart.value);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  scope.run(() => {
 | 
			
		||||
    watch([width, height], ([newWidth, newHeight]) => {
 | 
			
		||||
      renderChartBySize(newWidth, newHeight);
 | 
			
		||||
    });
 | 
			
		||||
    watch(
 | 
			
		||||
      [width, height],
 | 
			
		||||
      ([newWidth, newHeight]) => {
 | 
			
		||||
        renderChartBySize(newWidth, newHeight);
 | 
			
		||||
      },
 | 
			
		||||
      { flush: 'post' }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    watch(darkMode, () => {
 | 
			
		||||
      changeTheme();
 | 
			
		||||
@@ -233,6 +223,7 @@ export function useEcharts<T extends ECOption>(optionsFactory: () => T, hooks: C
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    domRef,
 | 
			
		||||
    chart,
 | 
			
		||||
    updateOptions,
 | 
			
		||||
    setOptions
 | 
			
		||||
  };
 | 
			
		||||
 
 | 
			
		||||
@@ -1,192 +1,55 @@
 | 
			
		||||
import { computed, effectScope, onScopeDispose, reactive, ref, watch } from 'vue';
 | 
			
		||||
import { computed, effectScope, onScopeDispose, reactive, shallowRef, watch } from 'vue';
 | 
			
		||||
import type { Ref } from 'vue';
 | 
			
		||||
import type { PaginationProps } from 'naive-ui';
 | 
			
		||||
import { useBoolean, useTable } from '@sa/hooks';
 | 
			
		||||
import type { PaginationData, TableColumnCheck, UseTableOptions } from '@sa/hooks';
 | 
			
		||||
import type { FlatResponseData } from '@sa/axios';
 | 
			
		||||
import { jsonClone } from '@sa/utils';
 | 
			
		||||
import { useBoolean, useHookTable } from '@sa/hooks';
 | 
			
		||||
import { useAppStore } from '@/store/modules/app';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
 | 
			
		||||
type TableData = NaiveUI.TableData;
 | 
			
		||||
type GetTableData<A extends NaiveUI.TableApiFn> = NaiveUI.GetTableData<A>;
 | 
			
		||||
type TableColumn<T> = NaiveUI.TableColumn<T>;
 | 
			
		||||
export type UseNaiveTableOptions<ResponseData, ApiData, Pagination extends boolean> = Omit<
 | 
			
		||||
  UseTableOptions<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, Pagination>,
 | 
			
		||||
  'pagination' | 'getColumnChecks' | 'getColumns'
 | 
			
		||||
> & {
 | 
			
		||||
  /**
 | 
			
		||||
   * get column visible
 | 
			
		||||
   *
 | 
			
		||||
   * @param column
 | 
			
		||||
   *
 | 
			
		||||
   * @default true
 | 
			
		||||
   *
 | 
			
		||||
   * @returns true if the column is visible, false otherwise
 | 
			
		||||
   */
 | 
			
		||||
  getColumnVisible?: (column: NaiveUI.TableColumn<ApiData>) => boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTableConfig<A>) {
 | 
			
		||||
const SELECTION_KEY = '__selection__';
 | 
			
		||||
 | 
			
		||||
const EXPAND_KEY = '__expand__';
 | 
			
		||||
 | 
			
		||||
export function useNaiveTable<ResponseData, ApiData>(options: UseNaiveTableOptions<ResponseData, ApiData, false>) {
 | 
			
		||||
  const scope = effectScope();
 | 
			
		||||
  const appStore = useAppStore();
 | 
			
		||||
 | 
			
		||||
  const isMobile = computed(() => appStore.isMobile);
 | 
			
		||||
 | 
			
		||||
  const { apiFn, apiParams, immediate, showTotal } = config;
 | 
			
		||||
 | 
			
		||||
  const SELECTION_KEY = '__selection__';
 | 
			
		||||
 | 
			
		||||
  const EXPAND_KEY = '__expand__';
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    loading,
 | 
			
		||||
    empty,
 | 
			
		||||
    data,
 | 
			
		||||
    columns,
 | 
			
		||||
    columnChecks,
 | 
			
		||||
    reloadColumns,
 | 
			
		||||
    getData,
 | 
			
		||||
    searchParams,
 | 
			
		||||
    updateSearchParams,
 | 
			
		||||
    resetSearchParams
 | 
			
		||||
  } = useHookTable<A, GetTableData<A>, TableColumn<NaiveUI.TableDataWithIndex<GetTableData<A>>>>({
 | 
			
		||||
    apiFn,
 | 
			
		||||
    apiParams,
 | 
			
		||||
    columns: config.columns,
 | 
			
		||||
    transformer: res => {
 | 
			
		||||
      const { records = [], current = 1, size = 10, total = 0 } = res.data || {};
 | 
			
		||||
 | 
			
		||||
      // Ensure that the size is greater than 0, If it is less than 0, it will cause paging calculation errors.
 | 
			
		||||
      const pageSize = size <= 0 ? 10 : size;
 | 
			
		||||
 | 
			
		||||
      const recordsWithIndex = records.map((item, index) => {
 | 
			
		||||
        return {
 | 
			
		||||
          ...item,
 | 
			
		||||
          index: (current - 1) * pageSize + index + 1
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        data: recordsWithIndex,
 | 
			
		||||
        pageNum: current,
 | 
			
		||||
        pageSize,
 | 
			
		||||
        total
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
    getColumnChecks: cols => {
 | 
			
		||||
      const checks: NaiveUI.TableColumnCheck[] = [];
 | 
			
		||||
 | 
			
		||||
      cols.forEach(column => {
 | 
			
		||||
        if (isTableColumnHasKey(column)) {
 | 
			
		||||
          checks.push({
 | 
			
		||||
            key: column.key as string,
 | 
			
		||||
            title: column.title!,
 | 
			
		||||
            checked: true
 | 
			
		||||
          });
 | 
			
		||||
        } else if (column.type === 'selection') {
 | 
			
		||||
          checks.push({
 | 
			
		||||
            key: SELECTION_KEY,
 | 
			
		||||
            title: $t('common.check'),
 | 
			
		||||
            checked: true
 | 
			
		||||
          });
 | 
			
		||||
        } else if (column.type === 'expand') {
 | 
			
		||||
          checks.push({
 | 
			
		||||
            key: EXPAND_KEY,
 | 
			
		||||
            title: $t('common.expandColumn'),
 | 
			
		||||
            checked: true
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return checks;
 | 
			
		||||
    },
 | 
			
		||||
    getColumns: (cols, checks) => {
 | 
			
		||||
      const columnMap = new Map<string, TableColumn<GetTableData<A>>>();
 | 
			
		||||
 | 
			
		||||
      cols.forEach(column => {
 | 
			
		||||
        if (isTableColumnHasKey(column)) {
 | 
			
		||||
          columnMap.set(column.key as string, column);
 | 
			
		||||
        } else if (column.type === 'selection') {
 | 
			
		||||
          columnMap.set(SELECTION_KEY, column);
 | 
			
		||||
        } else if (column.type === 'expand') {
 | 
			
		||||
          columnMap.set(EXPAND_KEY, column);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const filteredColumns = checks
 | 
			
		||||
        .filter(item => item.checked)
 | 
			
		||||
        .map(check => columnMap.get(check.key) as TableColumn<GetTableData<A>>);
 | 
			
		||||
 | 
			
		||||
      return filteredColumns;
 | 
			
		||||
    },
 | 
			
		||||
    onFetched: async transformed => {
 | 
			
		||||
      const { pageNum, pageSize, total } = transformed;
 | 
			
		||||
 | 
			
		||||
      updatePagination({
 | 
			
		||||
        page: pageNum,
 | 
			
		||||
        pageSize,
 | 
			
		||||
        itemCount: total
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    immediate
 | 
			
		||||
  const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, false>({
 | 
			
		||||
    ...options,
 | 
			
		||||
    getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
 | 
			
		||||
    getColumns
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const pagination: PaginationProps = reactive({
 | 
			
		||||
    page: 1,
 | 
			
		||||
    pageSize: 10,
 | 
			
		||||
    showSizePicker: true,
 | 
			
		||||
    itemCount: 0,
 | 
			
		||||
    pageSizes: [10, 15, 20, 25, 30],
 | 
			
		||||
    onUpdatePage: async (page: number) => {
 | 
			
		||||
      pagination.page = page;
 | 
			
		||||
 | 
			
		||||
      updateSearchParams({
 | 
			
		||||
        current: page,
 | 
			
		||||
        size: pagination.pageSize!
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      getData();
 | 
			
		||||
    },
 | 
			
		||||
    onUpdatePageSize: async (pageSize: number) => {
 | 
			
		||||
      pagination.pageSize = pageSize;
 | 
			
		||||
      pagination.page = 1;
 | 
			
		||||
 | 
			
		||||
      updateSearchParams({
 | 
			
		||||
        current: pagination.page,
 | 
			
		||||
        size: pageSize
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      getData();
 | 
			
		||||
    },
 | 
			
		||||
    ...(showTotal
 | 
			
		||||
      ? {
 | 
			
		||||
          prefix: page => $t('datatable.itemCount', { total: page.itemCount })
 | 
			
		||||
        }
 | 
			
		||||
      : {})
 | 
			
		||||
  // calculate the total width of the table this is used for horizontal scrolling
 | 
			
		||||
  const scrollX = computed(() => {
 | 
			
		||||
    return result.columns.value.reduce((acc, column) => {
 | 
			
		||||
      return acc + Number(column.width ?? column.minWidth ?? 120);
 | 
			
		||||
    }, 0);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // this is for mobile, if the system does not support mobile, you can use `pagination` directly
 | 
			
		||||
  const mobilePagination = computed(() => {
 | 
			
		||||
    const p: PaginationProps = {
 | 
			
		||||
      ...pagination,
 | 
			
		||||
      pageSlot: isMobile.value ? 3 : 9,
 | 
			
		||||
      prefix: !isMobile.value && showTotal ? pagination.prefix : undefined
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return p;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  function updatePagination(update: Partial<PaginationProps>) {
 | 
			
		||||
    Object.assign(pagination, update);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * get data by page number
 | 
			
		||||
   *
 | 
			
		||||
   * @param pageNum the page number. default is 1
 | 
			
		||||
   */
 | 
			
		||||
  async function getDataByPage(pageNum: number = 1) {
 | 
			
		||||
    updatePagination({
 | 
			
		||||
      page: pageNum
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    updateSearchParams({
 | 
			
		||||
      current: pageNum,
 | 
			
		||||
      size: pagination.pageSize!
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await getData();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  scope.run(() => {
 | 
			
		||||
    watch(
 | 
			
		||||
      () => appStore.locale,
 | 
			
		||||
      () => {
 | 
			
		||||
        reloadColumns();
 | 
			
		||||
        result.reloadColumns();
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
@@ -196,27 +59,126 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    loading,
 | 
			
		||||
    empty,
 | 
			
		||||
    data,
 | 
			
		||||
    columns,
 | 
			
		||||
    columnChecks,
 | 
			
		||||
    reloadColumns,
 | 
			
		||||
    pagination,
 | 
			
		||||
    mobilePagination,
 | 
			
		||||
    updatePagination,
 | 
			
		||||
    getData,
 | 
			
		||||
    getDataByPage,
 | 
			
		||||
    searchParams,
 | 
			
		||||
    updateSearchParams,
 | 
			
		||||
    resetSearchParams
 | 
			
		||||
    ...result,
 | 
			
		||||
    scrollX
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>, getData: () => Promise<void>) {
 | 
			
		||||
type PaginationParams = Pick<PaginationProps, 'page' | 'pageSize'>;
 | 
			
		||||
 | 
			
		||||
type UseNaivePaginatedTableOptions<ResponseData, ApiData> = UseNaiveTableOptions<ResponseData, ApiData, true> & {
 | 
			
		||||
  paginationProps?: Omit<PaginationProps, 'page' | 'pageSize' | 'itemCount'>;
 | 
			
		||||
  /**
 | 
			
		||||
   * whether to show the total count of the table
 | 
			
		||||
   *
 | 
			
		||||
   * @default true
 | 
			
		||||
   */
 | 
			
		||||
  showTotal?: boolean;
 | 
			
		||||
  onPaginationParamsChange?: (params: PaginationParams) => void | Promise<void>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function useNaivePaginatedTable<ResponseData, ApiData>(
 | 
			
		||||
  options: UseNaivePaginatedTableOptions<ResponseData, ApiData>
 | 
			
		||||
) {
 | 
			
		||||
  const scope = effectScope();
 | 
			
		||||
  const appStore = useAppStore();
 | 
			
		||||
 | 
			
		||||
  const isMobile = computed(() => appStore.isMobile);
 | 
			
		||||
 | 
			
		||||
  const showTotal = computed(() => options.showTotal ?? true);
 | 
			
		||||
 | 
			
		||||
  const pagination = reactive({
 | 
			
		||||
    page: 1,
 | 
			
		||||
    pageSize: 10,
 | 
			
		||||
    itemCount: 0,
 | 
			
		||||
    showSizePicker: true,
 | 
			
		||||
    pageSizes: [10, 15, 20, 25, 30],
 | 
			
		||||
    prefix: showTotal.value ? page => $t('datatable.itemCount', { total: page.itemCount }) : undefined,
 | 
			
		||||
    onUpdatePage(page) {
 | 
			
		||||
      pagination.page = page;
 | 
			
		||||
    },
 | 
			
		||||
    onUpdatePageSize(pageSize) {
 | 
			
		||||
      pagination.pageSize = pageSize;
 | 
			
		||||
      pagination.page = 1;
 | 
			
		||||
    },
 | 
			
		||||
    ...options.paginationProps
 | 
			
		||||
  }) as PaginationProps;
 | 
			
		||||
 | 
			
		||||
  // this is for mobile, if the system does not support mobile, you can use `pagination` directly
 | 
			
		||||
  const mobilePagination = computed(() => {
 | 
			
		||||
    const p: PaginationProps = {
 | 
			
		||||
      ...pagination,
 | 
			
		||||
      pageSlot: isMobile.value ? 3 : 9,
 | 
			
		||||
      prefix: !isMobile.value && showTotal.value ? pagination.prefix : undefined
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return p;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const paginationParams = computed(() => {
 | 
			
		||||
    const { page, pageSize } = pagination;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      page,
 | 
			
		||||
      pageSize
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const result = useTable<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, true>({
 | 
			
		||||
    ...options,
 | 
			
		||||
    pagination: true,
 | 
			
		||||
    getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible),
 | 
			
		||||
    getColumns,
 | 
			
		||||
    onFetched: data => {
 | 
			
		||||
      pagination.itemCount = data.total;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  async function getDataByPage(page: number = 1) {
 | 
			
		||||
    if (page !== pagination.page) {
 | 
			
		||||
      pagination.page = page;
 | 
			
		||||
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await result.getData();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  scope.run(() => {
 | 
			
		||||
    watch(
 | 
			
		||||
      () => appStore.locale,
 | 
			
		||||
      () => {
 | 
			
		||||
        result.reloadColumns();
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    watch(paginationParams, async newVal => {
 | 
			
		||||
      await options.onPaginationParamsChange?.(newVal);
 | 
			
		||||
 | 
			
		||||
      await result.getData();
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  onScopeDispose(() => {
 | 
			
		||||
    scope.stop();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    ...result,
 | 
			
		||||
    getDataByPage,
 | 
			
		||||
    pagination,
 | 
			
		||||
    mobilePagination
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useTableOperate<TableData>(
 | 
			
		||||
  data: Ref<TableData[]>,
 | 
			
		||||
  idKey: keyof TableData,
 | 
			
		||||
  getData: () => Promise<void>
 | 
			
		||||
) {
 | 
			
		||||
  const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean();
 | 
			
		||||
 | 
			
		||||
  const operateType = ref<NaiveUI.TableOperateType>('add');
 | 
			
		||||
  const operateType = shallowRef<NaiveUI.TableOperateType>('add');
 | 
			
		||||
 | 
			
		||||
  function handleAdd() {
 | 
			
		||||
    operateType.value = 'add';
 | 
			
		||||
@@ -224,18 +186,18 @@ export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>,
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** the editing row data */
 | 
			
		||||
  const editingData: Ref<T | null> = ref(null);
 | 
			
		||||
  const editingData = shallowRef<TableData | null>(null);
 | 
			
		||||
 | 
			
		||||
  function handleEdit(id: T['id']) {
 | 
			
		||||
  function handleEdit(id: TableData[keyof TableData]) {
 | 
			
		||||
    operateType.value = 'edit';
 | 
			
		||||
    const findItem = data.value.find(item => item.id === id) || null;
 | 
			
		||||
    const findItem = data.value.find(item => item[idKey] === id) || null;
 | 
			
		||||
    editingData.value = jsonClone(findItem);
 | 
			
		||||
 | 
			
		||||
    openDrawer();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** the checked row keys of table */
 | 
			
		||||
  const checkedRowKeys = ref<string[]>([]);
 | 
			
		||||
  const checkedRowKeys = shallowRef<string[]>([]);
 | 
			
		||||
 | 
			
		||||
  /** the hook after the batch delete operation is completed */
 | 
			
		||||
  async function onBatchDeleted() {
 | 
			
		||||
@@ -267,6 +229,82 @@ export function useTableOperate<T extends TableData = TableData>(data: Ref<T[]>,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isTableColumnHasKey<T>(column: TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
 | 
			
		||||
export function defaultTransform<ApiData>(
 | 
			
		||||
  response: FlatResponseData<any, Api.Common.PaginatingQueryRecord<ApiData>>
 | 
			
		||||
): PaginationData<ApiData> {
 | 
			
		||||
  const { data, error } = response;
 | 
			
		||||
 | 
			
		||||
  if (!error) {
 | 
			
		||||
    const { records, current, size, total } = data;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      data: records,
 | 
			
		||||
      pageNum: current,
 | 
			
		||||
      pageSize: size,
 | 
			
		||||
      total
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    data: [],
 | 
			
		||||
    pageNum: 1,
 | 
			
		||||
    pageSize: 10,
 | 
			
		||||
    total: 0
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getColumnChecks<Column extends NaiveUI.TableColumn<any>>(
 | 
			
		||||
  cols: Column[],
 | 
			
		||||
  getColumnVisible?: (column: Column) => boolean
 | 
			
		||||
) {
 | 
			
		||||
  const checks: TableColumnCheck[] = [];
 | 
			
		||||
 | 
			
		||||
  cols.forEach(column => {
 | 
			
		||||
    if (isTableColumnHasKey(column)) {
 | 
			
		||||
      checks.push({
 | 
			
		||||
        key: column.key as string,
 | 
			
		||||
        title: column.title!,
 | 
			
		||||
        checked: true,
 | 
			
		||||
        visible: getColumnVisible?.(column) ?? true
 | 
			
		||||
      });
 | 
			
		||||
    } else if (column.type === 'selection') {
 | 
			
		||||
      checks.push({
 | 
			
		||||
        key: SELECTION_KEY,
 | 
			
		||||
        title: $t('common.check'),
 | 
			
		||||
        checked: true,
 | 
			
		||||
        visible: getColumnVisible?.(column) ?? false
 | 
			
		||||
      });
 | 
			
		||||
    } else if (column.type === 'expand') {
 | 
			
		||||
      checks.push({
 | 
			
		||||
        key: EXPAND_KEY,
 | 
			
		||||
        title: $t('common.expandColumn'),
 | 
			
		||||
        checked: true,
 | 
			
		||||
        visible: getColumnVisible?.(column) ?? false
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return checks;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getColumns<Column extends NaiveUI.TableColumn<any>>(cols: Column[], checks: TableColumnCheck[]) {
 | 
			
		||||
  const columnMap = new Map<string, Column>();
 | 
			
		||||
 | 
			
		||||
  cols.forEach(column => {
 | 
			
		||||
    if (isTableColumnHasKey(column)) {
 | 
			
		||||
      columnMap.set(column.key as string, column);
 | 
			
		||||
    } else if (column.type === 'selection') {
 | 
			
		||||
      columnMap.set(SELECTION_KEY, column);
 | 
			
		||||
    } else if (column.type === 'expand') {
 | 
			
		||||
      columnMap.set(EXPAND_KEY, column);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const filteredColumns = checks.filter(item => item.checked).map(check => columnMap.get(check.key) as Column);
 | 
			
		||||
 | 
			
		||||
  return filteredColumns;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isTableColumnHasKey<T>(column: NaiveUI.TableColumn<T>): column is NaiveUI.TableColumnWithKey<T> {
 | 
			
		||||
  return Boolean((column as NaiveUI.TableColumnWithKey<T>).key);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ import GlobalTab from '../modules/global-tab/index.vue';
 | 
			
		||||
import GlobalContent from '../modules/global-content/index.vue';
 | 
			
		||||
import GlobalFooter from '../modules/global-footer/index.vue';
 | 
			
		||||
import ThemeDrawer from '../modules/theme-drawer/index.vue';
 | 
			
		||||
import { setupMixMenuContext } from '../context';
 | 
			
		||||
import { provideMixMenuContext } from '../modules/global-menu/context';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'BaseLayout'
 | 
			
		||||
@@ -18,7 +18,7 @@ defineOptions({
 | 
			
		||||
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
const { childLevelMenus, isActiveFirstLevelMenuHasChildren } = setupMixMenuContext();
 | 
			
		||||
const { secondLevelMenus, childLevelMenus, isActiveFirstLevelMenuHasChildren } = provideMixMenuContext();
 | 
			
		||||
 | 
			
		||||
const GlobalMenu = defineAsyncComponent(() => import('../modules/global-menu/index.vue'));
 | 
			
		||||
 | 
			
		||||
@@ -29,7 +29,7 @@ const layoutMode = computed(() => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const headerProps = computed(() => {
 | 
			
		||||
  const { mode, reverseHorizontalMix } = themeStore.layout;
 | 
			
		||||
  const { mode } = themeStore.layout;
 | 
			
		||||
 | 
			
		||||
  const headerPropsConfig: Record<UnionKey.ThemeLayoutMode, App.Global.HeaderProps> = {
 | 
			
		||||
    vertical: {
 | 
			
		||||
@@ -42,15 +42,25 @@ const headerProps = computed(() => {
 | 
			
		||||
      showMenu: false,
 | 
			
		||||
      showMenuToggler: false
 | 
			
		||||
    },
 | 
			
		||||
    'vertical-hybrid-header-first': {
 | 
			
		||||
      showLogo: !isActiveFirstLevelMenuHasChildren.value,
 | 
			
		||||
      showMenu: true,
 | 
			
		||||
      showMenuToggler: false
 | 
			
		||||
    },
 | 
			
		||||
    horizontal: {
 | 
			
		||||
      showLogo: true,
 | 
			
		||||
      showMenu: true,
 | 
			
		||||
      showMenuToggler: false
 | 
			
		||||
    },
 | 
			
		||||
    'horizontal-mix': {
 | 
			
		||||
    'top-hybrid-sidebar-first': {
 | 
			
		||||
      showLogo: true,
 | 
			
		||||
      showMenu: true,
 | 
			
		||||
      showMenuToggler: reverseHorizontalMix && isActiveFirstLevelMenuHasChildren.value
 | 
			
		||||
      showMenuToggler: false
 | 
			
		||||
    },
 | 
			
		||||
    'top-hybrid-header-first': {
 | 
			
		||||
      showLogo: true,
 | 
			
		||||
      showMenu: true,
 | 
			
		||||
      showMenuToggler: isActiveFirstLevelMenuHasChildren.value
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -61,44 +71,48 @@ const siderVisible = computed(() => themeStore.layout.mode !== 'horizontal');
 | 
			
		||||
 | 
			
		||||
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
 | 
			
		||||
 | 
			
		||||
const isHorizontalMix = computed(() => themeStore.layout.mode === 'horizontal-mix');
 | 
			
		||||
const isVerticalHybridHeaderFirst = computed(() => themeStore.layout.mode === 'vertical-hybrid-header-first');
 | 
			
		||||
 | 
			
		||||
const siderWidth = computed(() => getSiderWidth());
 | 
			
		||||
const isTopHybridSidebarFirst = computed(() => themeStore.layout.mode === 'top-hybrid-sidebar-first');
 | 
			
		||||
 | 
			
		||||
const siderCollapsedWidth = computed(() => getSiderCollapsedWidth());
 | 
			
		||||
const isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first');
 | 
			
		||||
 | 
			
		||||
function getSiderWidth() {
 | 
			
		||||
  const { reverseHorizontalMix } = themeStore.layout;
 | 
			
		||||
  const { width, mixWidth, mixChildMenuWidth } = themeStore.sider;
 | 
			
		||||
const siderWidth = computed(() => getSiderAndCollapsedWidth(false));
 | 
			
		||||
 | 
			
		||||
  if (isHorizontalMix.value && reverseHorizontalMix) {
 | 
			
		||||
const siderCollapsedWidth = computed(() => getSiderAndCollapsedWidth(true));
 | 
			
		||||
 | 
			
		||||
function getSiderAndCollapsedWidth(isCollapsed: boolean) {
 | 
			
		||||
  const {
 | 
			
		||||
    mixChildMenuWidth,
 | 
			
		||||
    collapsedWidth,
 | 
			
		||||
    width: themeWidth,
 | 
			
		||||
    mixCollapsedWidth,
 | 
			
		||||
    mixWidth: themeMixWidth
 | 
			
		||||
  } = themeStore.sider;
 | 
			
		||||
 | 
			
		||||
  const width = isCollapsed ? collapsedWidth : themeWidth;
 | 
			
		||||
  const mixWidth = isCollapsed ? mixCollapsedWidth : themeMixWidth;
 | 
			
		||||
 | 
			
		||||
  if (isTopHybridHeaderFirst.value) {
 | 
			
		||||
    return isActiveFirstLevelMenuHasChildren.value ? width : 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let w = isVerticalMix.value || isHorizontalMix.value ? mixWidth : width;
 | 
			
		||||
 | 
			
		||||
  if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
 | 
			
		||||
    w += mixChildMenuWidth;
 | 
			
		||||
  if (isVerticalHybridHeaderFirst.value && !isActiveFirstLevelMenuHasChildren.value) {
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return w;
 | 
			
		||||
}
 | 
			
		||||
  const isMixMode = isVerticalMix.value || isTopHybridSidebarFirst.value || isVerticalHybridHeaderFirst.value;
 | 
			
		||||
  let finalWidth = isMixMode ? mixWidth : width;
 | 
			
		||||
 | 
			
		||||
function getSiderCollapsedWidth() {
 | 
			
		||||
  const { reverseHorizontalMix } = themeStore.layout;
 | 
			
		||||
  const { collapsedWidth, mixCollapsedWidth, mixChildMenuWidth } = themeStore.sider;
 | 
			
		||||
 | 
			
		||||
  if (isHorizontalMix.value && reverseHorizontalMix) {
 | 
			
		||||
    return isActiveFirstLevelMenuHasChildren.value ? collapsedWidth : 0;
 | 
			
		||||
  if (isVerticalMix.value && appStore.mixSiderFixed && secondLevelMenus.value.length) {
 | 
			
		||||
    finalWidth += mixChildMenuWidth;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let w = isVerticalMix.value || isHorizontalMix.value ? mixCollapsedWidth : collapsedWidth;
 | 
			
		||||
 | 
			
		||||
  if (isVerticalMix.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
 | 
			
		||||
    w += mixChildMenuWidth;
 | 
			
		||||
  if (isVerticalHybridHeaderFirst.value && appStore.mixSiderFixed && childLevelMenus.value.length) {
 | 
			
		||||
    finalWidth += mixChildMenuWidth;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return w;
 | 
			
		||||
  return finalWidth;
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,83 +0,0 @@
 | 
			
		||||
import { computed, ref, watch } from 'vue';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import { useContext } from '@sa/hooks';
 | 
			
		||||
import { useRouteStore } from '@/store/modules/route';
 | 
			
		||||
 | 
			
		||||
export const { setupStore: setupMixMenuContext, useStore: useMixMenuContext } = useContext('mix-menu', useMixMenu);
 | 
			
		||||
 | 
			
		||||
function useMixMenu() {
 | 
			
		||||
  const route = useRoute();
 | 
			
		||||
  const routeStore = useRouteStore();
 | 
			
		||||
  const { selectedKey } = useMenu();
 | 
			
		||||
 | 
			
		||||
  const activeFirstLevelMenuKey = ref('');
 | 
			
		||||
 | 
			
		||||
  function setActiveFirstLevelMenuKey(key: string) {
 | 
			
		||||
    activeFirstLevelMenuKey.value = key;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function getActiveFirstLevelMenuKey() {
 | 
			
		||||
    const [firstLevelRouteName] = selectedKey.value.split('_');
 | 
			
		||||
 | 
			
		||||
    setActiveFirstLevelMenuKey(firstLevelRouteName);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const allMenus = computed<App.Global.Menu[]>(() => routeStore.menus);
 | 
			
		||||
 | 
			
		||||
  const firstLevelMenus = computed<App.Global.Menu[]>(() =>
 | 
			
		||||
    routeStore.menus.map(menu => {
 | 
			
		||||
      const { children: _, ...rest } = menu;
 | 
			
		||||
 | 
			
		||||
      return rest;
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const childLevelMenus = computed<App.Global.Menu[]>(
 | 
			
		||||
    () => routeStore.menus.find(menu => menu.key === activeFirstLevelMenuKey.value)?.children || []
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const isActiveFirstLevelMenuHasChildren = computed(() => {
 | 
			
		||||
    if (!activeFirstLevelMenuKey.value) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const findItem = allMenus.value.find(item => item.key === activeFirstLevelMenuKey.value);
 | 
			
		||||
 | 
			
		||||
    return Boolean(findItem?.children?.length);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  watch(
 | 
			
		||||
    () => route.name,
 | 
			
		||||
    () => {
 | 
			
		||||
      getActiveFirstLevelMenuKey();
 | 
			
		||||
    },
 | 
			
		||||
    { immediate: true }
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    allMenus,
 | 
			
		||||
    firstLevelMenus,
 | 
			
		||||
    childLevelMenus,
 | 
			
		||||
    isActiveFirstLevelMenuHasChildren,
 | 
			
		||||
    activeFirstLevelMenuKey,
 | 
			
		||||
    setActiveFirstLevelMenuKey,
 | 
			
		||||
    getActiveFirstLevelMenuKey
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useMenu() {
 | 
			
		||||
  const route = useRoute();
 | 
			
		||||
 | 
			
		||||
  const selectedKey = computed(() => {
 | 
			
		||||
    const { hideInMenu, activeMenu } = route.meta;
 | 
			
		||||
    const name = route.name as string;
 | 
			
		||||
 | 
			
		||||
    const routeName = (hideInMenu ? activeMenu : name) || name;
 | 
			
		||||
 | 
			
		||||
    return routeName;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    selectedKey
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@@ -3,6 +3,7 @@ import { computed } from 'vue';
 | 
			
		||||
import { createReusableTemplate } from '@vueuse/core';
 | 
			
		||||
import { SimpleScrollbar } from '@sa/materials';
 | 
			
		||||
import { transformColorWithOpacity } from '@sa/color';
 | 
			
		||||
import type { RouteKey } from '@elegant-router/types';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'FirstLevelMenu'
 | 
			
		||||
@@ -20,7 +21,7 @@ interface Props {
 | 
			
		||||
const props = defineProps<Props>();
 | 
			
		||||
 | 
			
		||||
interface Emits {
 | 
			
		||||
  (e: 'select', menu: App.Global.Menu): boolean;
 | 
			
		||||
  (e: 'select', menuKey: RouteKey): boolean;
 | 
			
		||||
  (e: 'toggleSiderCollapse'): void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -47,8 +48,8 @@ const selectedBgColor = computed(() => {
 | 
			
		||||
  return darkMode ? dark : light;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function handleClickMixMenu(menu: App.Global.Menu) {
 | 
			
		||||
  emit('select', menu);
 | 
			
		||||
function handleClickMixMenu(menuKey: RouteKey) {
 | 
			
		||||
  emit('select', menuKey);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toggleSiderCollapse() {
 | 
			
		||||
@@ -88,7 +89,7 @@ function toggleSiderCollapse() {
 | 
			
		||||
        :icon="menu.icon"
 | 
			
		||||
        :active="menu.key === activeMenuKey"
 | 
			
		||||
        :is-mini="siderCollapse"
 | 
			
		||||
        @click="handleClickMixMenu(menu)"
 | 
			
		||||
        @click="handleClickMixMenu(menu.routeKey)"
 | 
			
		||||
      />
 | 
			
		||||
    </SimpleScrollbar>
 | 
			
		||||
    <MenuToggler
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										143
									
								
								src/layouts/modules/global-menu/context/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								src/layouts/modules/global-menu/context/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,143 @@
 | 
			
		||||
import { computed, ref, watch } from 'vue';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import { useContext } from '@sa/hooks';
 | 
			
		||||
import type { RouteKey } from '@elegant-router/types';
 | 
			
		||||
import { useRouteStore } from '@/store/modules/route';
 | 
			
		||||
import { useRouterPush } from '@/hooks/common/router';
 | 
			
		||||
 | 
			
		||||
export const [provideMixMenuContext, useMixMenuContext] = useContext('MixMenu', useMixMenu);
 | 
			
		||||
 | 
			
		||||
function useMixMenu() {
 | 
			
		||||
  const route = useRoute();
 | 
			
		||||
  const routeStore = useRouteStore();
 | 
			
		||||
  const { selectedKey } = useMenu();
 | 
			
		||||
  const { routerPushByKeyWithMetaQuery } = useRouterPush();
 | 
			
		||||
 | 
			
		||||
  const allMenus = computed<App.Global.Menu[]>(() => routeStore.menus);
 | 
			
		||||
 | 
			
		||||
  const firstLevelMenus = computed<App.Global.Menu[]>(() =>
 | 
			
		||||
    routeStore.menus.map(menu => {
 | 
			
		||||
      const { children: _, ...rest } = menu;
 | 
			
		||||
 | 
			
		||||
      return rest;
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const activeFirstLevelMenuKey = ref('');
 | 
			
		||||
 | 
			
		||||
  function setActiveFirstLevelMenuKey(key: string) {
 | 
			
		||||
    activeFirstLevelMenuKey.value = key;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function getActiveFirstLevelMenuKey() {
 | 
			
		||||
    const [firstLevelRouteName] = selectedKey.value.split('_');
 | 
			
		||||
 | 
			
		||||
    setActiveFirstLevelMenuKey(firstLevelRouteName);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const isActiveFirstLevelMenuHasChildren = computed(() => {
 | 
			
		||||
    if (!activeFirstLevelMenuKey.value) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const findItem = allMenus.value.find(item => item.key === activeFirstLevelMenuKey.value);
 | 
			
		||||
 | 
			
		||||
    return Boolean(findItem?.children?.length);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  function handleSelectFirstLevelMenu(key: RouteKey) {
 | 
			
		||||
    setActiveFirstLevelMenuKey(key);
 | 
			
		||||
 | 
			
		||||
    if (!isActiveFirstLevelMenuHasChildren.value) {
 | 
			
		||||
      routerPushByKeyWithMetaQuery(key);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const secondLevelMenus = computed<App.Global.Menu[]>(
 | 
			
		||||
    () => allMenus.value.find(menu => menu.key === activeFirstLevelMenuKey.value)?.children || []
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const activeSecondLevelMenuKey = ref('');
 | 
			
		||||
 | 
			
		||||
  function setActiveSecondLevelMenuKey(key: string) {
 | 
			
		||||
    activeSecondLevelMenuKey.value = key;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function getActiveSecondLevelMenuKey() {
 | 
			
		||||
    const keys = selectedKey.value.split('_');
 | 
			
		||||
 | 
			
		||||
    if (keys.length < 2) {
 | 
			
		||||
      setActiveSecondLevelMenuKey('');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const [firstLevelRouteName, level2SuffixName] = keys;
 | 
			
		||||
 | 
			
		||||
    const secondLevelRouteName = `${firstLevelRouteName}_${level2SuffixName}`;
 | 
			
		||||
 | 
			
		||||
    setActiveSecondLevelMenuKey(secondLevelRouteName);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const isActiveSecondLevelMenuHasChildren = computed(() => {
 | 
			
		||||
    if (!activeSecondLevelMenuKey.value) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const findItem = secondLevelMenus.value.find(item => item.key === activeSecondLevelMenuKey.value);
 | 
			
		||||
 | 
			
		||||
    return Boolean(findItem?.children?.length);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  function handleSelectSecondLevelMenu(key: RouteKey) {
 | 
			
		||||
    setActiveSecondLevelMenuKey(key);
 | 
			
		||||
 | 
			
		||||
    if (!isActiveSecondLevelMenuHasChildren.value) {
 | 
			
		||||
      routerPushByKeyWithMetaQuery(key);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const childLevelMenus = computed<App.Global.Menu[]>(
 | 
			
		||||
    () => secondLevelMenus.value.find(menu => menu.key === activeSecondLevelMenuKey.value)?.children || []
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  watch(
 | 
			
		||||
    () => route.name,
 | 
			
		||||
    () => {
 | 
			
		||||
      getActiveFirstLevelMenuKey();
 | 
			
		||||
    },
 | 
			
		||||
    { immediate: true }
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    firstLevelMenus,
 | 
			
		||||
    activeFirstLevelMenuKey,
 | 
			
		||||
    setActiveFirstLevelMenuKey,
 | 
			
		||||
    isActiveFirstLevelMenuHasChildren,
 | 
			
		||||
    handleSelectFirstLevelMenu,
 | 
			
		||||
    getActiveFirstLevelMenuKey,
 | 
			
		||||
    secondLevelMenus,
 | 
			
		||||
    activeSecondLevelMenuKey,
 | 
			
		||||
    setActiveSecondLevelMenuKey,
 | 
			
		||||
    isActiveSecondLevelMenuHasChildren,
 | 
			
		||||
    handleSelectSecondLevelMenu,
 | 
			
		||||
    getActiveSecondLevelMenuKey,
 | 
			
		||||
    childLevelMenus
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useMenu() {
 | 
			
		||||
  const route = useRoute();
 | 
			
		||||
 | 
			
		||||
  const selectedKey = computed(() => {
 | 
			
		||||
    const { hideInMenu, activeMenu } = route.meta;
 | 
			
		||||
    const name = route.name as string;
 | 
			
		||||
 | 
			
		||||
    const routeName = (hideInMenu ? activeMenu : name) || name;
 | 
			
		||||
 | 
			
		||||
    return routeName;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    selectedKey
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@@ -5,9 +5,10 @@ import { useAppStore } from '@/store/modules/app';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import VerticalMenu from './modules/vertical-menu.vue';
 | 
			
		||||
import VerticalMixMenu from './modules/vertical-mix-menu.vue';
 | 
			
		||||
import VerticalHybridHeaderFirst from './modules/vertical-hybrid-header-first.vue';
 | 
			
		||||
import HorizontalMenu from './modules/horizontal-menu.vue';
 | 
			
		||||
import HorizontalMixMenu from './modules/horizontal-mix-menu.vue';
 | 
			
		||||
import ReversedHorizontalMixMenu from './modules/reversed-horizontal-mix-menu.vue';
 | 
			
		||||
import TopHybridSidebarFirst from './modules/top-hybrid-sidebar-first.vue';
 | 
			
		||||
import TopHybridHeaderFirst from './modules/top-hybrid-header-first.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'GlobalMenu'
 | 
			
		||||
@@ -20,8 +21,10 @@ const activeMenu = computed(() => {
 | 
			
		||||
  const menuMap: Record<UnionKey.ThemeLayoutMode, Component> = {
 | 
			
		||||
    vertical: VerticalMenu,
 | 
			
		||||
    'vertical-mix': VerticalMixMenu,
 | 
			
		||||
    'vertical-hybrid-header-first': VerticalHybridHeaderFirst,
 | 
			
		||||
    horizontal: HorizontalMenu,
 | 
			
		||||
    'horizontal-mix': themeStore.layout.reverseHorizontalMix ? ReversedHorizontalMixMenu : HorizontalMixMenu
 | 
			
		||||
    'top-hybrid-sidebar-first': TopHybridSidebarFirst,
 | 
			
		||||
    'top-hybrid-header-first': TopHybridHeaderFirst
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return menuMap[themeStore.layout.mode];
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
import { GLOBAL_HEADER_MENU_ID } from '@/constants/app';
 | 
			
		||||
import { useRouteStore } from '@/store/modules/route';
 | 
			
		||||
import { useRouterPush } from '@/hooks/common/router';
 | 
			
		||||
import { useMenu } from '../../../context';
 | 
			
		||||
import { useMenu } from '../context';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'HorizontalMenu'
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,16 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, watch } from 'vue';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import type { RouteKey } from '@elegant-router/types';
 | 
			
		||||
import { SimpleScrollbar } from '@sa/materials';
 | 
			
		||||
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
 | 
			
		||||
import { useAppStore } from '@/store/modules/app';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { useRouteStore } from '@/store/modules/route';
 | 
			
		||||
import { useRouterPush } from '@/hooks/common/router';
 | 
			
		||||
import { useMenu, useMixMenuContext } from '../../../context';
 | 
			
		||||
import { useMenu, useMixMenuContext } from '../context';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'ReversedHorizontalMixMenu'
 | 
			
		||||
  name: 'TopHybridHeaderFirst'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
@@ -19,23 +18,10 @@ const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
const routeStore = useRouteStore();
 | 
			
		||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
 | 
			
		||||
const {
 | 
			
		||||
  firstLevelMenus,
 | 
			
		||||
  childLevelMenus,
 | 
			
		||||
  activeFirstLevelMenuKey,
 | 
			
		||||
  setActiveFirstLevelMenuKey,
 | 
			
		||||
  isActiveFirstLevelMenuHasChildren
 | 
			
		||||
} = useMixMenuContext();
 | 
			
		||||
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } =
 | 
			
		||||
  useMixMenuContext('TopHybridHeaderFirst');
 | 
			
		||||
const { selectedKey } = useMenu();
 | 
			
		||||
 | 
			
		||||
function handleSelectMixMenu(key: RouteKey) {
 | 
			
		||||
  setActiveFirstLevelMenuKey(key);
 | 
			
		||||
 | 
			
		||||
  if (!isActiveFirstLevelMenuHasChildren.value) {
 | 
			
		||||
    routerPushByKeyWithMetaQuery(key);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const expandedKeys = ref<string[]>([]);
 | 
			
		||||
 | 
			
		||||
function updateExpandedKeys() {
 | 
			
		||||
@@ -63,7 +49,7 @@ watch(
 | 
			
		||||
      :options="firstLevelMenus"
 | 
			
		||||
      :indent="18"
 | 
			
		||||
      responsive
 | 
			
		||||
      @update:value="handleSelectMixMenu"
 | 
			
		||||
      @update:value="handleSelectFirstLevelMenu"
 | 
			
		||||
    />
 | 
			
		||||
  </Teleport>
 | 
			
		||||
  <Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
 | 
			
		||||
@@ -75,7 +61,7 @@ watch(
 | 
			
		||||
        :collapsed="appStore.siderCollapse"
 | 
			
		||||
        :collapsed-width="themeStore.sider.collapsedWidth"
 | 
			
		||||
        :collapsed-icon-size="22"
 | 
			
		||||
        :options="childLevelMenus"
 | 
			
		||||
        :options="secondLevelMenus"
 | 
			
		||||
        :indent="18"
 | 
			
		||||
        @update:value="routerPushByKeyWithMetaQuery"
 | 
			
		||||
      />
 | 
			
		||||
@@ -4,25 +4,18 @@ import { useAppStore } from '@/store/modules/app';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { useRouterPush } from '@/hooks/common/router';
 | 
			
		||||
import FirstLevelMenu from '../components/first-level-menu.vue';
 | 
			
		||||
import { useMenu, useMixMenuContext } from '../../../context';
 | 
			
		||||
import { useMenu, useMixMenuContext } from '../context';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'HorizontalMixMenu'
 | 
			
		||||
  name: 'TopHybridSidebarFirst'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
 | 
			
		||||
const { allMenus, childLevelMenus, activeFirstLevelMenuKey, setActiveFirstLevelMenuKey } = useMixMenuContext();
 | 
			
		||||
const { firstLevelMenus, secondLevelMenus, activeFirstLevelMenuKey, handleSelectFirstLevelMenu } =
 | 
			
		||||
  useMixMenuContext('TopHybridSidebarFirst');
 | 
			
		||||
const { selectedKey } = useMenu();
 | 
			
		||||
 | 
			
		||||
function handleSelectMixMenu(menu: App.Global.Menu) {
 | 
			
		||||
  setActiveFirstLevelMenuKey(menu.key);
 | 
			
		||||
 | 
			
		||||
  if (!menu.children?.length) {
 | 
			
		||||
    routerPushByKeyWithMetaQuery(menu.routeKey);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
@@ -30,22 +23,24 @@ function handleSelectMixMenu(menu: App.Global.Menu) {
 | 
			
		||||
    <NMenu
 | 
			
		||||
      mode="horizontal"
 | 
			
		||||
      :value="selectedKey"
 | 
			
		||||
      :options="childLevelMenus"
 | 
			
		||||
      :options="secondLevelMenus"
 | 
			
		||||
      :indent="18"
 | 
			
		||||
      responsive
 | 
			
		||||
      @update:value="routerPushByKeyWithMetaQuery"
 | 
			
		||||
    />
 | 
			
		||||
  </Teleport>
 | 
			
		||||
  <Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
 | 
			
		||||
    <FirstLevelMenu
 | 
			
		||||
      :menus="allMenus"
 | 
			
		||||
      :active-menu-key="activeFirstLevelMenuKey"
 | 
			
		||||
      :sider-collapse="appStore.siderCollapse"
 | 
			
		||||
      :dark-mode="themeStore.darkMode"
 | 
			
		||||
      :theme-color="themeStore.themeColor"
 | 
			
		||||
      @select="handleSelectMixMenu"
 | 
			
		||||
      @toggle-sider-collapse="appStore.toggleSiderCollapse"
 | 
			
		||||
    />
 | 
			
		||||
    <div class="h-full pt-2">
 | 
			
		||||
      <FirstLevelMenu
 | 
			
		||||
        :menus="firstLevelMenus"
 | 
			
		||||
        :active-menu-key="activeFirstLevelMenuKey"
 | 
			
		||||
        :sider-collapse="appStore.siderCollapse"
 | 
			
		||||
        :dark-mode="themeStore.darkMode"
 | 
			
		||||
        :theme-color="themeStore.themeColor"
 | 
			
		||||
        @select="handleSelectFirstLevelMenu"
 | 
			
		||||
        @toggle-sider-collapse="appStore.toggleSiderCollapse"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  </Teleport>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,149 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, ref, watch } from 'vue';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import type { RouteKey } from '@elegant-router/types';
 | 
			
		||||
import { SimpleScrollbar } from '@sa/materials';
 | 
			
		||||
import { useBoolean } from '@sa/hooks';
 | 
			
		||||
import { GLOBAL_HEADER_MENU_ID, GLOBAL_SIDER_MENU_ID } from '@/constants/app';
 | 
			
		||||
import { useAppStore } from '@/store/modules/app';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { useRouteStore } from '@/store/modules/route';
 | 
			
		||||
import { useRouterPush } from '@/hooks/common/router';
 | 
			
		||||
import { useMenu, useMixMenuContext } from '../context';
 | 
			
		||||
import FirstLevelMenu from '../components/first-level-menu.vue';
 | 
			
		||||
import GlobalLogo from '../../global-logo/index.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'VerticalHybridHeaderFirst'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
const routeStore = useRouteStore();
 | 
			
		||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
 | 
			
		||||
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
 | 
			
		||||
const {
 | 
			
		||||
  firstLevelMenus,
 | 
			
		||||
  activeFirstLevelMenuKey,
 | 
			
		||||
  handleSelectFirstLevelMenu,
 | 
			
		||||
  getActiveFirstLevelMenuKey,
 | 
			
		||||
  secondLevelMenus,
 | 
			
		||||
  activeSecondLevelMenuKey,
 | 
			
		||||
  isActiveSecondLevelMenuHasChildren,
 | 
			
		||||
  handleSelectSecondLevelMenu,
 | 
			
		||||
  getActiveSecondLevelMenuKey,
 | 
			
		||||
  childLevelMenus
 | 
			
		||||
} = useMixMenuContext('VerticalHybridHeaderFirst');
 | 
			
		||||
const { selectedKey } = useMenu();
 | 
			
		||||
 | 
			
		||||
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
 | 
			
		||||
 | 
			
		||||
const hasChildMenus = computed(() => childLevelMenus.value.length > 0);
 | 
			
		||||
 | 
			
		||||
const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
 | 
			
		||||
 | 
			
		||||
function handleSelectMixMenu(key: RouteKey) {
 | 
			
		||||
  handleSelectSecondLevelMenu(key);
 | 
			
		||||
 | 
			
		||||
  if (isActiveSecondLevelMenuHasChildren.value) {
 | 
			
		||||
    setDrawerVisible(true);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleSelectMenu(key: RouteKey) {
 | 
			
		||||
  handleSelectFirstLevelMenu(key);
 | 
			
		||||
 | 
			
		||||
  if (secondLevelMenus.value.length > 0) {
 | 
			
		||||
    handleSelectMixMenu(secondLevelMenus.value[0].routeKey);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleResetActiveMenu() {
 | 
			
		||||
  setDrawerVisible(false);
 | 
			
		||||
 | 
			
		||||
  if (!appStore.mixSiderFixed) {
 | 
			
		||||
    getActiveFirstLevelMenuKey();
 | 
			
		||||
    getActiveSecondLevelMenuKey();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const expandedKeys = ref<string[]>([]);
 | 
			
		||||
 | 
			
		||||
function updateExpandedKeys() {
 | 
			
		||||
  if (appStore.siderCollapse || !selectedKey.value) {
 | 
			
		||||
    expandedKeys.value = [];
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  expandedKeys.value = routeStore.getSelectedMenuKeyPath(selectedKey.value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
  () => route.name,
 | 
			
		||||
  () => {
 | 
			
		||||
    updateExpandedKeys();
 | 
			
		||||
  },
 | 
			
		||||
  { immediate: true }
 | 
			
		||||
);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <Teleport :to="`#${GLOBAL_HEADER_MENU_ID}`">
 | 
			
		||||
    <NMenu
 | 
			
		||||
      mode="horizontal"
 | 
			
		||||
      :value="activeFirstLevelMenuKey"
 | 
			
		||||
      :options="firstLevelMenus"
 | 
			
		||||
      :indent="18"
 | 
			
		||||
      responsive
 | 
			
		||||
      @update:value="handleSelectMenu"
 | 
			
		||||
    />
 | 
			
		||||
  </Teleport>
 | 
			
		||||
  <Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
 | 
			
		||||
    <div class="h-full flex" @mouseleave="handleResetActiveMenu">
 | 
			
		||||
      <FirstLevelMenu
 | 
			
		||||
        :menus="secondLevelMenus"
 | 
			
		||||
        :active-menu-key="activeSecondLevelMenuKey"
 | 
			
		||||
        :inverted="inverted"
 | 
			
		||||
        :sider-collapse="appStore.siderCollapse"
 | 
			
		||||
        :dark-mode="themeStore.darkMode"
 | 
			
		||||
        :theme-color="themeStore.themeColor"
 | 
			
		||||
        @select="handleSelectMixMenu"
 | 
			
		||||
        @toggle-sider-collapse="appStore.toggleSiderCollapse"
 | 
			
		||||
      >
 | 
			
		||||
        <GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" />
 | 
			
		||||
      </FirstLevelMenu>
 | 
			
		||||
      <div
 | 
			
		||||
        class="relative h-full transition-width-300"
 | 
			
		||||
        :style="{ width: appStore.mixSiderFixed && hasChildMenus ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
 | 
			
		||||
      >
 | 
			
		||||
        <DarkModeContainer
 | 
			
		||||
          class="absolute-lt h-full flex-col-stretch nowrap-hidden shadow-sm transition-all-300"
 | 
			
		||||
          :inverted="inverted"
 | 
			
		||||
          :style="{ width: showDrawer ? themeStore.sider.mixChildMenuWidth + 'px' : '0px' }"
 | 
			
		||||
        >
 | 
			
		||||
          <header class="flex-y-center justify-between px-12px" :style="{ height: themeStore.header.height + 'px' }">
 | 
			
		||||
            <h2 class="text-16px text-primary font-bold">{{ $t('system.title') }}</h2>
 | 
			
		||||
            <PinToggler
 | 
			
		||||
              :pin="appStore.mixSiderFixed"
 | 
			
		||||
              :class="{ 'text-white:88 !hover:text-white': inverted }"
 | 
			
		||||
              @click="appStore.toggleMixSiderFixed"
 | 
			
		||||
            />
 | 
			
		||||
          </header>
 | 
			
		||||
          <SimpleScrollbar>
 | 
			
		||||
            <NMenu
 | 
			
		||||
              v-model:expanded-keys="expandedKeys"
 | 
			
		||||
              mode="vertical"
 | 
			
		||||
              :value="selectedKey"
 | 
			
		||||
              :options="childLevelMenus"
 | 
			
		||||
              :inverted="inverted"
 | 
			
		||||
              :indent="18"
 | 
			
		||||
              @update:value="routerPushByKeyWithMetaQuery"
 | 
			
		||||
            />
 | 
			
		||||
          </SimpleScrollbar>
 | 
			
		||||
        </DarkModeContainer>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </Teleport>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
@@ -7,7 +7,7 @@ import { useAppStore } from '@/store/modules/app';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { useRouteStore } from '@/store/modules/route';
 | 
			
		||||
import { useRouterPush } from '@/hooks/common/router';
 | 
			
		||||
import { useMenu } from '../../../context';
 | 
			
		||||
import { useMenu } from '../context';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'VerticalMenu'
 | 
			
		||||
 
 | 
			
		||||
@@ -3,13 +3,14 @@ import { computed, ref, watch } from 'vue';
 | 
			
		||||
import { useRoute } from 'vue-router';
 | 
			
		||||
import { SimpleScrollbar } from '@sa/materials';
 | 
			
		||||
import { useBoolean } from '@sa/hooks';
 | 
			
		||||
import type { RouteKey } from '@elegant-router/types';
 | 
			
		||||
import { GLOBAL_SIDER_MENU_ID } from '@/constants/app';
 | 
			
		||||
import { useAppStore } from '@/store/modules/app';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { useRouteStore } from '@/store/modules/route';
 | 
			
		||||
import { useRouterPush } from '@/hooks/common/router';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
import { useMenu, useMixMenuContext } from '../../../context';
 | 
			
		||||
import { useMenu, useMixMenuContext } from '../context';
 | 
			
		||||
import FirstLevelMenu from '../components/first-level-menu.vue';
 | 
			
		||||
import GlobalLogo from '../../global-logo/index.vue';
 | 
			
		||||
 | 
			
		||||
@@ -24,28 +25,26 @@ const routeStore = useRouteStore();
 | 
			
		||||
const { routerPushByKeyWithMetaQuery } = useRouterPush();
 | 
			
		||||
const { bool: drawerVisible, setBool: setDrawerVisible } = useBoolean();
 | 
			
		||||
const {
 | 
			
		||||
  allMenus,
 | 
			
		||||
  childLevelMenus,
 | 
			
		||||
  firstLevelMenus,
 | 
			
		||||
  secondLevelMenus,
 | 
			
		||||
  activeFirstLevelMenuKey,
 | 
			
		||||
  setActiveFirstLevelMenuKey,
 | 
			
		||||
  getActiveFirstLevelMenuKey
 | 
			
		||||
  //
 | 
			
		||||
} = useMixMenuContext();
 | 
			
		||||
  isActiveFirstLevelMenuHasChildren,
 | 
			
		||||
  getActiveFirstLevelMenuKey,
 | 
			
		||||
  handleSelectFirstLevelMenu
 | 
			
		||||
} = useMixMenuContext('VerticalMixMenu');
 | 
			
		||||
const { selectedKey } = useMenu();
 | 
			
		||||
 | 
			
		||||
const inverted = computed(() => !themeStore.darkMode && themeStore.sider.inverted);
 | 
			
		||||
 | 
			
		||||
const hasChildMenus = computed(() => childLevelMenus.value.length > 0);
 | 
			
		||||
const hasChildMenus = computed(() => secondLevelMenus.value.length > 0);
 | 
			
		||||
 | 
			
		||||
const showDrawer = computed(() => hasChildMenus.value && (drawerVisible.value || appStore.mixSiderFixed));
 | 
			
		||||
 | 
			
		||||
function handleSelectMixMenu(menu: App.Global.Menu) {
 | 
			
		||||
  setActiveFirstLevelMenuKey(menu.key);
 | 
			
		||||
function handleSelectMenu(key: RouteKey) {
 | 
			
		||||
  handleSelectFirstLevelMenu(key);
 | 
			
		||||
 | 
			
		||||
  if (menu.children?.length) {
 | 
			
		||||
  if (isActiveFirstLevelMenuHasChildren.value) {
 | 
			
		||||
    setDrawerVisible(true);
 | 
			
		||||
  } else {
 | 
			
		||||
    routerPushByKeyWithMetaQuery(menu.routeKey);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -80,13 +79,13 @@ watch(
 | 
			
		||||
  <Teleport :to="`#${GLOBAL_SIDER_MENU_ID}`">
 | 
			
		||||
    <div class="h-full flex" @mouseleave="handleResetActiveMenu">
 | 
			
		||||
      <FirstLevelMenu
 | 
			
		||||
        :menus="allMenus"
 | 
			
		||||
        :menus="firstLevelMenus"
 | 
			
		||||
        :active-menu-key="activeFirstLevelMenuKey"
 | 
			
		||||
        :inverted="inverted"
 | 
			
		||||
        :sider-collapse="appStore.siderCollapse"
 | 
			
		||||
        :dark-mode="themeStore.darkMode"
 | 
			
		||||
        :theme-color="themeStore.themeColor"
 | 
			
		||||
        @select="handleSelectMixMenu"
 | 
			
		||||
        @select="handleSelectMenu"
 | 
			
		||||
        @toggle-sider-collapse="appStore.toggleSiderCollapse"
 | 
			
		||||
      >
 | 
			
		||||
        <GlobalLogo :show-title="false" :style="{ height: themeStore.header.height + 'px' }" />
 | 
			
		||||
@@ -113,7 +112,7 @@ watch(
 | 
			
		||||
              v-model:expanded-keys="expandedKeys"
 | 
			
		||||
              mode="vertical"
 | 
			
		||||
              :value="selectedKey"
 | 
			
		||||
              :options="childLevelMenus"
 | 
			
		||||
              :options="secondLevelMenus"
 | 
			
		||||
              :inverted="inverted"
 | 
			
		||||
              :indent="18"
 | 
			
		||||
              @update:value="routerPushByKeyWithMetaQuery"
 | 
			
		||||
 
 | 
			
		||||
@@ -12,10 +12,13 @@ defineOptions({
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
 | 
			
		||||
const isVerticalMix = computed(() => themeStore.layout.mode === 'vertical-mix');
 | 
			
		||||
const isHorizontalMix = computed(() => themeStore.layout.mode === 'horizontal-mix');
 | 
			
		||||
const darkMenu = computed(() => !themeStore.darkMode && !isHorizontalMix.value && themeStore.sider.inverted);
 | 
			
		||||
const showLogo = computed(() => !isVerticalMix.value && !isHorizontalMix.value);
 | 
			
		||||
const isTopHybridSidebarFirst = computed(() => themeStore.layout.mode === 'top-hybrid-sidebar-first');
 | 
			
		||||
const isTopHybridHeaderFirst = computed(() => themeStore.layout.mode === 'top-hybrid-header-first');
 | 
			
		||||
const darkMenu = computed(
 | 
			
		||||
  () =>
 | 
			
		||||
    !themeStore.darkMode && !isTopHybridSidebarFirst.value && !isTopHybridHeaderFirst.value && themeStore.sider.inverted
 | 
			
		||||
);
 | 
			
		||||
const showLogo = computed(() => themeStore.layout.mode === 'vertical');
 | 
			
		||||
const menuWrapperClass = computed(() => (showLogo.value ? 'flex-1-hidden' : 'h-full'));
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,8 @@ const tabRef = ref<HTMLElement>();
 | 
			
		||||
const isPCFlag = isPC();
 | 
			
		||||
 | 
			
		||||
const TAB_DATA_ID = 'data-tab-id';
 | 
			
		||||
const MIDDLE_MOUSE_BUTTON = 1;
 | 
			
		||||
const RIGHT_MOUSE_BUTTON = 2;
 | 
			
		||||
 | 
			
		||||
type TabNamedNodeMap = NamedNodeMap & {
 | 
			
		||||
  [TAB_DATA_ID]: Attr;
 | 
			
		||||
@@ -84,6 +86,26 @@ function handleCloseTab(tab: App.Global.Tab) {
 | 
			
		||||
  tabStore.removeTab(tab.id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleMousedown(e: MouseEvent, tab: App.Global.Tab) {
 | 
			
		||||
  const isMiddleClick = e.button === MIDDLE_MOUSE_BUTTON;
 | 
			
		||||
  if (!isMiddleClick || !themeStore.tab.closeTabByMiddleClick) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (tabStore.isTabRetain(tab.id)) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  e.preventDefault();
 | 
			
		||||
  handleCloseTab(tab);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function switchTab(e: MouseEvent, tab: App.Global.Tab) {
 | 
			
		||||
  if ([MIDDLE_MOUSE_BUTTON, RIGHT_MOUSE_BUTTON].includes(e.button)) return;
 | 
			
		||||
 | 
			
		||||
  tabStore.switchRouteByTab(tab);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function refresh() {
 | 
			
		||||
  appStore.reloadPage(500);
 | 
			
		||||
}
 | 
			
		||||
@@ -169,7 +191,9 @@ init();
 | 
			
		||||
        <div
 | 
			
		||||
          ref="tabRef"
 | 
			
		||||
          class="h-full flex pr-18px"
 | 
			
		||||
          :class="[themeStore.tab.mode === 'chrome' ? 'items-end' : 'items-center gap-12px']"
 | 
			
		||||
          :class="[
 | 
			
		||||
            themeStore.tab.mode === 'chrome' || themeStore.tab.mode === 'slider' ? 'items-end' : 'items-center gap-12px'
 | 
			
		||||
          ]"
 | 
			
		||||
        >
 | 
			
		||||
          <PageTab
 | 
			
		||||
            v-for="tab in tabStore.tabs"
 | 
			
		||||
@@ -180,7 +204,8 @@ init();
 | 
			
		||||
            :active="tab.id === tabStore.activeTabId"
 | 
			
		||||
            :active-color="themeStore.themeColor"
 | 
			
		||||
            :closable="!tabStore.isTabRetain(tab.id)"
 | 
			
		||||
            @pointerdown="tabStore.switchRouteByTab(tab)"
 | 
			
		||||
            @pointerdown="switchTab($event, tab)"
 | 
			
		||||
            @mousedown="handleMousedown($event, tab)"
 | 
			
		||||
            @close="handleCloseTab(tab)"
 | 
			
		||||
            @contextmenu="handleContextMenu($event, tab.id)"
 | 
			
		||||
          >
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,6 @@ type LayoutConfig = Record<
 | 
			
		||||
  UnionKey.ThemeLayoutMode,
 | 
			
		||||
  {
 | 
			
		||||
    placement: PopoverPlacement;
 | 
			
		||||
    headerClass: string;
 | 
			
		||||
    menuClass: string;
 | 
			
		||||
    mainClass: string;
 | 
			
		||||
  }
 | 
			
		||||
@@ -36,25 +35,31 @@ type LayoutConfig = Record<
 | 
			
		||||
const layoutConfig: LayoutConfig = {
 | 
			
		||||
  vertical: {
 | 
			
		||||
    placement: 'bottom',
 | 
			
		||||
    headerClass: '',
 | 
			
		||||
    menuClass: 'w-1/3 h-full',
 | 
			
		||||
    mainClass: 'w-2/3 h-3/4'
 | 
			
		||||
  },
 | 
			
		||||
  'vertical-mix': {
 | 
			
		||||
    placement: 'bottom',
 | 
			
		||||
    headerClass: '',
 | 
			
		||||
    menuClass: 'w-1/4 h-full',
 | 
			
		||||
    mainClass: 'w-2/3 h-3/4'
 | 
			
		||||
  },
 | 
			
		||||
  'vertical-hybrid-header-first': {
 | 
			
		||||
    placement: 'bottom',
 | 
			
		||||
    menuClass: 'w-1/4 h-full',
 | 
			
		||||
    mainClass: 'w-2/3 h-3/4'
 | 
			
		||||
  },
 | 
			
		||||
  horizontal: {
 | 
			
		||||
    placement: 'bottom',
 | 
			
		||||
    headerClass: '',
 | 
			
		||||
    menuClass: 'w-full h-1/4',
 | 
			
		||||
    mainClass: 'w-full h-3/4'
 | 
			
		||||
  },
 | 
			
		||||
  'horizontal-mix': {
 | 
			
		||||
  'top-hybrid-sidebar-first': {
 | 
			
		||||
    placement: 'bottom',
 | 
			
		||||
    menuClass: 'w-full h-1/4',
 | 
			
		||||
    mainClass: 'w-2/3 h-3/4'
 | 
			
		||||
  },
 | 
			
		||||
  'top-hybrid-header-first': {
 | 
			
		||||
    placement: 'bottom',
 | 
			
		||||
    headerClass: '',
 | 
			
		||||
    menuClass: 'w-full h-1/4',
 | 
			
		||||
    mainClass: 'w-2/3 h-3/4'
 | 
			
		||||
  }
 | 
			
		||||
@@ -68,25 +73,27 @@ function handleChangeMode(mode: UnionKey.ThemeLayoutMode) {
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="flex-center flex-wrap gap-x-32px gap-y-16px">
 | 
			
		||||
  <div class="grid grid-cols-2 gap-x-16px gap-y-12px md:grid-cols-3">
 | 
			
		||||
    <div
 | 
			
		||||
      v-for="(item, key) in layoutConfig"
 | 
			
		||||
      :key="key"
 | 
			
		||||
      class="flex cursor-pointer border-2px rounded-6px hover:border-primary"
 | 
			
		||||
      :class="[mode === key ? 'border-primary' : 'border-transparent']"
 | 
			
		||||
      class="flex-col-center cursor-pointer"
 | 
			
		||||
      @click="handleChangeMode(key)"
 | 
			
		||||
    >
 | 
			
		||||
      <NTooltip :placement="item.placement">
 | 
			
		||||
      <IconTooltip :placement="item.placement">
 | 
			
		||||
        <template #trigger>
 | 
			
		||||
          <div
 | 
			
		||||
            class="h-64px w-96px gap-6px rd-4px p-6px shadow dark:shadow-coolGray-5"
 | 
			
		||||
            :class="[key.includes('vertical') ? 'flex' : 'flex-col']"
 | 
			
		||||
            class="h-64px w-96px gap-6px rd-4px p-6px shadow ring-2 ring-transparent transition-all hover:ring-primary"
 | 
			
		||||
            :class="{ '!ring-primary': mode === key }"
 | 
			
		||||
          >
 | 
			
		||||
            <slot :name="key"></slot>
 | 
			
		||||
            <div class="h-full w-full gap-1" :class="[key.includes('vertical') ? 'flex' : 'flex-col']">
 | 
			
		||||
              <slot :name="key"></slot>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </template>
 | 
			
		||||
        {{ $t(themeLayoutModeRecord[key]) }}
 | 
			
		||||
      </NTooltip>
 | 
			
		||||
        {{ $t(`theme.layout.layoutMode.${key}_detail`) }}
 | 
			
		||||
      </IconTooltip>
 | 
			
		||||
      <p class="mt-8px text-12px">{{ $t(themeLayoutModeRecord[key]) }}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ defineProps<Props>();
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="w-full flex-y-center justify-between">
 | 
			
		||||
    <div>
 | 
			
		||||
    <div class="flex-y-center">
 | 
			
		||||
      <span class="pr-8px text-base-text">{{ label }}</span>
 | 
			
		||||
      <slot name="suffix"></slot>
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,26 +1,51 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, ref } from 'vue';
 | 
			
		||||
import { useAppStore } from '@/store/modules/app';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
import DarkMode from './modules/dark-mode.vue';
 | 
			
		||||
import LayoutMode from './modules/layout-mode.vue';
 | 
			
		||||
import ThemeColor from './modules/theme-color.vue';
 | 
			
		||||
import PageFun from './modules/page-fun.vue';
 | 
			
		||||
import AppearanceSettings from './modules/appearance/index.vue';
 | 
			
		||||
import LayoutSettings from './modules/layout/index.vue';
 | 
			
		||||
import GeneralSettings from './modules/general/index.vue';
 | 
			
		||||
import ConfigOperation from './modules/config-operation.vue';
 | 
			
		||||
import PresetSettings from './modules/preset/index.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'ThemeDrawer'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const activeTab = ref('appearance');
 | 
			
		||||
 | 
			
		||||
const drawerWidth = computed(() => {
 | 
			
		||||
  const width = 400;
 | 
			
		||||
 | 
			
		||||
  // On mobile devices, use 90% of viewport width with a maximum of 400px
 | 
			
		||||
  if (appStore.isMobile) {
 | 
			
		||||
    return `min(90vw, ${width}px)`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return width;
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <NDrawer v-model:show="appStore.themeDrawerVisible" display-directive="show" :width="360">
 | 
			
		||||
  <NDrawer v-model:show="appStore.themeDrawerVisible" display-directive="show" :width="drawerWidth">
 | 
			
		||||
    <NDrawerContent :title="$t('theme.themeDrawerTitle')" :native-scrollbar="false" closable>
 | 
			
		||||
      <DarkMode />
 | 
			
		||||
      <LayoutMode />
 | 
			
		||||
      <ThemeColor />
 | 
			
		||||
      <PageFun />
 | 
			
		||||
      <NTabs v-model:value="activeTab" type="segment" size="medium" class="mb-16px">
 | 
			
		||||
        <NTab name="appearance" :tab="$t('theme.tabs.appearance')"></NTab>
 | 
			
		||||
        <NTab name="layout" :tab="$t('theme.tabs.layout')"></NTab>
 | 
			
		||||
        <NTab name="general" :tab="$t('theme.tabs.general')"></NTab>
 | 
			
		||||
        <NTab name="preset" :tab="$t('theme.tabs.preset')"></NTab>
 | 
			
		||||
      </NTabs>
 | 
			
		||||
 | 
			
		||||
      <div class="min-h-400px">
 | 
			
		||||
        <KeepAlive>
 | 
			
		||||
          <AppearanceSettings v-if="activeTab === 'appearance'" />
 | 
			
		||||
          <LayoutSettings v-else-if="activeTab === 'layout'" />
 | 
			
		||||
          <GeneralSettings v-else-if="activeTab === 'general'" />
 | 
			
		||||
          <PresetSettings v-else-if="activeTab === 'preset'" />
 | 
			
		||||
        </KeepAlive>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <template #footer>
 | 
			
		||||
        <ConfigOperation />
 | 
			
		||||
      </template>
 | 
			
		||||
@@ -28,4 +53,14 @@ const appStore = useAppStore();
 | 
			
		||||
  </NDrawer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
<style scoped>
 | 
			
		||||
:deep(.n-tab) {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:deep(.n-tab-pane) {
 | 
			
		||||
  padding: 0;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import ThemeSchema from './modules/theme-schema.vue';
 | 
			
		||||
import ThemeColor from './modules/theme-color.vue';
 | 
			
		||||
import ThemeRadius from './modules/theme-radius.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'AppearanceSettings'
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="flex-col-stretch gap-16px">
 | 
			
		||||
    <ThemeSchema />
 | 
			
		||||
    <ThemeColor />
 | 
			
		||||
    <ThemeRadius />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
import SettingItem from '../components/setting-item.vue';
 | 
			
		||||
import SettingItem from '../../../components/setting-item.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'ThemeColor'
 | 
			
		||||
@@ -34,33 +34,38 @@ const swatches: string[] = [
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <NDivider>{{ $t('theme.themeColor.title') }}</NDivider>
 | 
			
		||||
  <NDivider>{{ $t('theme.appearance.themeColor.title') }}</NDivider>
 | 
			
		||||
  <div class="flex-col-stretch gap-12px">
 | 
			
		||||
    <NTooltip placement="top-start">
 | 
			
		||||
      <template #trigger>
 | 
			
		||||
        <SettingItem key="recommend-color" :label="$t('theme.recommendColor')">
 | 
			
		||||
          <NSwitch v-model:value="themeStore.recommendColor" />
 | 
			
		||||
        </SettingItem>
 | 
			
		||||
    <SettingItem key="recommend-color" :label="$t('theme.appearance.recommendColor')">
 | 
			
		||||
      <template #suffix>
 | 
			
		||||
        <IconTooltip>
 | 
			
		||||
          <p>
 | 
			
		||||
            <span class="pr-12px">{{ $t('theme.appearance.recommendColorDesc') }}</span>
 | 
			
		||||
            <br />
 | 
			
		||||
            <NButton
 | 
			
		||||
              text
 | 
			
		||||
              tag="a"
 | 
			
		||||
              href="https://uicolors.app/create"
 | 
			
		||||
              target="_blank"
 | 
			
		||||
              rel="noopener noreferrer"
 | 
			
		||||
              class="text-gray"
 | 
			
		||||
            >
 | 
			
		||||
              https://uicolors.app/create
 | 
			
		||||
            </NButton>
 | 
			
		||||
          </p>
 | 
			
		||||
        </IconTooltip>
 | 
			
		||||
      </template>
 | 
			
		||||
      <p>
 | 
			
		||||
        <span class="pr-12px">{{ $t('theme.recommendColorDesc') }}</span>
 | 
			
		||||
        <br />
 | 
			
		||||
        <NButton
 | 
			
		||||
          text
 | 
			
		||||
          tag="a"
 | 
			
		||||
          href="https://uicolors.app/create"
 | 
			
		||||
          target="_blank"
 | 
			
		||||
          rel="noopener noreferrer"
 | 
			
		||||
          class="text-gray"
 | 
			
		||||
        >
 | 
			
		||||
          https://uicolors.app/create
 | 
			
		||||
        </NButton>
 | 
			
		||||
      </p>
 | 
			
		||||
    </NTooltip>
 | 
			
		||||
    <SettingItem v-for="(_, key) in themeStore.themeColors" :key="key" :label="$t(`theme.themeColor.${key}`)">
 | 
			
		||||
      <NSwitch v-model:value="themeStore.recommendColor" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
 | 
			
		||||
    <SettingItem
 | 
			
		||||
      v-for="(_, key) in themeStore.themeColors"
 | 
			
		||||
      :key="key"
 | 
			
		||||
      :label="$t(`theme.appearance.themeColor.${key}`)"
 | 
			
		||||
    >
 | 
			
		||||
      <template v-if="key === 'info'" #suffix>
 | 
			
		||||
        <NCheckbox v-model:checked="themeStore.isInfoFollowPrimary">
 | 
			
		||||
          {{ $t('theme.themeColor.followPrimary') }}
 | 
			
		||||
          {{ $t('theme.appearance.themeColor.followPrimary') }}
 | 
			
		||||
        </NCheckbox>
 | 
			
		||||
      </template>
 | 
			
		||||
      <NColorPicker
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
import SettingItem from '../../../components/setting-item.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'ThemeRadius'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <NDivider>{{ $t('theme.appearance.themeRadius.title') }}</NDivider>
 | 
			
		||||
  <TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
 | 
			
		||||
    <SettingItem key="1" :label="$t('theme.appearance.themeRadius.title')">
 | 
			
		||||
      <NInputNumber v-model:value="themeStore.themeRadius" size="small" :step="1" :min="0" :max="16" class="w-120px" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
  </TransitionGroup>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
@@ -3,10 +3,10 @@ import { computed } from 'vue';
 | 
			
		||||
import { themeSchemaRecord } from '@/constants/app';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
import SettingItem from '../components/setting-item.vue';
 | 
			
		||||
import SettingItem from '../../../components/setting-item.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'DarkMode'
 | 
			
		||||
  name: 'ThemeSchema'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
@@ -33,7 +33,7 @@ const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layo
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <NDivider>{{ $t('theme.themeSchema.title') }}</NDivider>
 | 
			
		||||
  <NDivider>{{ $t('theme.appearance.themeSchema.title') }}</NDivider>
 | 
			
		||||
  <div class="flex-col-stretch gap-16px">
 | 
			
		||||
    <div class="i-flex-center">
 | 
			
		||||
      <NTabs
 | 
			
		||||
@@ -50,14 +50,14 @@ const showSiderInverted = computed(() => !themeStore.darkMode && themeStore.layo
 | 
			
		||||
      </NTabs>
 | 
			
		||||
    </div>
 | 
			
		||||
    <Transition name="sider-inverted">
 | 
			
		||||
      <SettingItem v-if="showSiderInverted" :label="$t('theme.sider.inverted')">
 | 
			
		||||
      <SettingItem v-if="showSiderInverted" :label="$t('theme.layout.sider.inverted')">
 | 
			
		||||
        <NSwitch v-model:value="themeStore.sider.inverted" />
 | 
			
		||||
      </SettingItem>
 | 
			
		||||
    </Transition>
 | 
			
		||||
    <SettingItem :label="$t('theme.grayscale')">
 | 
			
		||||
    <SettingItem :label="$t('theme.appearance.grayscale')">
 | 
			
		||||
      <NSwitch :value="themeStore.grayscale" @update:value="handleGrayscaleChange" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem :label="$t('theme.colourWeakness')">
 | 
			
		||||
    <SettingItem :label="$t('theme.appearance.colourWeakness')">
 | 
			
		||||
      <NSwitch :value="themeStore.colourWeakness" @update:value="handleColourWeaknessChange" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
  </div>
 | 
			
		||||
							
								
								
									
										17
									
								
								src/layouts/modules/theme-drawer/modules/general/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/layouts/modules/theme-drawer/modules/general/index.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import GlobalSettings from './modules/global-settings.vue';
 | 
			
		||||
import WatermarkSettings from './modules/watermark-settings.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'GeneralSettings'
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="flex-col-stretch gap-16px">
 | 
			
		||||
    <GlobalSettings />
 | 
			
		||||
    <WatermarkSettings />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
@@ -0,0 +1,39 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
import SettingItem from '../../../components/setting-item.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'GlobalSettings'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <NDivider>{{ $t('theme.general.title') }}</NDivider>
 | 
			
		||||
  <SettingItem :label="$t('theme.general.multilingual.visible')">
 | 
			
		||||
    <NSwitch v-model:value="themeStore.header.multilingual.visible" />
 | 
			
		||||
  </SettingItem>
 | 
			
		||||
 | 
			
		||||
  <SettingItem :label="$t('theme.general.globalSearch.visible')">
 | 
			
		||||
    <NSwitch v-model:value="themeStore.header.globalSearch.visible" />
 | 
			
		||||
  </SettingItem>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.setting-list-move,
 | 
			
		||||
.setting-list-enter-active,
 | 
			
		||||
.setting-list-leave-active {
 | 
			
		||||
  --uno: transition-all-300;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.setting-list-enter-from,
 | 
			
		||||
.setting-list-leave-to {
 | 
			
		||||
  --uno: opacity-0 -translate-x-30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.setting-list-leave-active {
 | 
			
		||||
  --uno: absolute;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,71 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { watermarkTimeFormatOptions } from '@/constants/app';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
import SettingItem from '../../../components/setting-item.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'WatermarkSettings'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
 | 
			
		||||
const isWatermarkTextVisible = computed(
 | 
			
		||||
  () => themeStore.watermark.visible && !themeStore.watermark.enableUserName && !themeStore.watermark.enableTime
 | 
			
		||||
);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <NDivider>{{ $t('theme.general.watermark.title') }}</NDivider>
 | 
			
		||||
  <TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
 | 
			
		||||
    <SettingItem key="1" :label="$t('theme.general.watermark.visible')">
 | 
			
		||||
      <NSwitch v-model:value="themeStore.watermark.visible" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="themeStore.watermark.visible" key="2" :label="$t('theme.general.watermark.enableUserName')">
 | 
			
		||||
      <NSwitch :value="themeStore.watermark.enableUserName" @update:value="themeStore.setWatermarkEnableUserName" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="themeStore.watermark.visible" key="3" :label="$t('theme.general.watermark.enableTime')">
 | 
			
		||||
      <NSwitch :value="themeStore.watermark.enableTime" @update:value="themeStore.setWatermarkEnableTime" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem
 | 
			
		||||
      v-if="themeStore.watermark.visible && themeStore.watermark.enableTime"
 | 
			
		||||
      key="4"
 | 
			
		||||
      :label="$t('theme.general.watermark.timeFormat')"
 | 
			
		||||
    >
 | 
			
		||||
      <NSelect
 | 
			
		||||
        v-model:value="themeStore.watermark.timeFormat"
 | 
			
		||||
        :options="watermarkTimeFormatOptions"
 | 
			
		||||
        size="small"
 | 
			
		||||
        class="w-210px"
 | 
			
		||||
      />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="isWatermarkTextVisible" key="5" :label="$t('theme.general.watermark.text')">
 | 
			
		||||
      <NInput
 | 
			
		||||
        v-model:value="themeStore.watermark.text"
 | 
			
		||||
        autosize
 | 
			
		||||
        type="text"
 | 
			
		||||
        size="small"
 | 
			
		||||
        class="w-120px"
 | 
			
		||||
        placeholder="SoybeanAdmin"
 | 
			
		||||
      />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
  </TransitionGroup>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.setting-list-move,
 | 
			
		||||
.setting-list-enter-active,
 | 
			
		||||
.setting-list-leave-active {
 | 
			
		||||
  --uno: transition-all-300;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.setting-list-enter-from,
 | 
			
		||||
.setting-list-leave-to {
 | 
			
		||||
  --uno: opacity-0 -translate-x-30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.setting-list-leave-active {
 | 
			
		||||
  --uno: absolute;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										29
									
								
								src/layouts/modules/theme-drawer/modules/layout/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/layouts/modules/theme-drawer/modules/layout/index.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import LayoutMode from './modules/layout-mode.vue';
 | 
			
		||||
import TabSettings from './modules/tab-settings.vue';
 | 
			
		||||
import HeaderSettings from './modules/header-settings.vue';
 | 
			
		||||
import SiderSettings from './modules/sider-settings.vue';
 | 
			
		||||
import FooterSettings from './modules/footer-settings.vue';
 | 
			
		||||
import ContentSettings from './modules/content-settings.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'LayoutSettings'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="flex-col-stretch gap-16px">
 | 
			
		||||
    <LayoutMode />
 | 
			
		||||
    <TabSettings />
 | 
			
		||||
    <HeaderSettings />
 | 
			
		||||
    <!-- The top menu mode does not have a sidebar -->
 | 
			
		||||
    <SiderSettings v-if="themeStore.layout.mode !== 'horizontal'" />
 | 
			
		||||
    <FooterSettings />
 | 
			
		||||
    <ContentSettings />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
@@ -0,0 +1,64 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { themePageAnimationModeOptions, themeScrollModeOptions } from '@/constants/app';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { translateOptions } from '@/utils/common';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
import SettingItem from '../../../components/setting-item.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'ContentSettings'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
 | 
			
		||||
const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wrapper');
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <NDivider>{{ $t('theme.layout.content.title') }}</NDivider>
 | 
			
		||||
  <TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
 | 
			
		||||
    <SettingItem key="1" :label="$t('theme.layout.content.scrollMode.title')">
 | 
			
		||||
      <template #suffix>
 | 
			
		||||
        <IconTooltip :desc="$t('theme.layout.content.scrollMode.tip')" />
 | 
			
		||||
      </template>
 | 
			
		||||
      <NSelect
 | 
			
		||||
        v-model:value="themeStore.layout.scrollMode"
 | 
			
		||||
        :options="translateOptions(themeScrollModeOptions)"
 | 
			
		||||
        size="small"
 | 
			
		||||
        class="w-120px"
 | 
			
		||||
      />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem key="2" :label="$t('theme.layout.content.page.animate')">
 | 
			
		||||
      <NSwitch v-model:value="themeStore.page.animate" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="themeStore.page.animate" key="3" :label="$t('theme.layout.content.page.mode.title')">
 | 
			
		||||
      <NSelect
 | 
			
		||||
        v-model:value="themeStore.page.animateMode"
 | 
			
		||||
        :options="translateOptions(themePageAnimationModeOptions)"
 | 
			
		||||
        size="small"
 | 
			
		||||
        class="w-120px"
 | 
			
		||||
      />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="isWrapperScrollMode" key="4" :label="$t('theme.layout.content.fixedHeaderAndTab')">
 | 
			
		||||
      <NSwitch v-model:value="themeStore.fixedHeaderAndTab" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
  </TransitionGroup>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.setting-list-move,
 | 
			
		||||
.setting-list-enter-active,
 | 
			
		||||
.setting-list-leave-active {
 | 
			
		||||
  --uno: transition-all-300;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.setting-list-enter-from,
 | 
			
		||||
.setting-list-leave-to {
 | 
			
		||||
  --uno: opacity-0 -translate-x-30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.setting-list-leave-active {
 | 
			
		||||
  --uno: absolute;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,61 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
import SettingItem from '../../../components/setting-item.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'FooterSettings'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
 | 
			
		||||
const layoutMode = computed(() => themeStore.layout.mode);
 | 
			
		||||
const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wrapper');
 | 
			
		||||
const isMixHorizontalMode = computed(() =>
 | 
			
		||||
  ['top-hybrid-sidebar-first', 'top-hybrid-header-first'].includes(layoutMode.value)
 | 
			
		||||
);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <NDivider>{{ $t('theme.layout.footer.title') }}</NDivider>
 | 
			
		||||
  <TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
 | 
			
		||||
    <SettingItem key="1" :label="$t('theme.layout.footer.visible')">
 | 
			
		||||
      <NSwitch v-model:value="themeStore.footer.visible" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem
 | 
			
		||||
      v-if="themeStore.footer.visible && isWrapperScrollMode"
 | 
			
		||||
      key="2"
 | 
			
		||||
      :label="$t('theme.layout.footer.fixed')"
 | 
			
		||||
    >
 | 
			
		||||
      <NSwitch v-model:value="themeStore.footer.fixed" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="themeStore.footer.visible" key="3" :label="$t('theme.layout.footer.height')">
 | 
			
		||||
      <NInputNumber v-model:value="themeStore.footer.height" size="small" :step="1" class="w-120px" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem
 | 
			
		||||
      v-if="themeStore.footer.visible && isMixHorizontalMode"
 | 
			
		||||
      key="4"
 | 
			
		||||
      :label="$t('theme.layout.footer.right')"
 | 
			
		||||
    >
 | 
			
		||||
      <NSwitch v-model:value="themeStore.footer.right" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
  </TransitionGroup>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.setting-list-move,
 | 
			
		||||
.setting-list-enter-active,
 | 
			
		||||
.setting-list-leave-active {
 | 
			
		||||
  --uno: transition-all-300;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.setting-list-enter-from,
 | 
			
		||||
.setting-list-leave-to {
 | 
			
		||||
  --uno: opacity-0 -translate-x-30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.setting-list-leave-active {
 | 
			
		||||
  --uno: absolute;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,47 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
import SettingItem from '../../../components/setting-item.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'HeaderSettings'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <NDivider>{{ $t('theme.layout.header.title') }}</NDivider>
 | 
			
		||||
  <TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
 | 
			
		||||
    <SettingItem key="1" :label="$t('theme.layout.header.height')">
 | 
			
		||||
      <NInputNumber v-model:value="themeStore.header.height" size="small" :step="1" class="w-120px" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem key="2" :label="$t('theme.layout.header.breadcrumb.visible')">
 | 
			
		||||
      <NSwitch v-model:value="themeStore.header.breadcrumb.visible" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem
 | 
			
		||||
      v-if="themeStore.header.breadcrumb.visible"
 | 
			
		||||
      key="3"
 | 
			
		||||
      :label="$t('theme.layout.header.breadcrumb.showIcon')"
 | 
			
		||||
    >
 | 
			
		||||
      <NSwitch v-model:value="themeStore.header.breadcrumb.showIcon" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
  </TransitionGroup>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.setting-list-move,
 | 
			
		||||
.setting-list-enter-active,
 | 
			
		||||
.setting-list-leave-active {
 | 
			
		||||
  --uno: transition-all-300;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.setting-list-enter-from,
 | 
			
		||||
.setting-list-leave-to {
 | 
			
		||||
  --uno: opacity-0 -translate-x-30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.setting-list-leave-active {
 | 
			
		||||
  --uno: absolute;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -2,8 +2,7 @@
 | 
			
		||||
import { useAppStore } from '@/store/modules/app';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
import LayoutModeCard from '../components/layout-mode-card.vue';
 | 
			
		||||
import SettingItem from '../components/setting-item.vue';
 | 
			
		||||
import LayoutModeCard from '../../../components/layout-mode-card.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'LayoutMode'
 | 
			
		||||
@@ -11,56 +10,60 @@ defineOptions({
 | 
			
		||||
 | 
			
		||||
const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
 | 
			
		||||
function handleReverseHorizontalMixChange(value: boolean) {
 | 
			
		||||
  themeStore.setLayoutReverseHorizontalMix(value);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <NDivider>{{ $t('theme.layoutMode.title') }}</NDivider>
 | 
			
		||||
  <NDivider>{{ $t('theme.layout.layoutMode.title') }}</NDivider>
 | 
			
		||||
  <LayoutModeCard v-model:mode="themeStore.layout.mode" :disabled="appStore.isMobile">
 | 
			
		||||
    <template #vertical>
 | 
			
		||||
      <div class="layout-sider h-full w-18px"></div>
 | 
			
		||||
      <div class="layout-sider h-full w-18px !bg-primary"></div>
 | 
			
		||||
      <div class="vertical-wrapper">
 | 
			
		||||
        <div class="layout-header"></div>
 | 
			
		||||
        <div class="layout-header bg-primary-200"></div>
 | 
			
		||||
        <div class="layout-main"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template #vertical-mix>
 | 
			
		||||
      <div class="layout-sider h-full w-8px"></div>
 | 
			
		||||
      <div class="layout-sider h-full w-16px"></div>
 | 
			
		||||
      <div class="layout-sider h-full w-8px !bg-primary"></div>
 | 
			
		||||
      <div class="layout-sider h-full w-16px !bg-primary-300"></div>
 | 
			
		||||
      <div class="vertical-wrapper">
 | 
			
		||||
        <div class="layout-header"></div>
 | 
			
		||||
        <div class="layout-header bg-primary-200"></div>
 | 
			
		||||
        <div class="layout-main"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template #vertical-hybrid-header-first>
 | 
			
		||||
      <div class="layout-sider h-full w-8px !bg-primary"></div>
 | 
			
		||||
      <div class="layout-sider h-full w-16px !bg-primary-300"></div>
 | 
			
		||||
      <div class="vertical-wrapper">
 | 
			
		||||
        <div class="layout-header bg-primary"></div>
 | 
			
		||||
        <div class="layout-main"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template #horizontal>
 | 
			
		||||
      <div class="layout-header"></div>
 | 
			
		||||
      <div class="layout-header !bg-primary"></div>
 | 
			
		||||
      <div class="horizontal-wrapper">
 | 
			
		||||
        <div class="layout-main"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template #horizontal-mix>
 | 
			
		||||
      <div class="layout-header"></div>
 | 
			
		||||
    <template #top-hybrid-sidebar-first>
 | 
			
		||||
      <div class="layout-header !bg-primary-300"></div>
 | 
			
		||||
      <div class="horizontal-wrapper">
 | 
			
		||||
        <div class="layout-sider w-18px !bg-primary"></div>
 | 
			
		||||
        <div class="layout-main"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template #top-hybrid-header-first>
 | 
			
		||||
      <div class="layout-header bg-primary"></div>
 | 
			
		||||
      <div class="horizontal-wrapper">
 | 
			
		||||
        <div class="layout-sider w-18px"></div>
 | 
			
		||||
        <div class="layout-main"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
  </LayoutModeCard>
 | 
			
		||||
  <SettingItem
 | 
			
		||||
    v-if="themeStore.layout.mode === 'horizontal-mix'"
 | 
			
		||||
    :label="$t('theme.layoutMode.reverseHorizontalMix')"
 | 
			
		||||
    class="mt-16px"
 | 
			
		||||
  >
 | 
			
		||||
    <NSwitch :value="themeStore.layout.reverseHorizontalMix" @update:value="handleReverseHorizontalMixChange" />
 | 
			
		||||
  </SettingItem>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.layout-header {
 | 
			
		||||
  --uno: h-16px bg-primary rd-4px;
 | 
			
		||||
  --uno: h-16px rd-4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.layout-sider {
 | 
			
		||||
@@ -0,0 +1,53 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
import SettingItem from '../../../components/setting-item.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'SiderSettings'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
 | 
			
		||||
const layoutMode = computed(() => themeStore.layout.mode);
 | 
			
		||||
const isMixLayoutMode = computed(() => layoutMode.value.includes('mix') || layoutMode.value.includes('hybrid'));
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <NDivider>{{ $t('theme.layout.sider.title') }}</NDivider>
 | 
			
		||||
  <TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
 | 
			
		||||
    <SettingItem v-if="layoutMode === 'vertical'" key="1" :label="$t('theme.layout.sider.width')">
 | 
			
		||||
      <NInputNumber v-model:value="themeStore.sider.width" size="small" :step="1" class="w-120px" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="layoutMode === 'vertical'" key="2" :label="$t('theme.layout.sider.collapsedWidth')">
 | 
			
		||||
      <NInputNumber v-model:value="themeStore.sider.collapsedWidth" size="small" :step="1" class="w-120px" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="isMixLayoutMode" key="3" :label="$t('theme.layout.sider.mixWidth')">
 | 
			
		||||
      <NInputNumber v-model:value="themeStore.sider.mixWidth" size="small" :step="1" class="w-120px" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="isMixLayoutMode" key="4" :label="$t('theme.layout.sider.mixCollapsedWidth')">
 | 
			
		||||
      <NInputNumber v-model:value="themeStore.sider.mixCollapsedWidth" size="small" :step="1" class="w-120px" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="layoutMode === 'vertical-mix'" key="5" :label="$t('theme.layout.sider.mixChildMenuWidth')">
 | 
			
		||||
      <NInputNumber v-model:value="themeStore.sider.mixChildMenuWidth" size="small" :step="1" class="w-120px" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
  </TransitionGroup>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.setting-list-move,
 | 
			
		||||
.setting-list-enter-active,
 | 
			
		||||
.setting-list-leave-active {
 | 
			
		||||
  --uno: transition-all-300;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.setting-list-enter-from,
 | 
			
		||||
.setting-list-leave-to {
 | 
			
		||||
  --uno: opacity-0 -translate-x-30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.setting-list-leave-active {
 | 
			
		||||
  --uno: absolute;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,62 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { themeTabModeOptions } from '@/constants/app';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { translateOptions } from '@/utils/common';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
import SettingItem from '../../../components/setting-item.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'TabSettings'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <NDivider>{{ $t('theme.layout.tab.title') }}</NDivider>
 | 
			
		||||
  <TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
 | 
			
		||||
    <SettingItem key="1" :label="$t('theme.layout.tab.visible')">
 | 
			
		||||
      <NSwitch v-model:value="themeStore.tab.visible" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="themeStore.tab.visible" key="2" :label="$t('theme.layout.tab.cache')">
 | 
			
		||||
      <template #suffix>
 | 
			
		||||
        <IconTooltip :desc="$t('theme.layout.tab.cacheTip')" />
 | 
			
		||||
      </template>
 | 
			
		||||
      <NSwitch v-model:value="themeStore.tab.cache" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="themeStore.tab.visible" key="3" :label="$t('theme.layout.tab.height')">
 | 
			
		||||
      <NInputNumber v-model:value="themeStore.tab.height" size="small" :step="1" class="w-120px" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="themeStore.tab.visible" key="4" :label="$t('theme.layout.tab.mode.title')">
 | 
			
		||||
      <NSelect
 | 
			
		||||
        v-model:value="themeStore.tab.mode"
 | 
			
		||||
        :options="translateOptions(themeTabModeOptions)"
 | 
			
		||||
        size="small"
 | 
			
		||||
        class="w-120px"
 | 
			
		||||
      />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="themeStore.tab.visible" key="5" :label="$t('theme.layout.tab.closeByMiddleClick')">
 | 
			
		||||
      <template #suffix>
 | 
			
		||||
        <IconTooltip :desc="$t('theme.layout.tab.closeByMiddleClickTip')" />
 | 
			
		||||
      </template>
 | 
			
		||||
      <NSwitch v-model:value="themeStore.tab.closeTabByMiddleClick" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
  </TransitionGroup>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.setting-list-move,
 | 
			
		||||
.setting-list-enter-active,
 | 
			
		||||
.setting-list-leave-active {
 | 
			
		||||
  --uno: transition-all-300;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.setting-list-enter-from,
 | 
			
		||||
.setting-list-leave-to {
 | 
			
		||||
  --uno: opacity-0 -translate-x-30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.setting-list-leave-active {
 | 
			
		||||
  --uno: absolute;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,157 +0,0 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import {
 | 
			
		||||
  resetCacheStrategyOptions,
 | 
			
		||||
  themePageAnimationModeOptions,
 | 
			
		||||
  themeScrollModeOptions,
 | 
			
		||||
  themeTabModeOptions
 | 
			
		||||
} from '@/constants/app';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { translateOptions } from '@/utils/common';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
import SettingItem from '../components/setting-item.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'PageFun'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
 | 
			
		||||
const layoutMode = computed(() => themeStore.layout.mode);
 | 
			
		||||
 | 
			
		||||
const isMixLayoutMode = computed(() => layoutMode.value.includes('mix'));
 | 
			
		||||
 | 
			
		||||
const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wrapper');
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <NDivider>{{ $t('theme.pageFunTitle') }}</NDivider>
 | 
			
		||||
  <TransitionGroup tag="div" name="setting-list" class="flex-col-stretch gap-12px">
 | 
			
		||||
    <SettingItem key="0" :label="$t('theme.resetCacheStrategy.title')">
 | 
			
		||||
      <NSelect
 | 
			
		||||
        v-model:value="themeStore.resetCacheStrategy"
 | 
			
		||||
        :options="translateOptions(resetCacheStrategyOptions)"
 | 
			
		||||
        size="small"
 | 
			
		||||
        class="w-120px"
 | 
			
		||||
      />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem key="1" :label="$t('theme.scrollMode.title')">
 | 
			
		||||
      <NSelect
 | 
			
		||||
        v-model:value="themeStore.layout.scrollMode"
 | 
			
		||||
        :options="translateOptions(themeScrollModeOptions)"
 | 
			
		||||
        size="small"
 | 
			
		||||
        class="w-120px"
 | 
			
		||||
      />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem key="1-1" :label="$t('theme.page.animate')">
 | 
			
		||||
      <NSwitch v-model:value="themeStore.page.animate" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="themeStore.page.animate" key="1-2" :label="$t('theme.page.mode.title')">
 | 
			
		||||
      <NSelect
 | 
			
		||||
        v-model:value="themeStore.page.animateMode"
 | 
			
		||||
        :options="translateOptions(themePageAnimationModeOptions)"
 | 
			
		||||
        size="small"
 | 
			
		||||
        class="w-120px"
 | 
			
		||||
      />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="isWrapperScrollMode" key="2" :label="$t('theme.fixedHeaderAndTab')">
 | 
			
		||||
      <NSwitch v-model:value="themeStore.fixedHeaderAndTab" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem key="3" :label="$t('theme.header.height')">
 | 
			
		||||
      <NInputNumber v-model:value="themeStore.header.height" size="small" :step="1" class="w-120px" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem key="4" :label="$t('theme.header.breadcrumb.visible')">
 | 
			
		||||
      <NSwitch v-model:value="themeStore.header.breadcrumb.visible" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="themeStore.header.breadcrumb.visible" key="4-1" :label="$t('theme.header.breadcrumb.showIcon')">
 | 
			
		||||
      <NSwitch v-model:value="themeStore.header.breadcrumb.showIcon" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem key="5" :label="$t('theme.tab.visible')">
 | 
			
		||||
      <NSwitch v-model:value="themeStore.tab.visible" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="themeStore.tab.visible" key="5-1" :label="$t('theme.tab.cache')">
 | 
			
		||||
      <NSwitch v-model:value="themeStore.tab.cache" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="themeStore.tab.visible" key="5-2" :label="$t('theme.tab.height')">
 | 
			
		||||
      <NInputNumber v-model:value="themeStore.tab.height" size="small" :step="1" class="w-120px" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="themeStore.tab.visible" key="5-3" :label="$t('theme.tab.mode.title')">
 | 
			
		||||
      <NSelect
 | 
			
		||||
        v-model:value="themeStore.tab.mode"
 | 
			
		||||
        :options="translateOptions(themeTabModeOptions)"
 | 
			
		||||
        size="small"
 | 
			
		||||
        class="w-120px"
 | 
			
		||||
      />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="layoutMode === 'vertical'" key="6-1" :label="$t('theme.sider.width')">
 | 
			
		||||
      <NInputNumber v-model:value="themeStore.sider.width" size="small" :step="1" class="w-120px" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="layoutMode === 'vertical'" key="6-2" :label="$t('theme.sider.collapsedWidth')">
 | 
			
		||||
      <NInputNumber v-model:value="themeStore.sider.collapsedWidth" size="small" :step="1" class="w-120px" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="isMixLayoutMode" key="6-3" :label="$t('theme.sider.mixWidth')">
 | 
			
		||||
      <NInputNumber v-model:value="themeStore.sider.mixWidth" size="small" :step="1" class="w-120px" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="isMixLayoutMode" key="6-4" :label="$t('theme.sider.mixCollapsedWidth')">
 | 
			
		||||
      <NInputNumber v-model:value="themeStore.sider.mixCollapsedWidth" size="small" :step="1" class="w-120px" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="layoutMode === 'vertical-mix'" key="6-5" :label="$t('theme.sider.mixChildMenuWidth')">
 | 
			
		||||
      <NInputNumber v-model:value="themeStore.sider.mixChildMenuWidth" size="small" :step="1" class="w-120px" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem key="7" :label="$t('theme.footer.visible')">
 | 
			
		||||
      <NSwitch v-model:value="themeStore.footer.visible" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="themeStore.footer.visible && isWrapperScrollMode" key="7-1" :label="$t('theme.footer.fixed')">
 | 
			
		||||
      <NSwitch v-model:value="themeStore.footer.fixed" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="themeStore.footer.visible" key="7-2" :label="$t('theme.footer.height')">
 | 
			
		||||
      <NInputNumber v-model:value="themeStore.footer.height" size="small" :step="1" class="w-120px" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem
 | 
			
		||||
      v-if="themeStore.footer.visible && layoutMode === 'horizontal-mix'"
 | 
			
		||||
      key="7-3"
 | 
			
		||||
      :label="$t('theme.footer.right')"
 | 
			
		||||
    >
 | 
			
		||||
      <NSwitch v-model:value="themeStore.footer.right" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem key="8" :label="$t('theme.watermark.visible')">
 | 
			
		||||
      <NSwitch v-model:value="themeStore.watermark.visible" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="themeStore.watermark.visible" key="8-1" :label="$t('theme.watermark.enableUserName')">
 | 
			
		||||
      <NSwitch v-model:value="themeStore.watermark.enableUserName" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem v-if="themeStore.watermark.visible" key="8-2" :label="$t('theme.watermark.text')">
 | 
			
		||||
      <NInput
 | 
			
		||||
        v-model:value="themeStore.watermark.text"
 | 
			
		||||
        autosize
 | 
			
		||||
        type="text"
 | 
			
		||||
        size="small"
 | 
			
		||||
        class="w-120px"
 | 
			
		||||
        placeholder="SoybeanAdmin"
 | 
			
		||||
      />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem key="9" :label="$t('theme.header.multilingual.visible')">
 | 
			
		||||
      <NSwitch v-model:value="themeStore.header.multilingual.visible" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
    <SettingItem key="10" :label="$t('theme.header.globalSearch.visible')">
 | 
			
		||||
      <NSwitch v-model:value="themeStore.header.globalSearch.visible" />
 | 
			
		||||
    </SettingItem>
 | 
			
		||||
  </TransitionGroup>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.setting-list-move,
 | 
			
		||||
.setting-list-enter-active,
 | 
			
		||||
.setting-list-leave-active {
 | 
			
		||||
  --uno: transition-all-300;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.setting-list-enter-from,
 | 
			
		||||
.setting-list-leave-to {
 | 
			
		||||
  --uno: opacity-0 -translate-x-30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.setting-list-leave-active {
 | 
			
		||||
  --uno: absolute;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										15
									
								
								src/layouts/modules/theme-drawer/modules/preset/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/layouts/modules/theme-drawer/modules/preset/index.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import ThemePreset from './modules/theme-preset.vue';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'PresetSettings'
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="flex-col-stretch gap-16px">
 | 
			
		||||
    <ThemePreset />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped></style>
 | 
			
		||||
@@ -0,0 +1,148 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'ThemePreset'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
type ThemePreset = Pick<
 | 
			
		||||
  App.Theme.ThemeSetting,
 | 
			
		||||
  | 'themeScheme'
 | 
			
		||||
  | 'grayscale'
 | 
			
		||||
  | 'colourWeakness'
 | 
			
		||||
  | 'recommendColor'
 | 
			
		||||
  | 'themeColor'
 | 
			
		||||
  | 'themeRadius'
 | 
			
		||||
  | 'otherColor'
 | 
			
		||||
  | 'isInfoFollowPrimary'
 | 
			
		||||
  | 'layout'
 | 
			
		||||
  | 'page'
 | 
			
		||||
  | 'header'
 | 
			
		||||
  | 'tab'
 | 
			
		||||
  | 'fixedHeaderAndTab'
 | 
			
		||||
  | 'sider'
 | 
			
		||||
  | 'footer'
 | 
			
		||||
  | 'watermark'
 | 
			
		||||
  | 'tokens'
 | 
			
		||||
> & {
 | 
			
		||||
  name: string;
 | 
			
		||||
  desc: string;
 | 
			
		||||
  i18nkey?: string;
 | 
			
		||||
  version: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const presetModules = import.meta.glob('@/theme/preset/*.json', { eager: true, import: 'default' });
 | 
			
		||||
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
 | 
			
		||||
// Extract preset data
 | 
			
		||||
const presets = computed(() =>
 | 
			
		||||
  Object.entries(presetModules)
 | 
			
		||||
    .map(([path, presetData]) => {
 | 
			
		||||
      const fileName = path.split('/').pop()?.replace('.json', '') || '';
 | 
			
		||||
      return {
 | 
			
		||||
        id: fileName,
 | 
			
		||||
        ...(presetData as ThemePreset)
 | 
			
		||||
      };
 | 
			
		||||
    })
 | 
			
		||||
    .sort((a, b) => {
 | 
			
		||||
      if (a.name === 'default') return -1;
 | 
			
		||||
      if (b.name === 'default') return 1;
 | 
			
		||||
      return a.name.localeCompare(b.name);
 | 
			
		||||
    })
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const getPresetName = (preset: ThemePreset): string => {
 | 
			
		||||
  if (!preset.i18nkey) return preset.name;
 | 
			
		||||
  try {
 | 
			
		||||
    const key = `${preset.i18nkey}.name` as App.I18n.I18nKey;
 | 
			
		||||
    const translated = $t(key);
 | 
			
		||||
    return translated !== key ? translated : preset.name;
 | 
			
		||||
  } catch {
 | 
			
		||||
    return preset.name;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getPresetDesc = (preset: ThemePreset): string => {
 | 
			
		||||
  if (!preset.i18nkey) return preset.desc;
 | 
			
		||||
  try {
 | 
			
		||||
    const key = `${preset.i18nkey}.desc` as App.I18n.I18nKey;
 | 
			
		||||
    const translated = $t(key);
 | 
			
		||||
    return translated !== key ? translated : preset.desc;
 | 
			
		||||
  } catch {
 | 
			
		||||
    return preset.desc;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const applyPreset = ({ themeScheme, grayscale, colourWeakness, layout, watermark, ...rest }: ThemePreset): void => {
 | 
			
		||||
  themeStore.setThemeScheme(themeScheme);
 | 
			
		||||
  themeStore.setGrayscale(grayscale);
 | 
			
		||||
  themeStore.setColourWeakness(colourWeakness);
 | 
			
		||||
  themeStore.setThemeLayout(layout.mode);
 | 
			
		||||
  themeStore.setWatermarkEnableUserName(watermark.enableUserName);
 | 
			
		||||
  themeStore.setWatermarkEnableTime(watermark.enableTime);
 | 
			
		||||
 | 
			
		||||
  Object.assign(themeStore, {
 | 
			
		||||
    ...rest,
 | 
			
		||||
    layout: { ...themeStore.layout, scrollMode: layout.scrollMode },
 | 
			
		||||
    page: { ...rest.page },
 | 
			
		||||
    header: { ...rest.header },
 | 
			
		||||
    tab: { ...rest.tab },
 | 
			
		||||
    sider: { ...rest.sider },
 | 
			
		||||
    footer: { ...rest.footer },
 | 
			
		||||
    watermark: { ...watermark },
 | 
			
		||||
    tokens: { ...rest.tokens }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  window.$message?.success($t('theme.appearance.preset.applySuccess'));
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <NDivider>{{ $t('theme.appearance.preset.title') }}</NDivider>
 | 
			
		||||
 | 
			
		||||
  <div class="flex flex-col gap-3">
 | 
			
		||||
    <div
 | 
			
		||||
      v-for="preset in presets"
 | 
			
		||||
      :key="preset.id"
 | 
			
		||||
      class="border border-primary/10 rounded-lg border-solid bg-white/5 p-3 backdrop-blur-10 transition-all duration-300 hover:(shadow-md -translate-y-0.5)"
 | 
			
		||||
    >
 | 
			
		||||
      <div class="mb-2 flex items-center justify-between">
 | 
			
		||||
        <div class="min-w-0 w-full flex flex-1 items-center justify-between gap-2">
 | 
			
		||||
          <h5 class="m-0 truncate text-sm text-primary font-600">
 | 
			
		||||
            {{ getPresetName(preset) }}
 | 
			
		||||
          </h5>
 | 
			
		||||
          <NBadge :value="`v${preset.version}`" type="info" size="small" class="flex-shrink-0 opacity-80" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <NButton type="primary" size="tiny" ghost round class="ml-2 flex-shrink-0" @click="applyPreset(preset)">
 | 
			
		||||
          {{ $t('theme.appearance.preset.apply') }}
 | 
			
		||||
        </NButton>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <p class="line-clamp-2 mb-3 text-xs text-gray-500 leading-4">{{ getPresetDesc(preset) }}</p>
 | 
			
		||||
 | 
			
		||||
      <div class="flex items-center justify-between">
 | 
			
		||||
        <div class="flex gap-1">
 | 
			
		||||
          <div
 | 
			
		||||
            v-for="(color, key) in { primary: preset.themeColor, ...preset.otherColor }"
 | 
			
		||||
            :key="key"
 | 
			
		||||
            class="h-3 w-3 cursor-pointer border border-white/30 rounded-full transition-transform hover:scale-110"
 | 
			
		||||
            :style="{ backgroundColor: color }"
 | 
			
		||||
            :class="{ 'ring-1 ring-primary/50': key === 'primary' }"
 | 
			
		||||
            :title="key"
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="flex items-center gap-1">
 | 
			
		||||
          <div class="text-lg">
 | 
			
		||||
            {{ preset.themeScheme === 'dark' ? '🌙' : '☀️' }}
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="text-lg">
 | 
			
		||||
            {{ preset.grayscale ? '🎨' : '' }}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -58,101 +58,160 @@ const local: App.I18n.Schema = {
 | 
			
		||||
    tokenExpired: 'The requested token has expired'
 | 
			
		||||
  },
 | 
			
		||||
  theme: {
 | 
			
		||||
    themeSchema: {
 | 
			
		||||
      title: 'Theme Schema',
 | 
			
		||||
      light: 'Light',
 | 
			
		||||
      dark: 'Dark',
 | 
			
		||||
      auto: 'Follow System'
 | 
			
		||||
    themeDrawerTitle: 'Theme Configuration',
 | 
			
		||||
    tabs: {
 | 
			
		||||
      appearance: 'Appearance',
 | 
			
		||||
      layout: 'Layout',
 | 
			
		||||
      general: 'General',
 | 
			
		||||
      preset: 'Preset'
 | 
			
		||||
    },
 | 
			
		||||
    grayscale: 'Grayscale',
 | 
			
		||||
    colourWeakness: 'Colour Weakness',
 | 
			
		||||
    layoutMode: {
 | 
			
		||||
      title: 'Layout Mode',
 | 
			
		||||
      vertical: 'Vertical Menu Mode',
 | 
			
		||||
      horizontal: 'Horizontal Menu Mode',
 | 
			
		||||
      'vertical-mix': 'Vertical Mix Menu Mode',
 | 
			
		||||
      'horizontal-mix': 'Horizontal Mix menu Mode',
 | 
			
		||||
      reverseHorizontalMix: 'Reverse first level menus and child level menus position'
 | 
			
		||||
    },
 | 
			
		||||
    recommendColor: 'Apply Recommended Color Algorithm',
 | 
			
		||||
    recommendColorDesc: 'The recommended color algorithm refers to',
 | 
			
		||||
    themeColor: {
 | 
			
		||||
      title: 'Theme Color',
 | 
			
		||||
      primary: 'Primary',
 | 
			
		||||
      info: 'Info',
 | 
			
		||||
      success: 'Success',
 | 
			
		||||
      warning: 'Warning',
 | 
			
		||||
      error: 'Error',
 | 
			
		||||
      followPrimary: 'Follow Primary'
 | 
			
		||||
    },
 | 
			
		||||
    scrollMode: {
 | 
			
		||||
      title: 'Scroll Mode',
 | 
			
		||||
      wrapper: 'Wrapper',
 | 
			
		||||
      content: 'Content'
 | 
			
		||||
    },
 | 
			
		||||
    page: {
 | 
			
		||||
      animate: 'Page Animate',
 | 
			
		||||
      mode: {
 | 
			
		||||
        title: 'Page Animate Mode',
 | 
			
		||||
        fade: 'Fade',
 | 
			
		||||
        'fade-slide': 'Slide',
 | 
			
		||||
        'fade-bottom': 'Fade Zoom',
 | 
			
		||||
        'fade-scale': 'Fade Scale',
 | 
			
		||||
        'zoom-fade': 'Zoom Fade',
 | 
			
		||||
        'zoom-out': 'Zoom Out',
 | 
			
		||||
        none: 'None'
 | 
			
		||||
    appearance: {
 | 
			
		||||
      themeSchema: {
 | 
			
		||||
        title: 'Theme Schema',
 | 
			
		||||
        light: 'Light',
 | 
			
		||||
        dark: 'Dark',
 | 
			
		||||
        auto: 'Follow System'
 | 
			
		||||
      },
 | 
			
		||||
      grayscale: 'Grayscale',
 | 
			
		||||
      colourWeakness: 'Colour Weakness',
 | 
			
		||||
      themeColor: {
 | 
			
		||||
        title: 'Theme Color',
 | 
			
		||||
        primary: 'Primary',
 | 
			
		||||
        info: 'Info',
 | 
			
		||||
        success: 'Success',
 | 
			
		||||
        warning: 'Warning',
 | 
			
		||||
        error: 'Error',
 | 
			
		||||
        followPrimary: 'Follow Primary'
 | 
			
		||||
      },
 | 
			
		||||
      themeRadius: {
 | 
			
		||||
        title: 'Theme Radius'
 | 
			
		||||
      },
 | 
			
		||||
      recommendColor: 'Apply Recommended Color Algorithm',
 | 
			
		||||
      recommendColorDesc: 'The recommended color algorithm refers to',
 | 
			
		||||
      preset: {
 | 
			
		||||
        title: 'Theme Presets',
 | 
			
		||||
        apply: 'Apply',
 | 
			
		||||
        applySuccess: 'Preset applied successfully',
 | 
			
		||||
        default: {
 | 
			
		||||
          name: 'Default Preset',
 | 
			
		||||
          desc: 'Default theme preset with balanced settings'
 | 
			
		||||
        },
 | 
			
		||||
        dark: {
 | 
			
		||||
          name: 'Dark Preset',
 | 
			
		||||
          desc: 'Dark theme preset for night time usage'
 | 
			
		||||
        },
 | 
			
		||||
        compact: {
 | 
			
		||||
          name: 'Compact Preset',
 | 
			
		||||
          desc: 'Compact layout preset for small screens'
 | 
			
		||||
        },
 | 
			
		||||
        azir: {
 | 
			
		||||
          name: "Azir's Preset",
 | 
			
		||||
          desc: 'It is a cold and elegant preset that Azir likes'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    fixedHeaderAndTab: 'Fixed Header And Tab',
 | 
			
		||||
    header: {
 | 
			
		||||
      height: 'Header Height',
 | 
			
		||||
      breadcrumb: {
 | 
			
		||||
        visible: 'Breadcrumb Visible',
 | 
			
		||||
        showIcon: 'Breadcrumb Icon Visible'
 | 
			
		||||
    layout: {
 | 
			
		||||
      layoutMode: {
 | 
			
		||||
        title: 'Layout Mode',
 | 
			
		||||
        vertical: 'Vertical Mode',
 | 
			
		||||
        horizontal: 'Horizontal Mode',
 | 
			
		||||
        'vertical-mix': 'Vertical Mix Mode',
 | 
			
		||||
        'vertical-hybrid-header-first': 'Left Hybrid Header-First',
 | 
			
		||||
        'top-hybrid-sidebar-first': 'Top-Hybrid Sidebar-First',
 | 
			
		||||
        'top-hybrid-header-first': 'Top-Hybrid Header-First',
 | 
			
		||||
        vertical_detail: 'Vertical menu layout, with the menu on the left and content on the right.',
 | 
			
		||||
        'vertical-mix_detail':
 | 
			
		||||
          'Vertical mix-menu layout, with the primary menu on the dark left side and the secondary menu on the lighter left side.',
 | 
			
		||||
        'vertical-hybrid-header-first_detail':
 | 
			
		||||
          'Left hybrid layout, with the primary menu at the top, the secondary menu on the dark left side, and the tertiary menu on the lighter left side.',
 | 
			
		||||
        horizontal_detail: 'Horizontal menu layout, with the menu at the top and content below.',
 | 
			
		||||
        'top-hybrid-sidebar-first_detail':
 | 
			
		||||
          'Top hybrid layout, with the primary menu on the left and the secondary menu at the top.',
 | 
			
		||||
        'top-hybrid-header-first_detail':
 | 
			
		||||
          'Top hybrid layout, with the primary menu at the top and the secondary menu on the left.'
 | 
			
		||||
      },
 | 
			
		||||
      tab: {
 | 
			
		||||
        title: 'Tab Settings',
 | 
			
		||||
        visible: 'Tab Visible',
 | 
			
		||||
        cache: 'Tag Bar Info Cache',
 | 
			
		||||
        cacheTip: 'One-click to open/close global keepalive',
 | 
			
		||||
        height: 'Tab Height',
 | 
			
		||||
        mode: {
 | 
			
		||||
          title: 'Tab Mode',
 | 
			
		||||
          slider: 'Slider',
 | 
			
		||||
          chrome: 'Chrome',
 | 
			
		||||
          button: 'Button'
 | 
			
		||||
        },
 | 
			
		||||
        closeByMiddleClick: 'Close Tab by Middle Click',
 | 
			
		||||
        closeByMiddleClickTip: 'Enable closing tabs by clicking with the middle mouse button'
 | 
			
		||||
      },
 | 
			
		||||
      header: {
 | 
			
		||||
        title: 'Header Settings',
 | 
			
		||||
        height: 'Header Height',
 | 
			
		||||
        breadcrumb: {
 | 
			
		||||
          visible: 'Breadcrumb Visible',
 | 
			
		||||
          showIcon: 'Breadcrumb Icon Visible'
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      sider: {
 | 
			
		||||
        title: 'Sider Settings',
 | 
			
		||||
        inverted: 'Dark Sider',
 | 
			
		||||
        width: 'Sider Width',
 | 
			
		||||
        collapsedWidth: 'Sider Collapsed Width',
 | 
			
		||||
        mixWidth: 'Mix Sider Width',
 | 
			
		||||
        mixCollapsedWidth: 'Mix Sider Collapse Width',
 | 
			
		||||
        mixChildMenuWidth: 'Mix Child Menu Width'
 | 
			
		||||
      },
 | 
			
		||||
      footer: {
 | 
			
		||||
        title: 'Footer Settings',
 | 
			
		||||
        visible: 'Footer Visible',
 | 
			
		||||
        fixed: 'Fixed Footer',
 | 
			
		||||
        height: 'Footer Height',
 | 
			
		||||
        right: 'Right Footer'
 | 
			
		||||
      },
 | 
			
		||||
      content: {
 | 
			
		||||
        title: 'Content Area Settings',
 | 
			
		||||
        scrollMode: {
 | 
			
		||||
          title: 'Scroll Mode',
 | 
			
		||||
          tip: 'The theme scroll only scrolls the main part, the outer scroll can carry the header and footer together',
 | 
			
		||||
          wrapper: 'Wrapper',
 | 
			
		||||
          content: 'Content'
 | 
			
		||||
        },
 | 
			
		||||
        page: {
 | 
			
		||||
          animate: 'Page Animate',
 | 
			
		||||
          mode: {
 | 
			
		||||
            title: 'Page Animate Mode',
 | 
			
		||||
            fade: 'Fade',
 | 
			
		||||
            'fade-slide': 'Slide',
 | 
			
		||||
            'fade-bottom': 'Fade Zoom',
 | 
			
		||||
            'fade-scale': 'Fade Scale',
 | 
			
		||||
            'zoom-fade': 'Zoom Fade',
 | 
			
		||||
            'zoom-out': 'Zoom Out',
 | 
			
		||||
            none: 'None'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        fixedHeaderAndTab: 'Fixed Header And Tab'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    general: {
 | 
			
		||||
      title: 'General Settings',
 | 
			
		||||
      watermark: {
 | 
			
		||||
        title: 'Watermark Settings',
 | 
			
		||||
        visible: 'Watermark Full Screen Visible',
 | 
			
		||||
        text: 'Custom Watermark Text',
 | 
			
		||||
        enableUserName: 'Enable User Name Watermark',
 | 
			
		||||
        enableTime: 'Show Current Time',
 | 
			
		||||
        timeFormat: 'Time Format'
 | 
			
		||||
      },
 | 
			
		||||
      multilingual: {
 | 
			
		||||
        title: 'Multilingual Settings',
 | 
			
		||||
        visible: 'Display multilingual button'
 | 
			
		||||
      },
 | 
			
		||||
      globalSearch: {
 | 
			
		||||
        title: 'Global Search Settings',
 | 
			
		||||
        visible: 'Display GlobalSearch button'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    tab: {
 | 
			
		||||
      visible: 'Tab Visible',
 | 
			
		||||
      cache: 'Tag Bar Info Cache',
 | 
			
		||||
      height: 'Tab Height',
 | 
			
		||||
      mode: {
 | 
			
		||||
        title: 'Tab Mode',
 | 
			
		||||
        chrome: 'Chrome',
 | 
			
		||||
        button: 'Button'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    sider: {
 | 
			
		||||
      inverted: 'Dark Sider',
 | 
			
		||||
      width: 'Sider Width',
 | 
			
		||||
      collapsedWidth: 'Sider Collapsed Width',
 | 
			
		||||
      mixWidth: 'Mix Sider Width',
 | 
			
		||||
      mixCollapsedWidth: 'Mix Sider Collapse Width',
 | 
			
		||||
      mixChildMenuWidth: 'Mix Child Menu Width'
 | 
			
		||||
    },
 | 
			
		||||
    footer: {
 | 
			
		||||
      visible: 'Footer Visible',
 | 
			
		||||
      fixed: 'Fixed Footer',
 | 
			
		||||
      height: 'Footer Height',
 | 
			
		||||
      right: 'Right Footer'
 | 
			
		||||
    },
 | 
			
		||||
    watermark: {
 | 
			
		||||
      visible: 'Watermark Full Screen Visible',
 | 
			
		||||
      text: 'Watermark Text',
 | 
			
		||||
      enableUserName: 'Enable User Name Watermark'
 | 
			
		||||
    },
 | 
			
		||||
    themeDrawerTitle: 'Theme Configuration',
 | 
			
		||||
    pageFunTitle: 'Page Function',
 | 
			
		||||
    resetCacheStrategy: {
 | 
			
		||||
      title: 'Reset Cache Strategy',
 | 
			
		||||
      close: 'Close Page',
 | 
			
		||||
      refresh: 'Refresh Page'
 | 
			
		||||
    },
 | 
			
		||||
    configOperation: {
 | 
			
		||||
      copyConfig: 'Copy Config',
 | 
			
		||||
      copySuccessMsg: 'Copy Success, Please replace the variable "themeSettings" in "src/theme/settings.ts"',
 | 
			
		||||
 
 | 
			
		||||
@@ -58,101 +58,157 @@ const local: App.I18n.Schema = {
 | 
			
		||||
    tokenExpired: 'token已过期'
 | 
			
		||||
  },
 | 
			
		||||
  theme: {
 | 
			
		||||
    themeSchema: {
 | 
			
		||||
      title: '主题模式',
 | 
			
		||||
      light: '亮色模式',
 | 
			
		||||
      dark: '暗黑模式',
 | 
			
		||||
      auto: '跟随系统'
 | 
			
		||||
    themeDrawerTitle: '主题配置',
 | 
			
		||||
    tabs: {
 | 
			
		||||
      appearance: '外观',
 | 
			
		||||
      layout: '布局',
 | 
			
		||||
      general: '通用',
 | 
			
		||||
      preset: '预设'
 | 
			
		||||
    },
 | 
			
		||||
    grayscale: '灰色模式',
 | 
			
		||||
    colourWeakness: '色弱模式',
 | 
			
		||||
    layoutMode: {
 | 
			
		||||
      title: '布局模式',
 | 
			
		||||
      vertical: '左侧菜单模式',
 | 
			
		||||
      'vertical-mix': '左侧菜单混合模式',
 | 
			
		||||
      horizontal: '顶部菜单模式',
 | 
			
		||||
      'horizontal-mix': '顶部菜单混合模式',
 | 
			
		||||
      reverseHorizontalMix: '一级菜单与子级菜单位置反转'
 | 
			
		||||
    },
 | 
			
		||||
    recommendColor: '应用推荐算法的颜色',
 | 
			
		||||
    recommendColorDesc: '推荐颜色的算法参照',
 | 
			
		||||
    themeColor: {
 | 
			
		||||
      title: '主题颜色',
 | 
			
		||||
      primary: '主色',
 | 
			
		||||
      info: '信息色',
 | 
			
		||||
      success: '成功色',
 | 
			
		||||
      warning: '警告色',
 | 
			
		||||
      error: '错误色',
 | 
			
		||||
      followPrimary: '跟随主色'
 | 
			
		||||
    },
 | 
			
		||||
    scrollMode: {
 | 
			
		||||
      title: '滚动模式',
 | 
			
		||||
      wrapper: '外层滚动',
 | 
			
		||||
      content: '主体滚动'
 | 
			
		||||
    },
 | 
			
		||||
    page: {
 | 
			
		||||
      animate: '页面切换动画',
 | 
			
		||||
      mode: {
 | 
			
		||||
        title: '页面切换动画类型',
 | 
			
		||||
        'fade-slide': '滑动',
 | 
			
		||||
        fade: '淡入淡出',
 | 
			
		||||
        'fade-bottom': '底部消退',
 | 
			
		||||
        'fade-scale': '缩放消退',
 | 
			
		||||
        'zoom-fade': '渐变',
 | 
			
		||||
        'zoom-out': '闪现',
 | 
			
		||||
        none: '无'
 | 
			
		||||
    appearance: {
 | 
			
		||||
      themeSchema: {
 | 
			
		||||
        title: '主题模式',
 | 
			
		||||
        light: '亮色模式',
 | 
			
		||||
        dark: '暗黑模式',
 | 
			
		||||
        auto: '跟随系统'
 | 
			
		||||
      },
 | 
			
		||||
      grayscale: '灰色模式',
 | 
			
		||||
      colourWeakness: '色弱模式',
 | 
			
		||||
      themeColor: {
 | 
			
		||||
        title: '主题颜色',
 | 
			
		||||
        primary: '主色',
 | 
			
		||||
        info: '信息色',
 | 
			
		||||
        success: '成功色',
 | 
			
		||||
        warning: '警告色',
 | 
			
		||||
        error: '错误色',
 | 
			
		||||
        followPrimary: '跟随主色'
 | 
			
		||||
      },
 | 
			
		||||
      themeRadius: {
 | 
			
		||||
        title: '主题圆角'
 | 
			
		||||
      },
 | 
			
		||||
      recommendColor: '应用推荐算法的颜色',
 | 
			
		||||
      recommendColorDesc: '推荐颜色的算法参照',
 | 
			
		||||
      preset: {
 | 
			
		||||
        title: '主题预设',
 | 
			
		||||
        apply: '应用',
 | 
			
		||||
        applySuccess: '预设应用成功',
 | 
			
		||||
        default: {
 | 
			
		||||
          name: '默认预设',
 | 
			
		||||
          desc: 'Soybean 默认主题预设'
 | 
			
		||||
        },
 | 
			
		||||
        dark: {
 | 
			
		||||
          name: '暗色预设',
 | 
			
		||||
          desc: '适用于夜间使用的暗色主题预设'
 | 
			
		||||
        },
 | 
			
		||||
        compact: {
 | 
			
		||||
          name: '紧凑型',
 | 
			
		||||
          desc: '适用于小屏幕的紧凑布局预设'
 | 
			
		||||
        },
 | 
			
		||||
        azir: {
 | 
			
		||||
          name: 'Azir的预设',
 | 
			
		||||
          desc: '是 Azir 比较喜欢的莫兰迪色系冷淡风'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    fixedHeaderAndTab: '固定头部和标签栏',
 | 
			
		||||
    header: {
 | 
			
		||||
      height: '头部高度',
 | 
			
		||||
      breadcrumb: {
 | 
			
		||||
        visible: '显示面包屑',
 | 
			
		||||
        showIcon: '显示面包屑图标'
 | 
			
		||||
    layout: {
 | 
			
		||||
      layoutMode: {
 | 
			
		||||
        title: '布局模式',
 | 
			
		||||
        vertical: '左侧菜单模式',
 | 
			
		||||
        'vertical-mix': '左侧菜单混合模式',
 | 
			
		||||
        'vertical-hybrid-header-first': '左侧混合-顶部优先',
 | 
			
		||||
        horizontal: '顶部菜单模式',
 | 
			
		||||
        'top-hybrid-sidebar-first': '顶部混合-侧边优先',
 | 
			
		||||
        'top-hybrid-header-first': '顶部混合-顶部优先',
 | 
			
		||||
        vertical_detail: '左侧菜单布局,菜单在左,内容在右。',
 | 
			
		||||
        'vertical-mix_detail': '左侧双菜单布局,一级菜单在左侧深色区域,二级菜单在左侧浅色区域。',
 | 
			
		||||
        'vertical-hybrid-header-first_detail':
 | 
			
		||||
          '左侧混合布局,一级菜单在顶部,二级菜单在左侧深色区域,三级菜单在左侧浅色区域。',
 | 
			
		||||
        horizontal_detail: '顶部菜单布局,菜单在顶部,内容在下方。',
 | 
			
		||||
        'top-hybrid-sidebar-first_detail': '顶部混合布局,一级菜单在左侧,二级菜单在顶部。',
 | 
			
		||||
        'top-hybrid-header-first_detail': '顶部混合布局,一级菜单在顶部,二级菜单在左侧。'
 | 
			
		||||
      },
 | 
			
		||||
      tab: {
 | 
			
		||||
        title: '标签栏设置',
 | 
			
		||||
        visible: '显示标签栏',
 | 
			
		||||
        cache: '标签栏信息缓存',
 | 
			
		||||
        cacheTip: '一键开启/关闭全局 keepalive',
 | 
			
		||||
        height: '标签栏高度',
 | 
			
		||||
        mode: {
 | 
			
		||||
          title: '标签栏风格',
 | 
			
		||||
          slider: '滑块风格',
 | 
			
		||||
          chrome: '谷歌风格',
 | 
			
		||||
          button: '按钮风格'
 | 
			
		||||
        },
 | 
			
		||||
        closeByMiddleClick: '鼠标中键关闭标签页',
 | 
			
		||||
        closeByMiddleClickTip: '启用后可以使用鼠标中键点击标签页进行关闭'
 | 
			
		||||
      },
 | 
			
		||||
      header: {
 | 
			
		||||
        title: '头部设置',
 | 
			
		||||
        height: '头部高度',
 | 
			
		||||
        breadcrumb: {
 | 
			
		||||
          visible: '显示面包屑',
 | 
			
		||||
          showIcon: '显示面包屑图标'
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      sider: {
 | 
			
		||||
        title: '侧边栏设置',
 | 
			
		||||
        inverted: '深色侧边栏',
 | 
			
		||||
        width: '侧边栏宽度',
 | 
			
		||||
        collapsedWidth: '侧边栏折叠宽度',
 | 
			
		||||
        mixWidth: '混合布局侧边栏宽度',
 | 
			
		||||
        mixCollapsedWidth: '混合布局侧边栏折叠宽度',
 | 
			
		||||
        mixChildMenuWidth: '混合布局子菜单宽度'
 | 
			
		||||
      },
 | 
			
		||||
      footer: {
 | 
			
		||||
        title: '底部设置',
 | 
			
		||||
        visible: '显示底部',
 | 
			
		||||
        fixed: '固定底部',
 | 
			
		||||
        height: '底部高度',
 | 
			
		||||
        right: '底部居右'
 | 
			
		||||
      },
 | 
			
		||||
      content: {
 | 
			
		||||
        title: '内容区域设置',
 | 
			
		||||
        scrollMode: {
 | 
			
		||||
          title: '滚动模式',
 | 
			
		||||
          tip: '主题滚动仅 main 部分滚动,外层滚动可携带头部底部一起滚动',
 | 
			
		||||
          wrapper: '外层滚动',
 | 
			
		||||
          content: '主体滚动'
 | 
			
		||||
        },
 | 
			
		||||
        page: {
 | 
			
		||||
          animate: '页面切换动画',
 | 
			
		||||
          mode: {
 | 
			
		||||
            title: '页面切换动画类型',
 | 
			
		||||
            'fade-slide': '滑动',
 | 
			
		||||
            fade: '淡入淡出',
 | 
			
		||||
            'fade-bottom': '底部消退',
 | 
			
		||||
            'fade-scale': '缩放消退',
 | 
			
		||||
            'zoom-fade': '渐变',
 | 
			
		||||
            'zoom-out': '闪现',
 | 
			
		||||
            none: '无'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        fixedHeaderAndTab: '固定头部和标签栏'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    general: {
 | 
			
		||||
      title: '通用设置',
 | 
			
		||||
      watermark: {
 | 
			
		||||
        title: '水印设置',
 | 
			
		||||
        visible: '显示全屏水印',
 | 
			
		||||
        text: '自定义水印文本',
 | 
			
		||||
        enableUserName: '启用用户名水印',
 | 
			
		||||
        enableTime: '显示当前时间',
 | 
			
		||||
        timeFormat: '时间格式'
 | 
			
		||||
      },
 | 
			
		||||
      multilingual: {
 | 
			
		||||
        title: '多语言设置',
 | 
			
		||||
        visible: '显示多语言按钮'
 | 
			
		||||
      },
 | 
			
		||||
      globalSearch: {
 | 
			
		||||
        title: '全局搜索设置',
 | 
			
		||||
        visible: '显示全局搜索按钮'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    tab: {
 | 
			
		||||
      visible: '显示标签栏',
 | 
			
		||||
      cache: '标签栏信息缓存',
 | 
			
		||||
      height: '标签栏高度',
 | 
			
		||||
      mode: {
 | 
			
		||||
        title: '标签栏风格',
 | 
			
		||||
        chrome: '谷歌风格',
 | 
			
		||||
        button: '按钮风格'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    sider: {
 | 
			
		||||
      inverted: '深色侧边栏',
 | 
			
		||||
      width: '侧边栏宽度',
 | 
			
		||||
      collapsedWidth: '侧边栏折叠宽度',
 | 
			
		||||
      mixWidth: '混合布局侧边栏宽度',
 | 
			
		||||
      mixCollapsedWidth: '混合布局侧边栏折叠宽度',
 | 
			
		||||
      mixChildMenuWidth: '混合布局子菜单宽度'
 | 
			
		||||
    },
 | 
			
		||||
    footer: {
 | 
			
		||||
      visible: '显示底部',
 | 
			
		||||
      fixed: '固定底部',
 | 
			
		||||
      height: '底部高度',
 | 
			
		||||
      right: '底部局右'
 | 
			
		||||
    },
 | 
			
		||||
    watermark: {
 | 
			
		||||
      visible: '显示全屏水印',
 | 
			
		||||
      text: '水印文本',
 | 
			
		||||
      enableUserName: '启用用户名水印'
 | 
			
		||||
    },
 | 
			
		||||
    themeDrawerTitle: '主题配置',
 | 
			
		||||
    pageFunTitle: '页面功能',
 | 
			
		||||
    resetCacheStrategy: {
 | 
			
		||||
      title: '重置缓存策略',
 | 
			
		||||
      close: '关闭页面',
 | 
			
		||||
      refresh: '刷新页面'
 | 
			
		||||
    },
 | 
			
		||||
    configOperation: {
 | 
			
		||||
      copyConfig: '复制配置',
 | 
			
		||||
      copySuccessMsg: '复制成功,请替换 src/theme/settings.ts 中的变量 themeSettings',
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ import type { RequestInstanceState } from './type';
 | 
			
		||||
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
 | 
			
		||||
const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
 | 
			
		||||
 | 
			
		||||
export const request = createFlatRequest<App.Service.Response, RequestInstanceState>(
 | 
			
		||||
export const request = createFlatRequest(
 | 
			
		||||
  {
 | 
			
		||||
    baseURL,
 | 
			
		||||
    headers: {
 | 
			
		||||
@@ -18,6 +18,13 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    defaultState: {
 | 
			
		||||
      errMsgStack: [],
 | 
			
		||||
      refreshTokenPromise: null
 | 
			
		||||
    } as RequestInstanceState,
 | 
			
		||||
    transform(response: AxiosResponse<App.Service.Response<any>>) {
 | 
			
		||||
      return response.data.data;
 | 
			
		||||
    },
 | 
			
		||||
    async onRequest(config) {
 | 
			
		||||
      const Authorization = getAuthorization();
 | 
			
		||||
      Object.assign(config.headers, { Authorization });
 | 
			
		||||
@@ -91,9 +98,6 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
 | 
			
		||||
 | 
			
		||||
      return null;
 | 
			
		||||
    },
 | 
			
		||||
    transformBackendResponse(response) {
 | 
			
		||||
      return response.data.data;
 | 
			
		||||
    },
 | 
			
		||||
    onError(error) {
 | 
			
		||||
      // when the request is fail, you can show error message
 | 
			
		||||
 | 
			
		||||
@@ -123,11 +127,14 @@ export const request = createFlatRequest<App.Service.Response, RequestInstanceSt
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const demoRequest = createRequest<App.Service.DemoResponse>(
 | 
			
		||||
export const demoRequest = createRequest(
 | 
			
		||||
  {
 | 
			
		||||
    baseURL: otherBaseURL.demo
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    transform(response: AxiosResponse<App.Service.DemoResponse>) {
 | 
			
		||||
      return response.data.result;
 | 
			
		||||
    },
 | 
			
		||||
    async onRequest(config) {
 | 
			
		||||
      const { headers } = config;
 | 
			
		||||
 | 
			
		||||
@@ -147,9 +154,6 @@ export const demoRequest = createRequest<App.Service.DemoResponse>(
 | 
			
		||||
      // when the backend response code is not "200", it means the request is fail
 | 
			
		||||
      // for example: the token is expired, refresh token and retry request
 | 
			
		||||
    },
 | 
			
		||||
    transformBackendResponse(response) {
 | 
			
		||||
      return response.data.result;
 | 
			
		||||
    },
 | 
			
		||||
    onError(error) {
 | 
			
		||||
      // when the request is fail, you can show error message
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -28,14 +28,14 @@ async function handleRefreshToken() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function handleExpiredRequest(state: RequestInstanceState) {
 | 
			
		||||
  if (!state.refreshTokenFn) {
 | 
			
		||||
    state.refreshTokenFn = handleRefreshToken();
 | 
			
		||||
  if (!state.refreshTokenPromise) {
 | 
			
		||||
    state.refreshTokenPromise = handleRefreshToken();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const success = await state.refreshTokenFn;
 | 
			
		||||
  const success = await state.refreshTokenPromise;
 | 
			
		||||
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
    state.refreshTokenFn = null;
 | 
			
		||||
    state.refreshTokenPromise = null;
 | 
			
		||||
  }, 1000);
 | 
			
		||||
 | 
			
		||||
  return success;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
export interface RequestInstanceState {
 | 
			
		||||
  /** whether the request is refreshing token */
 | 
			
		||||
  refreshTokenFn: Promise<boolean> | null;
 | 
			
		||||
  /** the promise of refreshing token */
 | 
			
		||||
  refreshTokenPromise: Promise<boolean> | null;
 | 
			
		||||
  /** the request error message stack */
 | 
			
		||||
  errMsgStack: string[];
 | 
			
		||||
  [key: string]: unknown;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -46,10 +46,7 @@ export const useAppStore = defineStore(SetupStoreId.App, () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    setReloadFlag(true);
 | 
			
		||||
 | 
			
		||||
    if (themeStore.resetCacheStrategy === 'refresh') {
 | 
			
		||||
      routeStore.resetRouteCache();
 | 
			
		||||
    }
 | 
			
		||||
    routeStore.resetRouteCache();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const locale = ref<App.I18n.LangType>(localStg.get('lang') || 'zh-CN');
 | 
			
		||||
 
 | 
			
		||||
@@ -318,7 +318,7 @@ export const useRouteStore = defineStore(SetupStoreId.Route, () => {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function onRouteSwitchWhenLoggedIn() {
 | 
			
		||||
    await authStore.initUserInfo();
 | 
			
		||||
    // some global init logic when logged in and switch route
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function onRouteSwitchWhenNotLoggedIn() {
 | 
			
		||||
 
 | 
			
		||||
@@ -112,10 +112,8 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
 | 
			
		||||
      await switchRouteByTab(nextTab);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // reset route cache if cache strategy is close
 | 
			
		||||
    if (themeStore.resetCacheStrategy === 'close') {
 | 
			
		||||
      routeStore.resetRouteCache(removedTabRouteKey);
 | 
			
		||||
    }
 | 
			
		||||
    // reset route cache
 | 
			
		||||
    routeStore.resetRouteCache(removedTabRouteKey);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** remove active tab */
 | 
			
		||||
@@ -147,10 +145,8 @@ export const useTabStore = defineStore(SetupStoreId.Tab, () => {
 | 
			
		||||
    const tabsToRemove = tabs.value.filter(tab => !remainTabIds.includes(tab.id));
 | 
			
		||||
    const routeKeysToReset: RouteKey[] = [];
 | 
			
		||||
 | 
			
		||||
    if (themeStore.resetCacheStrategy === 'close') {
 | 
			
		||||
      for (const tab of tabsToRemove) {
 | 
			
		||||
        routeKeysToReset.push(tab.routeKey);
 | 
			
		||||
      }
 | 
			
		||||
    for (const tab of tabsToRemove) {
 | 
			
		||||
      routeKeysToReset.push(tab.routeKey);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const removedTabsIds = tabsToRemove.map(tab => tab.id);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
import { computed, effectScope, onScopeDispose, ref, toRefs, watch } from 'vue';
 | 
			
		||||
import type { Ref } from 'vue';
 | 
			
		||||
import { useEventListener, usePreferredColorScheme } from '@vueuse/core';
 | 
			
		||||
import { useDateFormat, useEventListener, useNow, usePreferredColorScheme } from '@vueuse/core';
 | 
			
		||||
import { defineStore } from 'pinia';
 | 
			
		||||
import { getPaletteColorByNumber } from '@sa/color';
 | 
			
		||||
import { localStg } from '@/utils/storage';
 | 
			
		||||
import { SetupStoreId } from '@/enum';
 | 
			
		||||
import { useAuthStore } from '../auth';
 | 
			
		||||
import {
 | 
			
		||||
  addThemeVarsToGlobal,
 | 
			
		||||
  createThemeToken,
 | 
			
		||||
@@ -18,10 +19,14 @@ import {
 | 
			
		||||
export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
 | 
			
		||||
  const scope = effectScope();
 | 
			
		||||
  const osTheme = usePreferredColorScheme();
 | 
			
		||||
  const authStore = useAuthStore();
 | 
			
		||||
 | 
			
		||||
  /** Theme settings */
 | 
			
		||||
  const settings: Ref<App.Theme.ThemeSetting> = ref(initThemeSettings());
 | 
			
		||||
 | 
			
		||||
  /** Watermark time instance with controls */
 | 
			
		||||
  const { now: watermarkTime, pause: pauseWatermarkTime, resume: resumeWatermarkTime } = useNow({ controls: true });
 | 
			
		||||
 | 
			
		||||
  /** Dark mode */
 | 
			
		||||
  const darkMode = computed(() => {
 | 
			
		||||
    if (settings.value.themeScheme === 'auto') {
 | 
			
		||||
@@ -48,7 +53,7 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  /** Naive theme */
 | 
			
		||||
  const naiveTheme = computed(() => getNaiveTheme(themeColors.value, settings.value.recommendColor));
 | 
			
		||||
  const naiveTheme = computed(() => getNaiveTheme(themeColors.value, settings.value));
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Settings json
 | 
			
		||||
@@ -57,6 +62,28 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
 | 
			
		||||
   */
 | 
			
		||||
  const settingsJson = computed(() => JSON.stringify(settings.value));
 | 
			
		||||
 | 
			
		||||
  /** Watermark time date formatter */
 | 
			
		||||
  const formattedWatermarkTime = computed(() => {
 | 
			
		||||
    const { watermark } = settings.value;
 | 
			
		||||
    const date = useDateFormat(watermarkTime, watermark.timeFormat);
 | 
			
		||||
    return date.value;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  /** Watermark content */
 | 
			
		||||
  const watermarkContent = computed(() => {
 | 
			
		||||
    const { watermark } = settings.value;
 | 
			
		||||
 | 
			
		||||
    if (watermark.enableUserName && authStore.userInfo.userName) {
 | 
			
		||||
      return authStore.userInfo.userName;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (watermark.enableTime) {
 | 
			
		||||
      return formattedWatermarkTime.value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return watermark.text;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  /** Reset store */
 | 
			
		||||
  function resetStore() {
 | 
			
		||||
    const themeStore = useThemeStore();
 | 
			
		||||
@@ -144,13 +171,43 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
 | 
			
		||||
    );
 | 
			
		||||
    addThemeVarsToGlobal(themeTokens, darkThemeTokens);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Set layout reverse horizontal mix
 | 
			
		||||
   * Set watermark enable user name
 | 
			
		||||
   *
 | 
			
		||||
   * @param reverse Reverse horizontal mix
 | 
			
		||||
   * @param enable Whether to enable user name watermark
 | 
			
		||||
   */
 | 
			
		||||
  function setLayoutReverseHorizontalMix(reverse: boolean) {
 | 
			
		||||
    settings.value.layout.reverseHorizontalMix = reverse;
 | 
			
		||||
  function setWatermarkEnableUserName(enable: boolean) {
 | 
			
		||||
    settings.value.watermark.enableUserName = enable;
 | 
			
		||||
 | 
			
		||||
    if (enable) {
 | 
			
		||||
      settings.value.watermark.enableTime = false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Set watermark enable time
 | 
			
		||||
   *
 | 
			
		||||
   * @param enable Whether to enable time watermark
 | 
			
		||||
   */
 | 
			
		||||
  function setWatermarkEnableTime(enable: boolean) {
 | 
			
		||||
    settings.value.watermark.enableTime = enable;
 | 
			
		||||
 | 
			
		||||
    if (enable) {
 | 
			
		||||
      settings.value.watermark.enableUserName = false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Only run timer when watermark is visible and time display is enabled */
 | 
			
		||||
  function updateWatermarkTimer() {
 | 
			
		||||
    const { watermark } = settings.value;
 | 
			
		||||
    const shouldRunTimer = watermark.visible && watermark.enableTime;
 | 
			
		||||
 | 
			
		||||
    if (shouldRunTimer) {
 | 
			
		||||
      resumeWatermarkTime();
 | 
			
		||||
    } else {
 | 
			
		||||
      pauseWatermarkTime();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Cache theme settings */
 | 
			
		||||
@@ -196,6 +253,15 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
 | 
			
		||||
      },
 | 
			
		||||
      { immediate: true }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // watch watermark settings to control timer
 | 
			
		||||
    watch(
 | 
			
		||||
      () => [settings.value.watermark.visible, settings.value.watermark.enableTime],
 | 
			
		||||
      () => {
 | 
			
		||||
        updateWatermarkTimer();
 | 
			
		||||
      },
 | 
			
		||||
      { immediate: true }
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  /** On scope dispose */
 | 
			
		||||
@@ -209,6 +275,7 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
 | 
			
		||||
    themeColors,
 | 
			
		||||
    naiveTheme,
 | 
			
		||||
    settingsJson,
 | 
			
		||||
    watermarkContent,
 | 
			
		||||
    setGrayscale,
 | 
			
		||||
    setColourWeakness,
 | 
			
		||||
    resetStore,
 | 
			
		||||
@@ -216,6 +283,7 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
 | 
			
		||||
    toggleThemeScheme,
 | 
			
		||||
    updateThemeColors,
 | 
			
		||||
    setThemeLayout,
 | 
			
		||||
    setLayoutReverseHorizontalMix
 | 
			
		||||
    setWatermarkEnableUserName,
 | 
			
		||||
    setWatermarkEnableTime
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -236,22 +236,23 @@ function getNaiveThemeColors(colors: App.Theme.ThemeColor, recommended = false)
 | 
			
		||||
/**
 | 
			
		||||
 * Get naive theme
 | 
			
		||||
 *
 | 
			
		||||
 * @param colors Theme colors
 | 
			
		||||
 * @param [recommended=false] Use recommended color. Default is `false`
 | 
			
		||||
 * @param settings Theme settings object.
 | 
			
		||||
 * @param settings.recommendColor Whether to use recommended color palette.
 | 
			
		||||
 * @param settings.themeRadius Border radius to use in the theme (in px).
 | 
			
		||||
 */
 | 
			
		||||
export function getNaiveTheme(colors: App.Theme.ThemeColor, recommended = false) {
 | 
			
		||||
export function getNaiveTheme(colors: App.Theme.ThemeColor, settings: App.Theme.ThemeSetting) {
 | 
			
		||||
  const { primary: colorLoading } = colors;
 | 
			
		||||
 | 
			
		||||
  const theme: GlobalThemeOverrides = {
 | 
			
		||||
    common: {
 | 
			
		||||
      ...getNaiveThemeColors(colors, recommended),
 | 
			
		||||
      borderRadius: '6px'
 | 
			
		||||
      ...getNaiveThemeColors(colors, settings.recommendColor),
 | 
			
		||||
      borderRadius: `${settings.themeRadius}px`
 | 
			
		||||
    },
 | 
			
		||||
    LoadingBar: {
 | 
			
		||||
      colorLoading
 | 
			
		||||
    },
 | 
			
		||||
    Tag: {
 | 
			
		||||
      borderRadius: '6px'
 | 
			
		||||
      borderRadius: `${settings.themeRadius}px`
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ html,
 | 
			
		||||
body,
 | 
			
		||||
#app {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  text-autospace: normal;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,14 @@
 | 
			
		||||
@mixin scrollbar($size: 7px, $color: rgba(0, 0, 0, 0.5)) {
 | 
			
		||||
@mixin scrollbar($size: 7px, $color: rgba(0, 0, 0, 0.5), $dark-color: rgba(255, 255, 255, 0.5)) {
 | 
			
		||||
  scrollbar-width: thin;
 | 
			
		||||
  scrollbar-color: $color transparent;
 | 
			
		||||
 | 
			
		||||
  // Dark theme override
 | 
			
		||||
  .dark & {
 | 
			
		||||
    // guide safari use light color scrollbar
 | 
			
		||||
    color-scheme: dark;
 | 
			
		||||
    scrollbar-color: $dark-color transparent;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &::-webkit-scrollbar-thumb {
 | 
			
		||||
    background-color: $color;
 | 
			
		||||
    border-radius: $size;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										90
									
								
								src/theme/preset/azir.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/theme/preset/azir.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,90 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "Azir's Preset",
 | 
			
		||||
  "desc": "It is a cold and elegant preset that Azir likes",
 | 
			
		||||
  "i18nkey": "theme.appearance.preset.azir",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "themeScheme": "light",
 | 
			
		||||
  "grayscale": false,
 | 
			
		||||
  "colourWeakness": false,
 | 
			
		||||
  "recommendColor": true,
 | 
			
		||||
  "themeColor": "#78a878",
 | 
			
		||||
  "otherColor": {
 | 
			
		||||
    "info": "#89b989",
 | 
			
		||||
    "success": "#99c299",
 | 
			
		||||
    "warning": "#d4bb9d",
 | 
			
		||||
    "error": "#c49a9a"
 | 
			
		||||
  },
 | 
			
		||||
  "themeRadius": 6,
 | 
			
		||||
  "isInfoFollowPrimary": true,
 | 
			
		||||
  "layout": {
 | 
			
		||||
    "mode": "vertical-mix",
 | 
			
		||||
    "scrollMode": "wrapper"
 | 
			
		||||
  },
 | 
			
		||||
  "page": {
 | 
			
		||||
    "animate": true,
 | 
			
		||||
    "animateMode": "zoom-fade"
 | 
			
		||||
  },
 | 
			
		||||
  "header": {
 | 
			
		||||
    "height": 64,
 | 
			
		||||
    "breadcrumb": {
 | 
			
		||||
      "visible": true,
 | 
			
		||||
      "showIcon": true
 | 
			
		||||
    },
 | 
			
		||||
    "multilingual": {
 | 
			
		||||
      "visible": true
 | 
			
		||||
    },
 | 
			
		||||
    "globalSearch": {
 | 
			
		||||
      "visible": true
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "tab": {
 | 
			
		||||
    "visible": true,
 | 
			
		||||
    "cache": true,
 | 
			
		||||
    "height": 48,
 | 
			
		||||
    "mode": "chrome"
 | 
			
		||||
  },
 | 
			
		||||
  "fixedHeaderAndTab": true,
 | 
			
		||||
  "sider": {
 | 
			
		||||
    "inverted": false,
 | 
			
		||||
    "width": 220,
 | 
			
		||||
    "collapsedWidth": 64,
 | 
			
		||||
    "mixWidth": 90,
 | 
			
		||||
    "mixCollapsedWidth": 64,
 | 
			
		||||
    "mixChildMenuWidth": 200
 | 
			
		||||
  },
 | 
			
		||||
  "footer": {
 | 
			
		||||
    "visible": true,
 | 
			
		||||
    "fixed": true,
 | 
			
		||||
    "height": 56,
 | 
			
		||||
    "right": true
 | 
			
		||||
  },
 | 
			
		||||
  "watermark": {
 | 
			
		||||
    "visible": false,
 | 
			
		||||
    "text": "SoybeanAdmin",
 | 
			
		||||
    "enableUserName": false,
 | 
			
		||||
    "enableTime": true,
 | 
			
		||||
    "timeFormat": "YYYY-MM-DD HH:mm:ss"
 | 
			
		||||
  },
 | 
			
		||||
  "tokens": {
 | 
			
		||||
    "light": {
 | 
			
		||||
      "colors": {
 | 
			
		||||
        "container": "rgb(255, 255, 255)",
 | 
			
		||||
        "layout": "rgb(247, 250, 252)",
 | 
			
		||||
        "inverted": "rgb(0, 20, 40)",
 | 
			
		||||
        "base-text": "rgb(31, 31, 31)"
 | 
			
		||||
      },
 | 
			
		||||
      "boxShadow": {
 | 
			
		||||
        "header": "0 1px 2px rgb(0, 21, 41, 0.08)",
 | 
			
		||||
        "sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
 | 
			
		||||
        "tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "dark": {
 | 
			
		||||
      "colors": {
 | 
			
		||||
        "container": "rgb(28, 28, 28)",
 | 
			
		||||
        "layout": "rgb(18, 18, 18)",
 | 
			
		||||
        "base-text": "rgb(224, 224, 224)"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										90
									
								
								src/theme/preset/compact.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/theme/preset/compact.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,90 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "Compact Preset",
 | 
			
		||||
  "desc": "Compact layout preset for small screens",
 | 
			
		||||
  "i18nkey": "theme.appearance.preset.compact",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "themeScheme": "light",
 | 
			
		||||
  "grayscale": false,
 | 
			
		||||
  "colourWeakness": false,
 | 
			
		||||
  "recommendColor": false,
 | 
			
		||||
  "themeColor": "#646cff",
 | 
			
		||||
  "otherColor": {
 | 
			
		||||
    "info": "#2080f0",
 | 
			
		||||
    "success": "#52c41a",
 | 
			
		||||
    "warning": "#faad14",
 | 
			
		||||
    "error": "#f5222d"
 | 
			
		||||
  },
 | 
			
		||||
  "themeRadius": 6,
 | 
			
		||||
  "isInfoFollowPrimary": true,
 | 
			
		||||
  "layout": {
 | 
			
		||||
    "mode": "vertical",
 | 
			
		||||
    "scrollMode": "content"
 | 
			
		||||
  },
 | 
			
		||||
  "page": {
 | 
			
		||||
    "animate": true,
 | 
			
		||||
    "animateMode": "fade-slide"
 | 
			
		||||
  },
 | 
			
		||||
  "header": {
 | 
			
		||||
    "height": 48,
 | 
			
		||||
    "breadcrumb": {
 | 
			
		||||
      "visible": true,
 | 
			
		||||
      "showIcon": true
 | 
			
		||||
    },
 | 
			
		||||
    "multilingual": {
 | 
			
		||||
      "visible": false
 | 
			
		||||
    },
 | 
			
		||||
    "globalSearch": {
 | 
			
		||||
      "visible": false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "tab": {
 | 
			
		||||
    "visible": true,
 | 
			
		||||
    "cache": true,
 | 
			
		||||
    "height": 36,
 | 
			
		||||
    "mode": "button"
 | 
			
		||||
  },
 | 
			
		||||
  "fixedHeaderAndTab": true,
 | 
			
		||||
  "sider": {
 | 
			
		||||
    "inverted": false,
 | 
			
		||||
    "width": 180,
 | 
			
		||||
    "collapsedWidth": 48,
 | 
			
		||||
    "mixWidth": 80,
 | 
			
		||||
    "mixCollapsedWidth": 48,
 | 
			
		||||
    "mixChildMenuWidth": 180
 | 
			
		||||
  },
 | 
			
		||||
  "footer": {
 | 
			
		||||
    "visible": false,
 | 
			
		||||
    "fixed": false,
 | 
			
		||||
    "height": 40,
 | 
			
		||||
    "right": true
 | 
			
		||||
  },
 | 
			
		||||
  "watermark": {
 | 
			
		||||
    "visible": false,
 | 
			
		||||
    "text": "SoybeanAdmin",
 | 
			
		||||
    "enableUserName": false,
 | 
			
		||||
    "enableTime": false,
 | 
			
		||||
    "timeFormat": "YYYY-MM-DD HH:mm"
 | 
			
		||||
  },
 | 
			
		||||
  "tokens": {
 | 
			
		||||
    "light": {
 | 
			
		||||
      "colors": {
 | 
			
		||||
        "container": "rgb(255, 255, 255)",
 | 
			
		||||
        "layout": "rgb(247, 250, 252)",
 | 
			
		||||
        "inverted": "rgb(0, 20, 40)",
 | 
			
		||||
        "base-text": "rgb(31, 31, 31)"
 | 
			
		||||
      },
 | 
			
		||||
      "boxShadow": {
 | 
			
		||||
        "header": "0 1px 2px rgb(0, 21, 41, 0.08)",
 | 
			
		||||
        "sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
 | 
			
		||||
        "tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "dark": {
 | 
			
		||||
      "colors": {
 | 
			
		||||
        "container": "rgb(28, 28, 28)",
 | 
			
		||||
        "layout": "rgb(18, 18, 18)",
 | 
			
		||||
        "base-text": "rgb(224, 224, 224)"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										90
									
								
								src/theme/preset/dark.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/theme/preset/dark.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,90 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "Dark Preset",
 | 
			
		||||
  "desc": "Dark theme preset for night time usage",
 | 
			
		||||
  "i18nkey": "theme.appearance.preset.dark",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "themeScheme": "dark",
 | 
			
		||||
  "grayscale": false,
 | 
			
		||||
  "colourWeakness": false,
 | 
			
		||||
  "recommendColor": false,
 | 
			
		||||
  "themeColor": "#409eff",
 | 
			
		||||
  "otherColor": {
 | 
			
		||||
    "info": "#2080f0",
 | 
			
		||||
    "success": "#52c41a",
 | 
			
		||||
    "warning": "#faad14",
 | 
			
		||||
    "error": "#f5222d"
 | 
			
		||||
  },
 | 
			
		||||
  "themeRadius": 6,
 | 
			
		||||
  "isInfoFollowPrimary": true,
 | 
			
		||||
  "layout": {
 | 
			
		||||
    "mode": "vertical",
 | 
			
		||||
    "scrollMode": "content"
 | 
			
		||||
  },
 | 
			
		||||
  "page": {
 | 
			
		||||
    "animate": true,
 | 
			
		||||
    "animateMode": "fade-slide"
 | 
			
		||||
  },
 | 
			
		||||
  "header": {
 | 
			
		||||
    "height": 56,
 | 
			
		||||
    "breadcrumb": {
 | 
			
		||||
      "visible": true,
 | 
			
		||||
      "showIcon": true
 | 
			
		||||
    },
 | 
			
		||||
    "multilingual": {
 | 
			
		||||
      "visible": true
 | 
			
		||||
    },
 | 
			
		||||
    "globalSearch": {
 | 
			
		||||
      "visible": true
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "tab": {
 | 
			
		||||
    "visible": true,
 | 
			
		||||
    "cache": true,
 | 
			
		||||
    "height": 44,
 | 
			
		||||
    "mode": "chrome"
 | 
			
		||||
  },
 | 
			
		||||
  "fixedHeaderAndTab": true,
 | 
			
		||||
  "sider": {
 | 
			
		||||
    "inverted": true,
 | 
			
		||||
    "width": 220,
 | 
			
		||||
    "collapsedWidth": 64,
 | 
			
		||||
    "mixWidth": 90,
 | 
			
		||||
    "mixCollapsedWidth": 64,
 | 
			
		||||
    "mixChildMenuWidth": 200
 | 
			
		||||
  },
 | 
			
		||||
  "footer": {
 | 
			
		||||
    "visible": true,
 | 
			
		||||
    "fixed": false,
 | 
			
		||||
    "height": 48,
 | 
			
		||||
    "right": true
 | 
			
		||||
  },
 | 
			
		||||
  "watermark": {
 | 
			
		||||
    "visible": false,
 | 
			
		||||
    "text": "SoybeanAdmin",
 | 
			
		||||
    "enableUserName": false,
 | 
			
		||||
    "enableTime": false,
 | 
			
		||||
    "timeFormat": "YYYY-MM-DD HH:mm"
 | 
			
		||||
  },
 | 
			
		||||
  "tokens": {
 | 
			
		||||
    "light": {
 | 
			
		||||
      "colors": {
 | 
			
		||||
        "container": "rgb(255, 255, 255)",
 | 
			
		||||
        "layout": "rgb(247, 250, 252)",
 | 
			
		||||
        "inverted": "rgb(0, 20, 40)",
 | 
			
		||||
        "base-text": "rgb(31, 31, 31)"
 | 
			
		||||
      },
 | 
			
		||||
      "boxShadow": {
 | 
			
		||||
        "header": "0 1px 2px rgb(0, 21, 41, 0.08)",
 | 
			
		||||
        "sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
 | 
			
		||||
        "tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "dark": {
 | 
			
		||||
      "colors": {
 | 
			
		||||
        "container": "rgb(28, 28, 28)",
 | 
			
		||||
        "layout": "rgb(18, 18, 18)",
 | 
			
		||||
        "base-text": "rgb(224, 224, 224)"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										90
									
								
								src/theme/preset/default.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/theme/preset/default.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,90 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "default",
 | 
			
		||||
  "desc": "Default theme preset with balanced settings",
 | 
			
		||||
  "i18nkey": "theme.appearance.preset.default",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "themeScheme": "light",
 | 
			
		||||
  "grayscale": false,
 | 
			
		||||
  "colourWeakness": false,
 | 
			
		||||
  "recommendColor": false,
 | 
			
		||||
  "themeColor": "#646cff",
 | 
			
		||||
  "otherColor": {
 | 
			
		||||
    "info": "#2080f0",
 | 
			
		||||
    "success": "#52c41a",
 | 
			
		||||
    "warning": "#faad14",
 | 
			
		||||
    "error": "#f5222d"
 | 
			
		||||
  },
 | 
			
		||||
  "themeRadius": 6,
 | 
			
		||||
  "isInfoFollowPrimary": true,
 | 
			
		||||
  "layout": {
 | 
			
		||||
    "mode": "vertical",
 | 
			
		||||
    "scrollMode": "content"
 | 
			
		||||
  },
 | 
			
		||||
  "page": {
 | 
			
		||||
    "animate": true,
 | 
			
		||||
    "animateMode": "fade-slide"
 | 
			
		||||
  },
 | 
			
		||||
  "header": {
 | 
			
		||||
    "height": 56,
 | 
			
		||||
    "breadcrumb": {
 | 
			
		||||
      "visible": true,
 | 
			
		||||
      "showIcon": true
 | 
			
		||||
    },
 | 
			
		||||
    "multilingual": {
 | 
			
		||||
      "visible": true
 | 
			
		||||
    },
 | 
			
		||||
    "globalSearch": {
 | 
			
		||||
      "visible": true
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "tab": {
 | 
			
		||||
    "visible": true,
 | 
			
		||||
    "cache": true,
 | 
			
		||||
    "height": 44,
 | 
			
		||||
    "mode": "chrome"
 | 
			
		||||
  },
 | 
			
		||||
  "fixedHeaderAndTab": true,
 | 
			
		||||
  "sider": {
 | 
			
		||||
    "inverted": false,
 | 
			
		||||
    "width": 220,
 | 
			
		||||
    "collapsedWidth": 64,
 | 
			
		||||
    "mixWidth": 90,
 | 
			
		||||
    "mixCollapsedWidth": 64,
 | 
			
		||||
    "mixChildMenuWidth": 200
 | 
			
		||||
  },
 | 
			
		||||
  "footer": {
 | 
			
		||||
    "visible": true,
 | 
			
		||||
    "fixed": false,
 | 
			
		||||
    "height": 48,
 | 
			
		||||
    "right": true
 | 
			
		||||
  },
 | 
			
		||||
  "watermark": {
 | 
			
		||||
    "visible": false,
 | 
			
		||||
    "text": "SoybeanAdmin",
 | 
			
		||||
    "enableUserName": false,
 | 
			
		||||
    "enableTime": false,
 | 
			
		||||
    "timeFormat": "YYYY-MM-DD HH:mm"
 | 
			
		||||
  },
 | 
			
		||||
  "tokens": {
 | 
			
		||||
    "light": {
 | 
			
		||||
      "colors": {
 | 
			
		||||
        "container": "rgb(255, 255, 255)",
 | 
			
		||||
        "layout": "rgb(247, 250, 252)",
 | 
			
		||||
        "inverted": "rgb(0, 20, 40)",
 | 
			
		||||
        "base-text": "rgb(31, 31, 31)"
 | 
			
		||||
      },
 | 
			
		||||
      "boxShadow": {
 | 
			
		||||
        "header": "0 1px 2px rgb(0, 21, 41, 0.08)",
 | 
			
		||||
        "sider": "2px 0 8px 0 rgb(29, 35, 41, 0.05)",
 | 
			
		||||
        "tab": "0 1px 2px rgb(0, 21, 41, 0.08)"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "dark": {
 | 
			
		||||
      "colors": {
 | 
			
		||||
        "container": "rgb(28, 28, 28)",
 | 
			
		||||
        "layout": "rgb(18, 18, 18)",
 | 
			
		||||
        "base-text": "rgb(224, 224, 224)"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -5,6 +5,7 @@ export const themeSettings: App.Theme.ThemeSetting = {
 | 
			
		||||
  colourWeakness: false,
 | 
			
		||||
  recommendColor: false,
 | 
			
		||||
  themeColor: '#646cff',
 | 
			
		||||
  themeRadius: 6,
 | 
			
		||||
  otherColor: {
 | 
			
		||||
    info: '#2080f0',
 | 
			
		||||
    success: '#52c41a',
 | 
			
		||||
@@ -12,11 +13,9 @@ export const themeSettings: App.Theme.ThemeSetting = {
 | 
			
		||||
    error: '#f5222d'
 | 
			
		||||
  },
 | 
			
		||||
  isInfoFollowPrimary: true,
 | 
			
		||||
  resetCacheStrategy: 'close',
 | 
			
		||||
  layout: {
 | 
			
		||||
    mode: 'vertical',
 | 
			
		||||
    scrollMode: 'content',
 | 
			
		||||
    reverseHorizontalMix: false
 | 
			
		||||
    scrollMode: 'content'
 | 
			
		||||
  },
 | 
			
		||||
  page: {
 | 
			
		||||
    animate: true,
 | 
			
		||||
@@ -39,7 +38,8 @@ export const themeSettings: App.Theme.ThemeSetting = {
 | 
			
		||||
    visible: true,
 | 
			
		||||
    cache: true,
 | 
			
		||||
    height: 44,
 | 
			
		||||
    mode: 'chrome'
 | 
			
		||||
    mode: 'chrome',
 | 
			
		||||
    closeTabByMiddleClick: false
 | 
			
		||||
  },
 | 
			
		||||
  fixedHeaderAndTab: true,
 | 
			
		||||
  sider: {
 | 
			
		||||
@@ -59,7 +59,9 @@ export const themeSettings: App.Theme.ThemeSetting = {
 | 
			
		||||
  watermark: {
 | 
			
		||||
    visible: false,
 | 
			
		||||
    text: 'SoybeanAdmin',
 | 
			
		||||
    enableUserName: false
 | 
			
		||||
    enableUserName: false,
 | 
			
		||||
    enableTime: false,
 | 
			
		||||
    timeFormat: 'YYYY-MM-DD HH:mm'
 | 
			
		||||
  },
 | 
			
		||||
  tokens: {
 | 
			
		||||
    light: {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								src/typings/api/auth.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/typings/api/auth.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
declare namespace Api {
 | 
			
		||||
  /**
 | 
			
		||||
   * namespace Auth
 | 
			
		||||
   *
 | 
			
		||||
   * backend api module: "auth"
 | 
			
		||||
   */
 | 
			
		||||
  namespace Auth {
 | 
			
		||||
    interface LoginToken {
 | 
			
		||||
      token: string;
 | 
			
		||||
      refreshToken: string;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    interface UserInfo {
 | 
			
		||||
      userId: string;
 | 
			
		||||
      userName: string;
 | 
			
		||||
      roles: string[];
 | 
			
		||||
      buttons: string[];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -47,41 +47,4 @@ declare namespace Api {
 | 
			
		||||
      status: EnableStatus | null;
 | 
			
		||||
    } & T;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * namespace Auth
 | 
			
		||||
   *
 | 
			
		||||
   * backend api module: "auth"
 | 
			
		||||
   */
 | 
			
		||||
  namespace Auth {
 | 
			
		||||
    interface LoginToken {
 | 
			
		||||
      token: string;
 | 
			
		||||
      refreshToken: string;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    interface UserInfo {
 | 
			
		||||
      userId: string;
 | 
			
		||||
      userName: string;
 | 
			
		||||
      roles: string[];
 | 
			
		||||
      buttons: string[];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * namespace Route
 | 
			
		||||
   *
 | 
			
		||||
   * backend api module: "route"
 | 
			
		||||
   */
 | 
			
		||||
  namespace Route {
 | 
			
		||||
    type ElegantConstRoute = import('@elegant-router/types').ElegantConstRoute;
 | 
			
		||||
 | 
			
		||||
    interface MenuRoute extends ElegantConstRoute {
 | 
			
		||||
      id: string;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    interface UserRoute {
 | 
			
		||||
      routes: MenuRoute[];
 | 
			
		||||
      home: import('@elegant-router/types').LastLevelRouteKey;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								src/typings/api/route.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/typings/api/route.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
declare namespace Api {
 | 
			
		||||
  /**
 | 
			
		||||
   * namespace Route
 | 
			
		||||
   *
 | 
			
		||||
   * backend api module: "route"
 | 
			
		||||
   */
 | 
			
		||||
  namespace Route {
 | 
			
		||||
    type ElegantConstRoute = import('@elegant-router/types').ElegantConstRoute;
 | 
			
		||||
 | 
			
		||||
    interface MenuRoute extends ElegantConstRoute {
 | 
			
		||||
      id: string;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    interface UserRoute {
 | 
			
		||||
      routes: MenuRoute[];
 | 
			
		||||
      home: import('@elegant-router/types').LastLevelRouteKey;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										166
									
								
								src/typings/app.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										166
									
								
								src/typings/app.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -16,24 +16,18 @@ declare namespace App {
 | 
			
		||||
      recommendColor: boolean;
 | 
			
		||||
      /** Theme color */
 | 
			
		||||
      themeColor: string;
 | 
			
		||||
      /** Theme radius */
 | 
			
		||||
      themeRadius: number;
 | 
			
		||||
      /** Other color */
 | 
			
		||||
      otherColor: OtherColor;
 | 
			
		||||
      /** Whether info color is followed by the primary color */
 | 
			
		||||
      isInfoFollowPrimary: boolean;
 | 
			
		||||
      /** Reset cache strategy */
 | 
			
		||||
      resetCacheStrategy: UnionKey.ResetCacheStrategy;
 | 
			
		||||
      /** Layout */
 | 
			
		||||
      layout: {
 | 
			
		||||
        /** Layout mode */
 | 
			
		||||
        mode: UnionKey.ThemeLayoutMode;
 | 
			
		||||
        /** Scroll mode */
 | 
			
		||||
        scrollMode: UnionKey.ThemeScrollMode;
 | 
			
		||||
        /**
 | 
			
		||||
         * Whether to reverse the horizontal mix
 | 
			
		||||
         *
 | 
			
		||||
         * if true, the vertical child level menus in left and horizontal first level menus in top
 | 
			
		||||
         */
 | 
			
		||||
        reverseHorizontalMix: boolean;
 | 
			
		||||
      };
 | 
			
		||||
      /** Page */
 | 
			
		||||
      page: {
 | 
			
		||||
@@ -77,6 +71,8 @@ declare namespace App {
 | 
			
		||||
        height: number;
 | 
			
		||||
        /** Tab mode */
 | 
			
		||||
        mode: UnionKey.ThemeTabMode;
 | 
			
		||||
        /** Whether to close tab by middle click */
 | 
			
		||||
        closeTabByMiddleClick: boolean;
 | 
			
		||||
      };
 | 
			
		||||
      /** Fixed header and tab */
 | 
			
		||||
      fixedHeaderAndTab: boolean;
 | 
			
		||||
@@ -88,11 +84,14 @@ declare namespace App {
 | 
			
		||||
        width: number;
 | 
			
		||||
        /** Collapsed sider width */
 | 
			
		||||
        collapsedWidth: number;
 | 
			
		||||
        /** Sider width when the layout is 'vertical-mix' or 'horizontal-mix' */
 | 
			
		||||
        /** Sider width when the layout is 'vertical-mix', 'top-hybrid-sidebar-first', or 'top-hybrid-header-first' */
 | 
			
		||||
        mixWidth: number;
 | 
			
		||||
        /** Collapsed sider width when the layout is 'vertical-mix' or 'horizontal-mix' */
 | 
			
		||||
        /**
 | 
			
		||||
         * Collapsed sider width when the layout is 'vertical-mix', 'top-hybrid-sidebar-first', or
 | 
			
		||||
         * 'top-hybrid-header-first'
 | 
			
		||||
         */
 | 
			
		||||
        mixCollapsedWidth: number;
 | 
			
		||||
        /** Child menu width when the layout is 'vertical-mix' or 'horizontal-mix' */
 | 
			
		||||
        /** Child menu width when the layout is 'vertical-mix', 'top-hybrid-sidebar-first', or 'top-hybrid-header-first' */
 | 
			
		||||
        mixChildMenuWidth: number;
 | 
			
		||||
      };
 | 
			
		||||
      /** Footer */
 | 
			
		||||
@@ -103,7 +102,10 @@ declare namespace App {
 | 
			
		||||
        fixed: boolean;
 | 
			
		||||
        /** Footer height */
 | 
			
		||||
        height: number;
 | 
			
		||||
        /** Whether float the footer to the right when the layout is 'horizontal-mix' */
 | 
			
		||||
        /**
 | 
			
		||||
         * Whether float the footer to the right when the layout is 'top-hybrid-sidebar-first' or
 | 
			
		||||
         * 'top-hybrid-header-first'
 | 
			
		||||
         */
 | 
			
		||||
        right: boolean;
 | 
			
		||||
      };
 | 
			
		||||
      /** Watermark */
 | 
			
		||||
@@ -114,6 +116,10 @@ declare namespace App {
 | 
			
		||||
        text: string;
 | 
			
		||||
        /** Whether to use user name as watermark text */
 | 
			
		||||
        enableUserName: boolean;
 | 
			
		||||
        /** Whether to use current time as watermark text */
 | 
			
		||||
        enableTime: boolean;
 | 
			
		||||
        /** Time format for watermark text */
 | 
			
		||||
        timeFormat: string;
 | 
			
		||||
      };
 | 
			
		||||
      /** define some theme settings tokens, will transform to css variables */
 | 
			
		||||
      tokens: {
 | 
			
		||||
@@ -358,63 +364,105 @@ declare namespace App {
 | 
			
		||||
        tokenExpired: string;
 | 
			
		||||
      };
 | 
			
		||||
      theme: {
 | 
			
		||||
        themeSchema: { title: string } & Record<UnionKey.ThemeScheme, string>;
 | 
			
		||||
        grayscale: string;
 | 
			
		||||
        colourWeakness: string;
 | 
			
		||||
        layoutMode: { title: string; reverseHorizontalMix: string } & Record<UnionKey.ThemeLayoutMode, string>;
 | 
			
		||||
        recommendColor: string;
 | 
			
		||||
        recommendColorDesc: string;
 | 
			
		||||
        themeColor: {
 | 
			
		||||
          title: string;
 | 
			
		||||
          followPrimary: string;
 | 
			
		||||
        } & Theme.ThemeColor;
 | 
			
		||||
        scrollMode: { title: string } & Record<UnionKey.ThemeScrollMode, string>;
 | 
			
		||||
        page: {
 | 
			
		||||
          animate: string;
 | 
			
		||||
          mode: { title: string } & Record<UnionKey.ThemePageAnimateMode, string>;
 | 
			
		||||
        themeDrawerTitle: string;
 | 
			
		||||
        tabs: {
 | 
			
		||||
          appearance: string;
 | 
			
		||||
          layout: string;
 | 
			
		||||
          general: string;
 | 
			
		||||
          preset: string;
 | 
			
		||||
        };
 | 
			
		||||
        fixedHeaderAndTab: string;
 | 
			
		||||
        header: {
 | 
			
		||||
          height: string;
 | 
			
		||||
          breadcrumb: {
 | 
			
		||||
        appearance: {
 | 
			
		||||
          themeSchema: { title: string } & Record<UnionKey.ThemeScheme, string>;
 | 
			
		||||
          grayscale: string;
 | 
			
		||||
          colourWeakness: string;
 | 
			
		||||
          themeColor: {
 | 
			
		||||
            title: string;
 | 
			
		||||
            followPrimary: string;
 | 
			
		||||
          } & Record<Theme.ThemeColorKey, string>;
 | 
			
		||||
          recommendColor: string;
 | 
			
		||||
          recommendColorDesc: string;
 | 
			
		||||
          themeRadius: {
 | 
			
		||||
            title: string;
 | 
			
		||||
          };
 | 
			
		||||
          preset: {
 | 
			
		||||
            title: string;
 | 
			
		||||
            apply: string;
 | 
			
		||||
            applySuccess: string;
 | 
			
		||||
            [key: string]:
 | 
			
		||||
              | {
 | 
			
		||||
                  name: string;
 | 
			
		||||
                  desc: string;
 | 
			
		||||
                }
 | 
			
		||||
              | string;
 | 
			
		||||
          };
 | 
			
		||||
        };
 | 
			
		||||
        layout: {
 | 
			
		||||
          layoutMode: { title: string } & Record<UnionKey.ThemeLayoutMode, string> & {
 | 
			
		||||
              [K in `${UnionKey.ThemeLayoutMode}_detail`]: string;
 | 
			
		||||
            };
 | 
			
		||||
          tab: {
 | 
			
		||||
            title: string;
 | 
			
		||||
            visible: string;
 | 
			
		||||
            showIcon: string;
 | 
			
		||||
            cache: string;
 | 
			
		||||
            cacheTip: string;
 | 
			
		||||
            height: string;
 | 
			
		||||
            mode: { title: string } & Record<UnionKey.ThemeTabMode, string>;
 | 
			
		||||
            closeByMiddleClick: string;
 | 
			
		||||
            closeByMiddleClickTip: string;
 | 
			
		||||
          };
 | 
			
		||||
          header: {
 | 
			
		||||
            title: string;
 | 
			
		||||
            height: string;
 | 
			
		||||
            breadcrumb: {
 | 
			
		||||
              visible: string;
 | 
			
		||||
              showIcon: string;
 | 
			
		||||
            };
 | 
			
		||||
          };
 | 
			
		||||
          sider: {
 | 
			
		||||
            title: string;
 | 
			
		||||
            inverted: string;
 | 
			
		||||
            width: string;
 | 
			
		||||
            collapsedWidth: string;
 | 
			
		||||
            mixWidth: string;
 | 
			
		||||
            mixCollapsedWidth: string;
 | 
			
		||||
            mixChildMenuWidth: string;
 | 
			
		||||
          };
 | 
			
		||||
          footer: {
 | 
			
		||||
            title: string;
 | 
			
		||||
            visible: string;
 | 
			
		||||
            fixed: string;
 | 
			
		||||
            height: string;
 | 
			
		||||
            right: string;
 | 
			
		||||
          };
 | 
			
		||||
          content: {
 | 
			
		||||
            title: string;
 | 
			
		||||
            scrollMode: { title: string; tip: string } & Record<UnionKey.ThemeScrollMode, string>;
 | 
			
		||||
            page: {
 | 
			
		||||
              animate: string;
 | 
			
		||||
              mode: { title: string } & Record<UnionKey.ThemePageAnimateMode, string>;
 | 
			
		||||
            };
 | 
			
		||||
            fixedHeaderAndTab: string;
 | 
			
		||||
          };
 | 
			
		||||
        };
 | 
			
		||||
        general: {
 | 
			
		||||
          title: string;
 | 
			
		||||
          watermark: {
 | 
			
		||||
            title: string;
 | 
			
		||||
            visible: string;
 | 
			
		||||
            text: string;
 | 
			
		||||
            enableUserName: string;
 | 
			
		||||
            enableTime: string;
 | 
			
		||||
            timeFormat: string;
 | 
			
		||||
          };
 | 
			
		||||
          multilingual: {
 | 
			
		||||
            title: string;
 | 
			
		||||
            visible: string;
 | 
			
		||||
          };
 | 
			
		||||
          globalSearch: {
 | 
			
		||||
            title: string;
 | 
			
		||||
            visible: string;
 | 
			
		||||
          };
 | 
			
		||||
        };
 | 
			
		||||
        tab: {
 | 
			
		||||
          visible: string;
 | 
			
		||||
          cache: string;
 | 
			
		||||
          height: string;
 | 
			
		||||
          mode: { title: string } & Record<UnionKey.ThemeTabMode, string>;
 | 
			
		||||
        };
 | 
			
		||||
        sider: {
 | 
			
		||||
          inverted: string;
 | 
			
		||||
          width: string;
 | 
			
		||||
          collapsedWidth: string;
 | 
			
		||||
          mixWidth: string;
 | 
			
		||||
          mixCollapsedWidth: string;
 | 
			
		||||
          mixChildMenuWidth: string;
 | 
			
		||||
        };
 | 
			
		||||
        footer: {
 | 
			
		||||
          visible: string;
 | 
			
		||||
          fixed: string;
 | 
			
		||||
          height: string;
 | 
			
		||||
          right: string;
 | 
			
		||||
        };
 | 
			
		||||
        watermark: {
 | 
			
		||||
          visible: string;
 | 
			
		||||
          text: string;
 | 
			
		||||
          enableUserName: string;
 | 
			
		||||
        };
 | 
			
		||||
        themeDrawerTitle: string;
 | 
			
		||||
        pageFunTitle: string;
 | 
			
		||||
        resetCacheStrategy: { title: string } & Record<UnionKey.ResetCacheStrategy, string>;
 | 
			
		||||
        configOperation: {
 | 
			
		||||
          copyConfig: string;
 | 
			
		||||
          copySuccessMsg: string;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										103
									
								
								src/typings/components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										103
									
								
								src/typings/components.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -1,8 +1,12 @@
 | 
			
		||||
/* eslint-disable */
 | 
			
		||||
// @ts-nocheck
 | 
			
		||||
// biome-ignore lint: disable
 | 
			
		||||
// oxlint-disable
 | 
			
		||||
// ------
 | 
			
		||||
// Generated by unplugin-vue-components
 | 
			
		||||
// Read more: https://github.com/vuejs/core/pull/3399
 | 
			
		||||
// biome-ignore lint: disable
 | 
			
		||||
import { GlobalComponents } from 'vue'
 | 
			
		||||
 | 
			
		||||
export {}
 | 
			
		||||
 | 
			
		||||
/* prettier-ignore */
 | 
			
		||||
@@ -17,47 +21,35 @@ declare module 'vue' {
 | 
			
		||||
    FullScreen: typeof import('./../components/common/full-screen.vue')['default']
 | 
			
		||||
    IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default']
 | 
			
		||||
    IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default']
 | 
			
		||||
    IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined')['default']
 | 
			
		||||
    IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
 | 
			
		||||
    IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
 | 
			
		||||
    'IconIc:roundPlus': typeof import('~icons/ic/round-plus')['default']
 | 
			
		||||
    IconIcRoundDelete: typeof import('~icons/ic/round-delete')['default']
 | 
			
		||||
    IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default']
 | 
			
		||||
    IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
 | 
			
		||||
    IconIcRoundRemove: typeof import('~icons/ic/round-remove')['default']
 | 
			
		||||
    IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
 | 
			
		||||
    IconLocalBanner: typeof import('~icons/local/banner')['default']
 | 
			
		||||
    IconLocalLogo: typeof import('~icons/local/logo')['default']
 | 
			
		||||
    IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
 | 
			
		||||
    IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
 | 
			
		||||
    IconMdiDrag: typeof import('~icons/mdi/drag')['default']
 | 
			
		||||
    IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
 | 
			
		||||
    IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
 | 
			
		||||
    IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
 | 
			
		||||
    IconTooltip: typeof import('./../components/common/icon-tooltip.vue')['default']
 | 
			
		||||
    IconUilSearch: typeof import('~icons/uil/search')['default']
 | 
			
		||||
    LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
 | 
			
		||||
    LookForward: typeof import('./../components/custom/look-forward.vue')['default']
 | 
			
		||||
    MenuToggler: typeof import('./../components/common/menu-toggler.vue')['default']
 | 
			
		||||
    NAlert: typeof import('naive-ui')['NAlert']
 | 
			
		||||
    NBadge: typeof import('naive-ui')['NBadge']
 | 
			
		||||
    NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
 | 
			
		||||
    NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
 | 
			
		||||
    NButton: typeof import('naive-ui')['NButton']
 | 
			
		||||
    NCard: typeof import('naive-ui')['NCard']
 | 
			
		||||
    NCheckbox: typeof import('naive-ui')['NCheckbox']
 | 
			
		||||
    NColorPicker: typeof import('naive-ui')['NColorPicker']
 | 
			
		||||
    NDataTable: typeof import('naive-ui')['NDataTable']
 | 
			
		||||
    NDescriptions: typeof import('naive-ui')['NDescriptions']
 | 
			
		||||
    NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
 | 
			
		||||
    NDialogProvider: typeof import('naive-ui')['NDialogProvider']
 | 
			
		||||
    NDivider: typeof import('naive-ui')['NDivider']
 | 
			
		||||
    NDrawer: typeof import('naive-ui')['NDrawer']
 | 
			
		||||
    NDrawerContent: typeof import('naive-ui')['NDrawerContent']
 | 
			
		||||
    NDropdown: typeof import('naive-ui')['NDropdown']
 | 
			
		||||
    NDynamicInput: typeof import('naive-ui')['NDynamicInput']
 | 
			
		||||
    NEmpty: typeof import('naive-ui')['NEmpty']
 | 
			
		||||
    NForm: typeof import('naive-ui')['NForm']
 | 
			
		||||
    NFormItem: typeof import('naive-ui')['NFormItem']
 | 
			
		||||
    NFormItemGi: typeof import('naive-ui')['NFormItemGi']
 | 
			
		||||
    NGi: typeof import('naive-ui')['NGi']
 | 
			
		||||
    NGrid: typeof import('naive-ui')['NGrid']
 | 
			
		||||
    NInput: typeof import('naive-ui')['NInput']
 | 
			
		||||
@@ -70,10 +62,7 @@ declare module 'vue' {
 | 
			
		||||
    NMessageProvider: typeof import('naive-ui')['NMessageProvider']
 | 
			
		||||
    NModal: typeof import('naive-ui')['NModal']
 | 
			
		||||
    NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
 | 
			
		||||
    NPopconfirm: typeof import('naive-ui')['NPopconfirm']
 | 
			
		||||
    NPopover: typeof import('naive-ui')['NPopover']
 | 
			
		||||
    NRadio: typeof import('naive-ui')['NRadio']
 | 
			
		||||
    NRadioGroup: typeof import('naive-ui')['NRadioGroup']
 | 
			
		||||
    NScrollbar: typeof import('naive-ui')['NScrollbar']
 | 
			
		||||
    NSelect: typeof import('naive-ui')['NSelect']
 | 
			
		||||
    NSpace: typeof import('naive-ui')['NSpace']
 | 
			
		||||
@@ -81,10 +70,8 @@ declare module 'vue' {
 | 
			
		||||
    NSwitch: typeof import('naive-ui')['NSwitch']
 | 
			
		||||
    NTab: typeof import('naive-ui')['NTab']
 | 
			
		||||
    NTabs: typeof import('naive-ui')['NTabs']
 | 
			
		||||
    NTag: typeof import('naive-ui')['NTag']
 | 
			
		||||
    NThing: typeof import('naive-ui')['NThing']
 | 
			
		||||
    NTooltip: typeof import('naive-ui')['NTooltip']
 | 
			
		||||
    NTree: typeof import('naive-ui')['NTree']
 | 
			
		||||
    NWatermark: typeof import('naive-ui')['NWatermark']
 | 
			
		||||
    PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
 | 
			
		||||
    ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
 | 
			
		||||
@@ -99,3 +86,79 @@ declare module 'vue' {
 | 
			
		||||
    WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// For TSX support
 | 
			
		||||
declare global {
 | 
			
		||||
  const AppProvider: typeof import('./../components/common/app-provider.vue')['default']
 | 
			
		||||
  const BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
 | 
			
		||||
  const ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
 | 
			
		||||
  const CountTo: typeof import('./../components/custom/count-to.vue')['default']
 | 
			
		||||
  const DarkModeContainer: typeof import('./../components/common/dark-mode-container.vue')['default']
 | 
			
		||||
  const ExceptionBase: typeof import('./../components/common/exception-base.vue')['default']
 | 
			
		||||
  const FullScreen: typeof import('./../components/common/full-screen.vue')['default']
 | 
			
		||||
  const IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default']
 | 
			
		||||
  const IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default']
 | 
			
		||||
  const IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
 | 
			
		||||
  const IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
 | 
			
		||||
  const IconLocalBanner: typeof import('~icons/local/banner')['default']
 | 
			
		||||
  const IconLocalLogo: typeof import('~icons/local/logo')['default']
 | 
			
		||||
  const IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
 | 
			
		||||
  const IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
 | 
			
		||||
  const IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
 | 
			
		||||
  const IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
 | 
			
		||||
  const IconTooltip: typeof import('./../components/common/icon-tooltip.vue')['default']
 | 
			
		||||
  const IconUilSearch: typeof import('~icons/uil/search')['default']
 | 
			
		||||
  const LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
 | 
			
		||||
  const LookForward: typeof import('./../components/custom/look-forward.vue')['default']
 | 
			
		||||
  const MenuToggler: typeof import('./../components/common/menu-toggler.vue')['default']
 | 
			
		||||
  const NAlert: typeof import('naive-ui')['NAlert']
 | 
			
		||||
  const NBadge: typeof import('naive-ui')['NBadge']
 | 
			
		||||
  const NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
 | 
			
		||||
  const NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
 | 
			
		||||
  const NButton: typeof import('naive-ui')['NButton']
 | 
			
		||||
  const NCard: typeof import('naive-ui')['NCard']
 | 
			
		||||
  const NCheckbox: typeof import('naive-ui')['NCheckbox']
 | 
			
		||||
  const NColorPicker: typeof import('naive-ui')['NColorPicker']
 | 
			
		||||
  const NDialogProvider: typeof import('naive-ui')['NDialogProvider']
 | 
			
		||||
  const NDivider: typeof import('naive-ui')['NDivider']
 | 
			
		||||
  const NDrawer: typeof import('naive-ui')['NDrawer']
 | 
			
		||||
  const NDrawerContent: typeof import('naive-ui')['NDrawerContent']
 | 
			
		||||
  const NDropdown: typeof import('naive-ui')['NDropdown']
 | 
			
		||||
  const NEmpty: typeof import('naive-ui')['NEmpty']
 | 
			
		||||
  const NForm: typeof import('naive-ui')['NForm']
 | 
			
		||||
  const NFormItem: typeof import('naive-ui')['NFormItem']
 | 
			
		||||
  const NGi: typeof import('naive-ui')['NGi']
 | 
			
		||||
  const NGrid: typeof import('naive-ui')['NGrid']
 | 
			
		||||
  const NInput: typeof import('naive-ui')['NInput']
 | 
			
		||||
  const NInputGroup: typeof import('naive-ui')['NInputGroup']
 | 
			
		||||
  const NInputNumber: typeof import('naive-ui')['NInputNumber']
 | 
			
		||||
  const NList: typeof import('naive-ui')['NList']
 | 
			
		||||
  const NListItem: typeof import('naive-ui')['NListItem']
 | 
			
		||||
  const NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
 | 
			
		||||
  const NMenu: typeof import('naive-ui')['NMenu']
 | 
			
		||||
  const NMessageProvider: typeof import('naive-ui')['NMessageProvider']
 | 
			
		||||
  const NModal: typeof import('naive-ui')['NModal']
 | 
			
		||||
  const NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
 | 
			
		||||
  const NPopover: typeof import('naive-ui')['NPopover']
 | 
			
		||||
  const NScrollbar: typeof import('naive-ui')['NScrollbar']
 | 
			
		||||
  const NSelect: typeof import('naive-ui')['NSelect']
 | 
			
		||||
  const NSpace: typeof import('naive-ui')['NSpace']
 | 
			
		||||
  const NStatistic: typeof import('naive-ui')['NStatistic']
 | 
			
		||||
  const NSwitch: typeof import('naive-ui')['NSwitch']
 | 
			
		||||
  const NTab: typeof import('naive-ui')['NTab']
 | 
			
		||||
  const NTabs: typeof import('naive-ui')['NTabs']
 | 
			
		||||
  const NThing: typeof import('naive-ui')['NThing']
 | 
			
		||||
  const NTooltip: typeof import('naive-ui')['NTooltip']
 | 
			
		||||
  const NWatermark: typeof import('naive-ui')['NWatermark']
 | 
			
		||||
  const PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
 | 
			
		||||
  const ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
 | 
			
		||||
  const RouterLink: typeof import('vue-router')['RouterLink']
 | 
			
		||||
  const RouterView: typeof import('vue-router')['RouterView']
 | 
			
		||||
  const SoybeanAvatar: typeof import('./../components/custom/soybean-avatar.vue')['default']
 | 
			
		||||
  const SvgIcon: typeof import('./../components/custom/svg-icon.vue')['default']
 | 
			
		||||
  const SystemLogo: typeof import('./../components/common/system-logo.vue')['default']
 | 
			
		||||
  const TableColumnSetting: typeof import('./../components/advanced/table-column-setting.vue')['default']
 | 
			
		||||
  const TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
 | 
			
		||||
  const ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
 | 
			
		||||
  const WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										32
									
								
								src/typings/naive-ui.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										32
									
								
								src/typings/naive-ui.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -6,30 +6,14 @@ declare namespace NaiveUI {
 | 
			
		||||
  type DataTableExpandColumn<T> = import('naive-ui').DataTableExpandColumn<T>;
 | 
			
		||||
  type DataTableSelectionColumn<T> = import('naive-ui').DataTableSelectionColumn<T>;
 | 
			
		||||
  type TableColumnGroup<T> = import('naive-ui/es/data-table/src/interface').TableColumnGroup<T>;
 | 
			
		||||
  type PaginationProps = import('naive-ui').PaginationProps;
 | 
			
		||||
  type TableColumnCheck = import('@sa/hooks').TableColumnCheck;
 | 
			
		||||
  type TableDataWithIndex<T> = import('@sa/hooks').TableDataWithIndex<T>;
 | 
			
		||||
  type FlatResponseData<T> = import('@sa/axios').FlatResponseData<T>;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * the custom column key
 | 
			
		||||
   *
 | 
			
		||||
   * if you want to add a custom column, you should add a key to this type
 | 
			
		||||
   */
 | 
			
		||||
  type CustomColumnKey = 'operate';
 | 
			
		||||
 | 
			
		||||
  type SetTableColumnKey<C, T> = Omit<C, 'key'> & { key: keyof T | CustomColumnKey };
 | 
			
		||||
 | 
			
		||||
  type TableData = Api.Common.CommonRecord<object>;
 | 
			
		||||
  type SetTableColumnKey<C, T> = Omit<C, 'key'> & { key: keyof T | (string & {}) };
 | 
			
		||||
 | 
			
		||||
  type TableColumnWithKey<T> = SetTableColumnKey<DataTableBaseColumn<T>, T> | SetTableColumnKey<TableColumnGroup<T>, T>;
 | 
			
		||||
 | 
			
		||||
  type TableColumn<T> = TableColumnWithKey<T> | DataTableSelectionColumn<T> | DataTableExpandColumn<T>;
 | 
			
		||||
 | 
			
		||||
  type TableApiFn<T = any, R = Api.Common.CommonSearchParams> = (
 | 
			
		||||
    params: R
 | 
			
		||||
  ) => Promise<FlatResponseData<Api.Common.PaginatingQueryRecord<T>>>;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * the type of table operation
 | 
			
		||||
   *
 | 
			
		||||
@@ -37,18 +21,4 @@ declare namespace NaiveUI {
 | 
			
		||||
   * - edit: edit table item
 | 
			
		||||
   */
 | 
			
		||||
  type TableOperateType = 'add' | 'edit';
 | 
			
		||||
 | 
			
		||||
  type GetTableData<A extends TableApiFn> = A extends TableApiFn<infer T> ? T : never;
 | 
			
		||||
 | 
			
		||||
  type NaiveTableConfig<A extends TableApiFn> = Pick<
 | 
			
		||||
    import('@sa/hooks').TableConfig<A, GetTableData<A>, TableColumn<TableDataWithIndex<GetTableData<A>>>>,
 | 
			
		||||
    'apiFn' | 'apiParams' | 'columns' | 'immediate'
 | 
			
		||||
  > & {
 | 
			
		||||
    /**
 | 
			
		||||
     * whether to display the total items count
 | 
			
		||||
     *
 | 
			
		||||
     * @default false
 | 
			
		||||
     */
 | 
			
		||||
    showTotal?: boolean;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								src/typings/union-key.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								src/typings/union-key.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -14,23 +14,22 @@ declare namespace UnionKey {
 | 
			
		||||
  /** Theme scheme */
 | 
			
		||||
  type ThemeScheme = 'light' | 'dark' | 'auto';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Reset cache strategy
 | 
			
		||||
   *
 | 
			
		||||
   * - close: re-cache when close page
 | 
			
		||||
   * - refresh: re-cache when refresh page
 | 
			
		||||
   */
 | 
			
		||||
  type ResetCacheStrategy = 'close' | 'refresh';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The layout mode
 | 
			
		||||
   *
 | 
			
		||||
   * - vertical: the vertical menu in left
 | 
			
		||||
   * - horizontal: the horizontal menu in top
 | 
			
		||||
   * - vertical-mix: two vertical mixed menus in left
 | 
			
		||||
   * - horizontal-mix: the vertical first level menus in left and horizontal child level menus in top
 | 
			
		||||
   * - top-hybrid-sidebar-first: the vertical first level menus in left and horizontal child level menus in top
 | 
			
		||||
   * - top-hybrid-header-first: the horizontal first level menus in top and vertical child level menus in left
 | 
			
		||||
   */
 | 
			
		||||
  type ThemeLayoutMode = 'vertical' | 'horizontal' | 'vertical-mix' | 'horizontal-mix';
 | 
			
		||||
  type ThemeLayoutMode =
 | 
			
		||||
    | 'vertical'
 | 
			
		||||
    | 'horizontal'
 | 
			
		||||
    | 'vertical-mix'
 | 
			
		||||
    | 'vertical-hybrid-header-first'
 | 
			
		||||
    | 'top-hybrid-sidebar-first'
 | 
			
		||||
    | 'top-hybrid-header-first';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The scroll mode when content overflow
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ const appStore = useAppStore();
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
 | 
			
		||||
interface LoginModule {
 | 
			
		||||
  label: string;
 | 
			
		||||
  label: App.I18n.I18nKey;
 | 
			
		||||
  component: Component;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { createReusableTemplate } from '@vueuse/core';
 | 
			
		||||
import { useThemeStore } from '@/store/modules/theme';
 | 
			
		||||
import { $t } from '@/locales';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
@@ -72,6 +73,8 @@ interface GradientBgProps {
 | 
			
		||||
 | 
			
		||||
const [DefineGradientBg, GradientBg] = createReusableTemplate<GradientBgProps>();
 | 
			
		||||
 | 
			
		||||
const themeStore = useThemeStore();
 | 
			
		||||
 | 
			
		||||
function getGradientColor(color: CardData['color']) {
 | 
			
		||||
  return `linear-gradient(to bottom right, ${color.start}, ${color.end})`;
 | 
			
		||||
}
 | 
			
		||||
@@ -81,7 +84,10 @@ function getGradientColor(color: CardData['color']) {
 | 
			
		||||
  <NCard :bordered="false" size="small" class="card-wrapper">
 | 
			
		||||
    <!-- define component start: GradientBg -->
 | 
			
		||||
    <DefineGradientBg v-slot="{ $slots, gradientColor }">
 | 
			
		||||
      <div class="rd-8px px-16px pb-4px pt-8px text-white" :style="{ backgroundImage: gradientColor }">
 | 
			
		||||
      <div
 | 
			
		||||
        class="px-16px pb-4px pt-8px text-white"
 | 
			
		||||
        :style="{ backgroundImage: gradientColor, borderRadius: themeStore.themeRadius + 'px' }"
 | 
			
		||||
      >
 | 
			
		||||
        <component :is="$slots.default" />
 | 
			
		||||
      </div>
 | 
			
		||||
    </DefineGradientBg>
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user