├── .editorconfig ├── .env ├── .env.prod ├── .env.test ├── .gitattributes ├── .gitignore ├── .npmrc ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── build ├── config │ ├── index.ts │ ├── proxy.ts │ └── time.ts └── plugins │ ├── devtools.ts │ ├── html.ts │ ├── index.ts │ ├── router.ts │ ├── unocss.ts │ └── unplugin.ts ├── doc └── images │ ├── Home-2.png │ ├── Home-Mobile.png │ ├── Home.png │ ├── Login.png │ ├── Menu-2.png │ ├── Menu-3.png │ ├── Menu-Mobile-Dark.png │ ├── Menu-Mobile.png │ ├── Menu.png │ ├── Role-Menu.png │ ├── Role-Permission.png │ ├── User-2.png │ ├── User-Dark.png │ ├── User-Mobile.png │ └── User.png ├── eslint.config.js ├── index.html ├── nginx └── default.conf ├── package.json ├── packages ├── alova │ ├── package.json │ ├── src │ │ ├── client.ts │ │ ├── constant.ts │ │ ├── fetch.ts │ │ ├── index.ts │ │ ├── mock.ts │ │ └── type.ts │ └── tsconfig.json ├── axios │ ├── package.json │ ├── src │ │ ├── constant.ts │ │ ├── index.ts │ │ ├── options.ts │ │ ├── shared.ts │ │ └── type.ts │ └── tsconfig.json ├── color │ ├── package.json │ ├── src │ │ ├── constant │ │ │ ├── index.ts │ │ │ ├── name.ts │ │ │ └── palette.ts │ │ ├── index.ts │ │ ├── palette │ │ │ ├── antd.ts │ │ │ ├── index.ts │ │ │ └── recommend.ts │ │ ├── shared │ │ │ ├── colord.ts │ │ │ ├── index.ts │ │ │ └── name.ts │ │ └── types │ │ │ └── index.ts │ └── tsconfig.json ├── hooks │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── use-boolean.ts │ │ ├── use-context.ts │ │ ├── use-count-down.ts │ │ ├── use-loading.ts │ │ ├── use-request.ts │ │ ├── use-signal.ts │ │ ├── use-svg-icon-render.ts │ │ └── use-table.ts │ └── tsconfig.json ├── materials │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── libs │ │ │ ├── admin-layout │ │ │ │ ├── index.module.css │ │ │ │ ├── index.module.css.d.ts │ │ │ │ ├── index.ts │ │ │ │ ├── index.vue │ │ │ │ └── shared.ts │ │ │ ├── page-tab │ │ │ │ ├── button-tab.vue │ │ │ │ ├── chrome-tab-bg.vue │ │ │ │ ├── chrome-tab.vue │ │ │ │ ├── index.module.css │ │ │ │ ├── index.module.css.d.ts │ │ │ │ ├── index.ts │ │ │ │ ├── index.vue │ │ │ │ ├── shared.ts │ │ │ │ └── svg-close.vue │ │ │ └── simple-scrollbar │ │ │ │ ├── index.ts │ │ │ │ └── index.vue │ │ └── types │ │ │ └── index.ts │ └── tsconfig.json ├── ofetch │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── scripts │ ├── bin.ts │ ├── package.json │ ├── src │ │ ├── commands │ │ │ ├── changelog.ts │ │ │ ├── cleanup.ts │ │ │ ├── git-commit.ts │ │ │ ├── index.ts │ │ │ ├── release.ts │ │ │ ├── router.ts │ │ │ └── update-pkg.ts │ │ ├── config │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── locales │ │ │ └── index.ts │ │ ├── shared │ │ │ └── index.ts │ │ └── types │ │ │ └── index.ts │ └── tsconfig.json ├── uno-preset │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json └── utils │ ├── package.json │ ├── src │ ├── crypto.ts │ ├── index.ts │ ├── klona.ts │ ├── nanoid.ts │ └── storage.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── public └── favicon.svg ├── src ├── App.vue ├── assets │ ├── imgs │ │ └── soybean.jpg │ └── svg-icon │ │ ├── activity.svg │ │ ├── at-sign.svg │ │ ├── avatar.svg │ │ ├── banner.svg │ │ ├── cast.svg │ │ ├── chrome.svg │ │ ├── copy.svg │ │ ├── custom-icon.svg │ │ ├── empty-data.svg │ │ ├── expectation.svg │ │ ├── heart.svg │ │ ├── logo.svg │ │ ├── network-error.svg │ │ ├── no-icon.svg │ │ ├── no-permission.svg │ │ ├── not-found.svg │ │ ├── service-error.svg │ │ └── wind.svg ├── components │ ├── advanced │ │ ├── table-column-setting.vue │ │ └── table-header-operation.vue │ ├── common │ │ ├── app-provider.vue │ │ ├── dark-mode-container.vue │ │ ├── exception-base.vue │ │ ├── full-screen.vue │ │ ├── lang-switch.vue │ │ ├── menu-toggler.vue │ │ ├── pin-toggler.vue │ │ ├── reload-button.vue │ │ ├── system-logo.vue │ │ └── theme-schema-switch.vue │ └── custom │ │ ├── better-scroll.vue │ │ ├── button-icon.vue │ │ ├── count-to.vue │ │ ├── look-forward.vue │ │ ├── soybean-avatar.vue │ │ ├── svg-icon.vue │ │ └── wave-bg.vue ├── constants │ ├── app.ts │ ├── business.ts │ ├── common.ts │ └── reg.ts ├── enum │ └── index.ts ├── hooks │ ├── business │ │ ├── auth.ts │ │ ├── captcha.ts │ │ └── dict.ts │ └── common │ │ ├── button-auth-dropdown.ts │ │ ├── echarts.ts │ │ ├── form.ts │ │ ├── icon.ts │ │ ├── router.ts │ │ └── table.ts ├── layouts │ ├── base-layout │ │ └── index.vue │ ├── blank-layout │ │ └── index.vue │ ├── context │ │ └── index.ts │ ├── hooks │ │ └── index.ts │ └── modules │ │ ├── global-breadcrumb │ │ └── index.vue │ │ ├── global-content │ │ └── index.vue │ │ ├── global-footer │ │ └── index.vue │ │ ├── global-header │ │ ├── components │ │ │ ├── theme-button.vue │ │ │ └── user-avatar.vue │ │ └── index.vue │ │ ├── global-logo │ │ └── index.vue │ │ ├── global-menu │ │ ├── components │ │ │ └── first-level-menu.vue │ │ ├── index.vue │ │ └── modules │ │ │ ├── horizontal-menu.vue │ │ │ ├── horizontal-mix-menu.vue │ │ │ ├── reversed-horizontal-mix-menu.vue │ │ │ ├── vertical-menu.vue │ │ │ └── vertical-mix-menu.vue │ │ ├── global-search │ │ ├── components │ │ │ ├── search-footer.vue │ │ │ ├── search-modal.vue │ │ │ └── search-result.vue │ │ └── index.vue │ │ ├── global-sider │ │ └── index.vue │ │ ├── global-tab │ │ ├── context-menu.vue │ │ └── index.vue │ │ └── theme-drawer │ │ ├── components │ │ ├── layout-mode-card.vue │ │ └── setting-item.vue │ │ ├── index.vue │ │ └── modules │ │ ├── config-operation.vue │ │ ├── dark-mode.vue │ │ ├── layout-mode.vue │ │ ├── page-fun.vue │ │ └── theme-color.vue ├── locales │ ├── dayjs.ts │ ├── index.ts │ ├── langs │ │ ├── en-us.ts │ │ └── zh-cn.ts │ ├── locale.ts │ └── naive.ts ├── main.ts ├── plugins │ ├── app.ts │ ├── assets.ts │ ├── dayjs.ts │ ├── iconify.ts │ ├── index.ts │ ├── loading.ts │ ├── nprogress.ts │ └── nsettings.ts ├── router │ ├── elegant │ │ ├── imports.ts │ │ ├── routes.ts │ │ └── transform.ts │ ├── guard │ │ ├── index.ts │ │ ├── progress.ts │ │ ├── route.ts │ │ └── title.ts │ ├── index.ts │ └── routes │ │ ├── builtin.ts │ │ └── index.ts ├── service │ ├── api │ │ ├── auth.ts │ │ ├── index.ts │ │ ├── manage │ │ │ ├── dict.ts │ │ │ ├── index.ts │ │ │ ├── menu.ts │ │ │ ├── notice.ts │ │ │ ├── org-units.ts │ │ │ ├── permission.ts │ │ │ ├── position.ts │ │ │ ├── role.ts │ │ │ └── user.ts │ │ ├── monitor │ │ │ ├── cache.ts │ │ │ ├── file.ts │ │ │ ├── index.ts │ │ │ ├── logs-error.ts │ │ │ ├── logs-login.ts │ │ │ ├── logs-operation.ts │ │ │ ├── logs-scheduler.ts │ │ │ ├── scheduler.ts │ │ │ └── system-info.ts │ │ ├── route.ts │ │ └── tools │ │ │ ├── data-table.ts │ │ │ ├── generate-table-column.ts │ │ │ ├── generate-table.ts │ │ │ └── index.ts │ └── request │ │ ├── index.ts │ │ ├── shared.ts │ │ └── type.ts ├── store │ ├── index.ts │ ├── modules │ │ ├── app │ │ │ └── index.ts │ │ ├── auth │ │ │ ├── index.ts │ │ │ └── shared.ts │ │ ├── dict │ │ │ └── index.ts │ │ ├── route │ │ │ ├── index.ts │ │ │ └── shared.ts │ │ ├── tab │ │ │ ├── index.ts │ │ │ └── shared.ts │ │ └── theme │ │ │ ├── index.ts │ │ │ └── shared.ts │ └── plugins │ │ └── index.ts ├── styles │ ├── css │ │ ├── global.css │ │ ├── nprogress.css │ │ ├── reset.css │ │ └── transition.css │ └── scss │ │ ├── global.scss │ │ ├── naive-ui.scss │ │ └── scrollbar.scss ├── theme │ ├── settings.ts │ └── vars.ts ├── typings │ ├── api.d.ts │ ├── api │ │ ├── auth.d.ts │ │ ├── manage │ │ │ ├── dict.d.ts │ │ │ ├── index.d.ts │ │ │ ├── menu.d.ts │ │ │ ├── notice.d.ts │ │ │ ├── org-units.d.ts │ │ │ ├── permission.d.ts │ │ │ ├── position.d.ts │ │ │ ├── role.d.ts │ │ │ └── user.d.ts │ │ ├── monitor │ │ │ ├── cache.d.ts │ │ │ ├── file.d.ts │ │ │ ├── index.d.ts │ │ │ ├── logs-error.d.ts │ │ │ ├── logs-login.d.ts │ │ │ ├── logs-operation.d.ts │ │ │ ├── logs-scheduler.d.ts │ │ │ ├── scheduler.d.ts │ │ │ └── system-info.d.ts │ │ ├── route.d.ts │ │ └── tools │ │ │ ├── data-table.d.ts │ │ │ ├── generate-table.d.ts │ │ │ └── index.d.ts │ ├── app.d.ts │ ├── common.d.ts │ ├── components.d.ts │ ├── elegant-router.d.ts │ ├── global.d.ts │ ├── naive-ui.d.ts │ ├── router.d.ts │ ├── storage.d.ts │ ├── union-key.d.ts │ └── vite-env.d.ts ├── utils │ ├── agent.ts │ ├── clipboard.ts │ ├── common.ts │ ├── crypto.ts │ ├── date.ts │ ├── download.ts │ ├── icon.ts │ ├── service.ts │ └── storage.ts └── views │ ├── _builtin │ ├── 403 │ │ └── index.vue │ ├── 404 │ │ └── index.vue │ ├── 500 │ │ └── index.vue │ ├── iframe-page │ │ └── [url].vue │ └── login │ │ ├── index.vue │ │ └── modules │ │ ├── bind-wechat.vue │ │ ├── code-login.vue │ │ ├── pwd-login.vue │ │ ├── register.vue │ │ └── reset-pwd.vue │ ├── about │ └── index.vue │ ├── function │ ├── hide-child │ │ ├── one │ │ │ └── index.vue │ │ ├── three │ │ │ └── index.vue │ │ └── two │ │ │ └── index.vue │ ├── multi-tab │ │ └── index.vue │ ├── request │ │ └── index.vue │ ├── super-page │ │ └── index.vue │ ├── tab │ │ └── index.vue │ └── toggle-auth │ │ └── index.vue │ ├── home │ ├── index.vue │ └── modules │ │ ├── card-data.vue │ │ ├── creativity-banner.vue │ │ ├── header-banner.vue │ │ ├── line-chart.vue │ │ ├── pie-chart.vue │ │ └── project-news.vue │ ├── manage │ ├── dict │ │ ├── index.vue │ │ └── modules │ │ │ ├── dict-item-operate-drawer.vue │ │ │ ├── dict-item-page-table.vue │ │ │ ├── dict-item-search.vue │ │ │ └── dict-operate-drawer.vue │ ├── menu │ │ ├── index.vue │ │ └── modules │ │ │ ├── menu-operate-drawer.vue │ │ │ ├── permission-list-table.vue │ │ │ ├── permission-operate-modal.vue │ │ │ └── shared.ts │ ├── notice │ │ ├── index.vue │ │ └── modules │ │ │ ├── notice-operate-drawer.vue │ │ │ └── notice-search.vue │ ├── org │ │ ├── index.vue │ │ └── modules │ │ │ ├── org-units-operate-drawer.vue │ │ │ ├── org-units-search.vue │ │ │ └── shared.ts │ ├── position │ │ ├── index.vue │ │ └── modules │ │ │ ├── position-operate-drawer.vue │ │ │ └── position-search.vue │ ├── role │ │ ├── index.vue │ │ └── modules │ │ │ ├── button-auth-modal.vue │ │ │ ├── menu-auth-modal.vue │ │ │ ├── role-operate-drawer.vue │ │ │ ├── role-search.vue │ │ │ └── shared.ts │ ├── user-detail │ │ └── [id].vue │ └── user │ │ ├── index.vue │ │ └── modules │ │ ├── org-utils-tree.vue │ │ ├── shared.ts │ │ ├── user-operate-drawer.vue │ │ ├── user-page-table.vue │ │ ├── user-responsibilities-modal.vue │ │ └── user-search.vue │ ├── monitor │ ├── cache │ │ └── index.vue │ ├── file │ │ ├── index.vue │ │ └── modules │ │ │ └── file-search.vue │ ├── logs_error │ │ └── index.vue │ ├── logs_login │ │ ├── index.vue │ │ └── modules │ │ │ └── login-search.vue │ ├── logs_operation │ │ ├── index.vue │ │ └── modules │ │ │ └── operation-search.vue │ ├── logs_scheduler │ │ ├── index.vue │ │ └── modules │ │ │ └── scheduler-search.vue │ ├── scheduler │ │ ├── index.vue │ │ └── modules │ │ │ ├── scheduler-operate-drawer.vue │ │ │ ├── scheduler-search.vue │ │ │ └── shared.ts │ └── system │ │ └── index.vue │ ├── multi-menu │ ├── first_child │ │ └── index.vue │ └── second_child_home │ │ └── index.vue │ ├── tools │ └── generate-table │ │ ├── index.vue │ │ └── modules │ │ ├── generate-table-operate-modal.vue │ │ ├── generate-table-search.vue │ │ └── shared.ts │ └── user-center │ └── index.vue ├── tsconfig.json ├── uno.config.ts └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | # backend service base url, prod environment 2 | VITE_SERVICE_BASE_URL=https://domain.com/api/ 3 | 4 | # other backend service base url, prod environment 5 | VITE_OTHER_SERVICE_BASE_URL= `{ 6 | "demo": "http://localhost:9529" 7 | }` 8 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # backend service base url, test environment 2 | VITE_SERVICE_BASE_URL=http://127.0.0.1:9999 3 | 4 | # other backend service base url, test environment 5 | VITE_OTHER_SERVICE_BASE_URL= `{ 6 | "demo": "http://localhost:9634" 7 | }` 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | "*.vue" eol=lf 2 | "*.js" eol=lf 3 | "*.ts" eol=lf 4 | "*.jsx" eol=lf 5 | "*.tsx" eol=lf 6 | "*.mjs" eol=lf 7 | "*.json" eol=lf 8 | "*.html" eol=lf 9 | "*.css" eol=lf 10 | "*.scss" eol=lf 11 | "*.md" eol=lf 12 | "*.yaml" eol=lf 13 | "*.yml" eol=lf 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | !.vscode/settings.json 24 | !.vscode/launch.json 25 | .idea 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | *.sw? 31 | 32 | package-lock.json 33 | yarn.lock 34 | 35 | .VSCodeCounter 36 | .env.prod 37 | .history 38 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com/ 2 | shamefully-hoist=true 3 | ignore-workspace-root-check=true 4 | link-workspace-packages=true 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "printWidth": 150, 7 | "trailingComma": "none" 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "afzalsayed96.icones", 4 | "antfu.iconify", 5 | "antfu.unocss", 6 | "dbaeumer.vscode-eslint", 7 | "editorconfig.editorconfig", 8 | "esbenp.prettier-vscode", 9 | "lokalise.i18n-ally", 10 | "mhutchie.git-graph", 11 | "mikestead.dotenv", 12 | "naumovs.color-highlight", 13 | "pkief.material-icon-theme", 14 | "sdras.vue-vscode-snippets", 15 | "vue.volar", 16 | "whtouche.vscode-js-console-utils", 17 | "zhuangtongfa.material-theme" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "chrome", 6 | "request": "launch", 7 | "name": "Vue Debugger", 8 | "url": "http://localhost:9527", 9 | "webRoot": "${workspaceFolder}" 10 | }, 11 | { 12 | "type": "node", 13 | "request": "launch", 14 | "name": "TS Debugger", 15 | "runtimeExecutable": "tsx", 16 | "skipFiles": ["/**", "${workspaceFolder}/node_modules/**"], 17 | "program": "${file}", 18 | "console": "integratedTerminal", 19 | "internalConsoleOptions": "neverOpen" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit", 4 | "source.organizeImports": "never" 5 | }, 6 | "editor.fontFamily": "MonoLisa-Regular", 7 | "editor.fontSize": 13, 8 | "editor.tabSize": 2, 9 | "editor.wordWrap": "wordWrapColumn", 10 | "editor.wordWrapColumn": 200, 11 | "editor.formatOnSave": false, 12 | "eslint.validate": ["html", "css", "scss", "json", "jsonc"], 13 | "i18n-ally.displayLanguage": "zh-cn", 14 | "i18n-ally.enabledParsers": ["ts"], 15 | "i18n-ally.enabledFrameworks": ["vue"], 16 | "i18n-ally.editor.preferEditor": true, 17 | "i18n-ally.keystyle": "nested", 18 | "i18n-ally.localesPaths": ["src/locales/langs"], 19 | "prettier.enable": false, 20 | "typescript.tsdk": "node_modules/typescript/lib", 21 | "unocss.root": ["./"], 22 | "vue.server.hybridMode": true 23 | } 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | COPY dist/ /usr/share/nginx/html/ 3 | COPY nginx/default.conf /etc/nginx/conf.d/default.conf 4 | EXPOSE 443 5 | -------------------------------------------------------------------------------- /build/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './proxy'; 2 | export * from './time'; 3 | -------------------------------------------------------------------------------- /build/config/proxy.ts: -------------------------------------------------------------------------------- 1 | import type { HttpProxy, ProxyOptions } from 'vite'; 2 | import { bgRed, bgYellow, green, lightBlue } from 'kolorist'; 3 | import { consola } from 'consola'; 4 | import { createServiceConfig } from '../../src/utils/service'; 5 | 6 | /** 7 | * Set http proxy 8 | * 9 | * @param env - The current env 10 | * @param enable - If enable http proxy 11 | */ 12 | export function createViteProxy(env: Env.ImportMeta, enable: boolean) { 13 | const isEnableHttpProxy = enable && env.VITE_HTTP_PROXY === 'Y'; 14 | 15 | if (!isEnableHttpProxy) return undefined; 16 | 17 | const isEnableProxyLog = env.VITE_PROXY_LOG === 'Y'; 18 | 19 | const { baseURL, proxyPattern, other } = createServiceConfig(env); 20 | 21 | const proxy: Record = createProxyItem({ baseURL, proxyPattern }, isEnableProxyLog); 22 | 23 | other.forEach(item => { 24 | Object.assign(proxy, createProxyItem(item, isEnableProxyLog)); 25 | }); 26 | 27 | return proxy; 28 | } 29 | 30 | function createProxyItem(item: App.Service.ServiceConfigItem, enableLog: boolean) { 31 | const proxy: Record = {}; 32 | 33 | proxy[item.proxyPattern] = { 34 | target: item.baseURL, 35 | changeOrigin: true, 36 | configure: (_proxy: HttpProxy.Server, options: ProxyOptions) => { 37 | _proxy.on('proxyReq', (_proxyReq, req, _res) => { 38 | if (!enableLog) return; 39 | 40 | const requestUrl = `${lightBlue('[proxy url]')}: ${bgYellow(` ${req.method} `)} ${green(`${item.proxyPattern}${req.url}`)}`; 41 | 42 | const proxyUrl = `${lightBlue('[real request url]')}: ${green(`${options.target}${req.url}`)}`; 43 | 44 | consola.log(`${requestUrl}\n${proxyUrl}`); 45 | }); 46 | _proxy.on('error', (_err, req, _res) => { 47 | if (!enableLog) return; 48 | consola.log(bgRed(`Error: ${req.method} `), green(`${options.target}${req.url}`)); 49 | }); 50 | }, 51 | rewrite: path => path.replace(new RegExp(`^${item.proxyPattern}`), '') 52 | }; 53 | 54 | return proxy; 55 | } 56 | -------------------------------------------------------------------------------- /build/config/time.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import utc from 'dayjs/plugin/utc'; 3 | import timezone from 'dayjs/plugin/timezone'; 4 | 5 | export function getBuildTime() { 6 | dayjs.extend(utc); 7 | dayjs.extend(timezone); 8 | 9 | const buildTime = dayjs.tz(Date.now(), 'Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss'); 10 | 11 | return buildTime; 12 | } 13 | -------------------------------------------------------------------------------- /build/plugins/devtools.ts: -------------------------------------------------------------------------------- 1 | import VueDevtools from 'vite-plugin-vue-devtools'; 2 | 3 | export function setupDevtoolsPlugin(viteEnv: Env.ImportMeta) { 4 | const { VITE_DEVTOOLS_LAUNCH_EDITOR } = viteEnv; 5 | 6 | return VueDevtools({ 7 | launchEditor: VITE_DEVTOOLS_LAUNCH_EDITOR 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /build/plugins/html.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vite'; 2 | 3 | export function setupHtmlPlugin(buildTime: string) { 4 | const plugin: Plugin = { 5 | name: 'html-plugin', 6 | apply: 'build', 7 | transformIndexHtml(html) { 8 | return html.replace('', `\n `); 9 | } 10 | }; 11 | 12 | return plugin; 13 | } 14 | -------------------------------------------------------------------------------- /build/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import type { PluginOption } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | import vueJsx from '@vitejs/plugin-vue-jsx'; 4 | import progress from 'vite-plugin-progress'; 5 | import { setupElegantRouter } from './router'; 6 | import { setupUnocss } from './unocss'; 7 | import { setupUnplugin } from './unplugin'; 8 | import { setupHtmlPlugin } from './html'; 9 | import { setupDevtoolsPlugin } from './devtools'; 10 | 11 | export function setupVitePlugins(viteEnv: Env.ImportMeta, buildTime: string) { 12 | const plugins: PluginOption = [ 13 | vue(), 14 | vueJsx(), 15 | setupDevtoolsPlugin(viteEnv), 16 | setupElegantRouter(), 17 | setupUnocss(viteEnv), 18 | ...setupUnplugin(viteEnv), 19 | progress(), 20 | setupHtmlPlugin(buildTime) 21 | ]; 22 | 23 | return plugins; 24 | } 25 | -------------------------------------------------------------------------------- /build/plugins/router.ts: -------------------------------------------------------------------------------- 1 | import type { RouteMeta } from 'vue-router'; 2 | import ElegantVueRouter from '@elegant-router/vue/vite'; 3 | import type { RouteKey } from '@elegant-router/types'; 4 | 5 | export function setupElegantRouter() { 6 | return ElegantVueRouter({ 7 | layouts: { 8 | base: 'src/layouts/base-layout/index.vue', 9 | blank: 'src/layouts/blank-layout/index.vue' 10 | }, 11 | customRoutes: { 12 | names: [ 13 | 'exception_403', 14 | 'exception_404', 15 | 'exception_500', 16 | 'document_project', 17 | 'document_project-link', 18 | 'document_vue', 19 | 'document_vite', 20 | 'document_unocss', 21 | 'document_naive', 22 | 'document_antd' 23 | ] 24 | }, 25 | routePathTransformer(routeName, routePath) { 26 | const key = routeName as RouteKey; 27 | 28 | if (key === 'login') { 29 | const modules: UnionKey.LoginModule[] = ['pwd-login', 'code-login', 'register', 'reset-pwd', 'bind-wechat']; 30 | 31 | const moduleReg = modules.join('|'); 32 | 33 | return `/login/:module(${moduleReg})?`; 34 | } 35 | 36 | return routePath; 37 | }, 38 | onRouteMetaGen(routeName) { 39 | const key = routeName as RouteKey; 40 | 41 | const constantRoutes: RouteKey[] = ['login', '403', '404', '500']; 42 | 43 | const meta: Partial = { 44 | title: key, 45 | i18nKey: `route.${key}` as App.I18n.I18nKey 46 | }; 47 | 48 | if (constantRoutes.includes(key)) { 49 | meta.constant = true; 50 | } 51 | 52 | return meta; 53 | } 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /build/plugins/unocss.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import path from 'node:path'; 3 | import unocss from '@unocss/vite'; 4 | import presetIcons from '@unocss/preset-icons'; 5 | import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders'; 6 | 7 | export function setupUnocss(viteEnv: Env.ImportMeta) { 8 | const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv; 9 | 10 | const localIconPath = path.join(process.cwd(), 'src/assets/svg-icon'); 11 | 12 | /** The name of the local icon collection */ 13 | const collectionName = VITE_ICON_LOCAL_PREFIX.replace(`${VITE_ICON_PREFIX}-`, ''); 14 | 15 | return unocss({ 16 | presets: [ 17 | presetIcons({ 18 | prefix: `${VITE_ICON_PREFIX}-`, 19 | scale: 1, 20 | extraProperties: { 21 | display: 'inline-block' 22 | }, 23 | collections: { 24 | [collectionName]: FileSystemIconLoader(localIconPath, svg => svg.replace(/^ svg.replace(/^ 2 | 3 | 4 | 5 | 6 | 7 | 8 | %VITE_APP_TITLE% 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/alova/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa/alova", 3 | "version": "1.0.5", 4 | "exports": { 5 | ".": "./src/index.ts", 6 | "./fetch": "./src/fetch.ts", 7 | "./client": "./src/client.ts", 8 | "./mock": "./src/mock.ts" 9 | }, 10 | "typesVersions": { 11 | "*": { 12 | "*": ["./src/*"] 13 | } 14 | }, 15 | "dependencies": { 16 | "@alova/mock": "2.0.12", 17 | "@sa/utils": "workspace:*", 18 | "alova": "3.2.10" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/alova/src/client.ts: -------------------------------------------------------------------------------- 1 | export * from 'alova/client'; 2 | -------------------------------------------------------------------------------- /packages/alova/src/constant.ts: -------------------------------------------------------------------------------- 1 | /** the backend error code key */ 2 | export const BACKEND_ERROR_CODE = 'BACKEND_ERROR'; 3 | -------------------------------------------------------------------------------- /packages/alova/src/fetch.ts: -------------------------------------------------------------------------------- 1 | import adapterFetch from 'alova/fetch'; 2 | export default adapterFetch; 3 | -------------------------------------------------------------------------------- /packages/alova/src/mock.ts: -------------------------------------------------------------------------------- 1 | export * from '@alova/mock'; 2 | -------------------------------------------------------------------------------- /packages/alova/src/type.ts: -------------------------------------------------------------------------------- 1 | import type { AlovaGenerics, AlovaOptions, AlovaRequestAdapter, Method, ResponseCompleteHandler } from 'alova'; 2 | 3 | export type CustomAlovaConfig = Omit, 'statesHook' | 'beforeRequest' | 'responded' | 'requestAdapter'> & { 4 | /** request adapter. all request of alova will be sent by it. */ 5 | requestAdapter?: AlovaRequestAdapter; 6 | }; 7 | 8 | export interface RequestOptions { 9 | /** 10 | * The hook before request 11 | * 12 | * For example: You can add header token in this hook 13 | * 14 | * @param method alova Method Instance 15 | */ 16 | onRequest?: AlovaOptions['beforeRequest']; 17 | /** 18 | * The hook to check backend response is success or not 19 | * 20 | * @param response alova response 21 | */ 22 | isBackendSuccess: (response: AG['Response']) => Promise; 23 | 24 | /** The config to refresh token */ 25 | tokenRefresher?: { 26 | /** detect the token is expired */ 27 | isExpired(response: AG['Response'], Method: Method): Promise | boolean; 28 | /** refresh token handler */ 29 | handler(response: AG['Response'], Method: Method): Promise; 30 | }; 31 | 32 | /** The hook after backend request complete */ 33 | onComplete?: ResponseCompleteHandler; 34 | 35 | /** 36 | * The hook to handle error 37 | * 38 | * For example: You can show error message in this hook 39 | * 40 | * @param error 41 | */ 42 | onError?: (error: any, response: AG['Response'] | null, methodInstance: Method) => any | Promise; 43 | /** 44 | * transform backend response when the responseType is json 45 | * 46 | * @param response alova response 47 | */ 48 | transformBackendResponse: (response: AG['Response']) => any; 49 | } 50 | -------------------------------------------------------------------------------- /packages/alova/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "preserve", 5 | "lib": ["DOM", "ESNext"], 6 | "baseUrl": ".", 7 | "module": "ESNext", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "types": ["node"], 11 | "strict": true, 12 | "strictNullChecks": true, 13 | "noUnusedLocals": true, 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/axios/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa/axios", 3 | "version": "1.0.5", 4 | "exports": { 5 | ".": "./src/index.ts" 6 | }, 7 | "typesVersions": { 8 | "*": { 9 | "*": ["./src/*"] 10 | } 11 | }, 12 | "dependencies": { 13 | "@sa/utils": "workspace:*", 14 | "axios": "1.8.3", 15 | "axios-retry": "4.5.0", 16 | "qs": "6.14.0" 17 | }, 18 | "devDependencies": { 19 | "@types/qs": "6.9.18" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/axios/src/constant.ts: -------------------------------------------------------------------------------- 1 | /** request id key */ 2 | export const REQUEST_ID_KEY = 'P-Request-Id'; 3 | 4 | /** request language key */ 5 | export const REQUEST_LANGUAGE = 'P-Language'; 6 | 7 | /** the backend error code key */ 8 | export const BACKEND_ERROR_CODE = 'BACKEND_ERROR'; 9 | -------------------------------------------------------------------------------- /packages/axios/src/options.ts: -------------------------------------------------------------------------------- 1 | import type { CreateAxiosDefaults } from 'axios'; 2 | import type { IAxiosRetryConfig } from 'axios-retry'; 3 | import { stringify } from 'qs'; 4 | import { isHttpSuccess } from './shared'; 5 | import type { RequestOption } from './type'; 6 | 7 | export function createDefaultOptions(options?: Partial>) { 8 | const opts: RequestOption = { 9 | onRequest: async config => config, 10 | isBackendSuccess: _response => true, 11 | onBackendFail: async () => {}, 12 | transformBackendResponse: async response => response.data, 13 | onError: async () => {} 14 | }; 15 | 16 | Object.assign(opts, options); 17 | 18 | return opts; 19 | } 20 | 21 | export function createRetryOptions(config?: Partial) { 22 | const retryConfig: IAxiosRetryConfig = { 23 | retries: 0 24 | }; 25 | 26 | Object.assign(retryConfig, config); 27 | 28 | return retryConfig; 29 | } 30 | 31 | export function createAxiosConfig(config?: Partial) { 32 | const TEN_SECONDS = 10 * 1000; 33 | 34 | const axiosConfig: CreateAxiosDefaults = { 35 | timeout: TEN_SECONDS, 36 | headers: { 37 | 'Content-Type': 'application/json' 38 | }, 39 | validateStatus: isHttpSuccess, 40 | paramsSerializer: params => { 41 | return stringify(params); 42 | } 43 | }; 44 | 45 | Object.assign(axiosConfig, config); 46 | 47 | return axiosConfig; 48 | } 49 | -------------------------------------------------------------------------------- /packages/axios/src/shared.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosHeaderValue, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; 2 | 3 | export function getContentType(config: InternalAxiosRequestConfig) { 4 | const contentType: AxiosHeaderValue = config.headers?.['Content-Type'] || 'application/json'; 5 | 6 | return contentType; 7 | } 8 | 9 | /** 10 | * check if http status is success 11 | * 12 | * @param status 13 | */ 14 | export function isHttpSuccess(status: number) { 15 | const isSuccessCode = status >= 200 && status < 300; 16 | return isSuccessCode || status === 304; 17 | } 18 | 19 | /** 20 | * is response json 21 | * 22 | * @param response axios response 23 | */ 24 | export function isResponseJson(response: AxiosResponse) { 25 | const { responseType } = response.config; 26 | 27 | return responseType === 'json' || responseType === undefined; 28 | } 29 | -------------------------------------------------------------------------------- /packages/axios/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "preserve", 5 | "lib": ["DOM", "ESNext"], 6 | "baseUrl": ".", 7 | "module": "ESNext", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "types": ["node"], 11 | "strict": true, 12 | "strictNullChecks": true, 13 | "noUnusedLocals": true, 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/color/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa/color", 3 | "version": "1.0.5", 4 | "exports": { 5 | ".": "./src/index.ts" 6 | }, 7 | "typesVersions": { 8 | "*": { 9 | "*": ["./src/*"] 10 | } 11 | }, 12 | "dependencies": { 13 | "@sa/utils": "workspace:*", 14 | "colord": "2.9.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/color/src/constant/index.ts: -------------------------------------------------------------------------------- 1 | export * from './name'; 2 | export * from './palette'; 3 | -------------------------------------------------------------------------------- /packages/color/src/index.ts: -------------------------------------------------------------------------------- 1 | import { colorPalettes } from './constant'; 2 | 3 | export * from './palette'; 4 | export * from './shared'; 5 | export { colorPalettes }; 6 | 7 | export * from './types'; 8 | -------------------------------------------------------------------------------- /packages/color/src/palette/index.ts: -------------------------------------------------------------------------------- 1 | import type { AnyColor } from 'colord'; 2 | import { getHex } from '../shared'; 3 | import type { ColorPaletteNumber } from '../types'; 4 | import { getRecommendedColorPalette } from './recommend'; 5 | import { getAntDColorPalette } from './antd'; 6 | 7 | /** 8 | * get color palette by provided color 9 | * 10 | * @param color 11 | * @param recommended whether to get recommended color palette (the provided color may not be the main color) 12 | */ 13 | export function getColorPalette(color: AnyColor, recommended = false) { 14 | const colorMap = new Map(); 15 | 16 | if (recommended) { 17 | const colorPalette = getRecommendedColorPalette(getHex(color)); 18 | colorPalette.palettes.forEach(palette => { 19 | colorMap.set(palette.number, palette.hex); 20 | }); 21 | } else { 22 | const colors = getAntDColorPalette(color); 23 | 24 | const colorNumbers: ColorPaletteNumber[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]; 25 | 26 | colorNumbers.forEach((number, index) => { 27 | colorMap.set(number, colors[index]); 28 | }); 29 | } 30 | 31 | return colorMap; 32 | } 33 | 34 | /** 35 | * get color palette color by number 36 | * 37 | * @param color the provided color 38 | * @param number the color palette number 39 | * @param recommended whether to get recommended color palette (the provided color may not be the main color) 40 | */ 41 | export function getPaletteColorByNumber(color: AnyColor, number: ColorPaletteNumber, recommended = false) { 42 | const colorMap = getColorPalette(color, recommended); 43 | 44 | return colorMap.get(number as ColorPaletteNumber)!; 45 | } 46 | -------------------------------------------------------------------------------- /packages/color/src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './colord'; 2 | export * from './name'; 3 | -------------------------------------------------------------------------------- /packages/color/src/shared/name.ts: -------------------------------------------------------------------------------- 1 | import { colorNames } from '../constant'; 2 | import { getHex, getHsl, getRgb } from './colord'; 3 | 4 | /** 5 | * Get color name 6 | * 7 | * @param color 8 | */ 9 | export function getColorName(color: string) { 10 | const hex = getHex(color); 11 | const rgb = getRgb(color); 12 | const hsl = getHsl(color); 13 | 14 | let ndf = 0; 15 | let ndf1 = 0; 16 | let ndf2 = 0; 17 | let cl = -1; 18 | let df = -1; 19 | 20 | let name = ''; 21 | 22 | colorNames.some((item, index) => { 23 | const [hexValue, colorName] = item; 24 | 25 | const match = hex === hexValue; 26 | 27 | if (match) { 28 | name = colorName; 29 | } else { 30 | const { r, g, b } = getRgb(hexValue); 31 | const { h, s, l } = getHsl(hexValue); 32 | 33 | ndf1 = (rgb.r - r) ** 2 + (rgb.g - g) ** 2 + (rgb.b - b) ** 2; 34 | ndf2 = (hsl.h - h) ** 2 + (hsl.s - s) ** 2 + (hsl.l - l) ** 2; 35 | 36 | ndf = ndf1 + ndf2 * 2; 37 | if (df < 0 || df > ndf) { 38 | df = ndf; 39 | cl = index; 40 | } 41 | } 42 | 43 | return match; 44 | }); 45 | 46 | name = colorNames[cl][1]; 47 | 48 | return name; 49 | } 50 | -------------------------------------------------------------------------------- /packages/color/src/types/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * the color palette number 3 | * 4 | * the main color number is 500 5 | */ 6 | export type ColorPaletteNumber = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950; 7 | 8 | /** the color palette */ 9 | export type ColorPalette = { 10 | /** the color hex value */ 11 | hex: string; 12 | /** 13 | * the color number 14 | * 15 | * - 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950 16 | */ 17 | number: ColorPaletteNumber; 18 | }; 19 | 20 | /** the color palette family */ 21 | export type ColorPaletteFamily = { 22 | /** the color palette family name */ 23 | name: string; 24 | /** the color palettes */ 25 | palettes: ColorPalette[]; 26 | }; 27 | 28 | /** the color palette with delta */ 29 | export type ColorPaletteWithDelta = ColorPalette & { 30 | delta: number; 31 | }; 32 | 33 | /** the color palette family with nearest palette */ 34 | export type ColorPaletteFamilyWithNearestPalette = ColorPaletteFamily & { 35 | nearestPalette: ColorPaletteWithDelta; 36 | nearestLightnessPalette: ColorPaletteWithDelta; 37 | }; 38 | 39 | /** the color palette match */ 40 | export type ColorPaletteMatch = ColorPaletteFamily & { 41 | /** the color map of the palette */ 42 | colorMap: Map; 43 | /** 44 | * the main color of the palette 45 | * 46 | * which number is 500 47 | */ 48 | main: ColorPalette; 49 | /** the match color of the palette */ 50 | match: ColorPalette; 51 | }; 52 | 53 | /** 54 | * The color index of color palette 55 | * 56 | * From left to right, the color is from light to dark, 6 is main color 57 | */ 58 | export type ColorIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11; 59 | -------------------------------------------------------------------------------- /packages/color/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "preserve", 5 | "lib": ["DOM", "ESNext"], 6 | "baseUrl": ".", 7 | "module": "ESNext", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "types": ["node"], 11 | "strict": true, 12 | "strictNullChecks": true, 13 | "noUnusedLocals": true, 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa/hooks", 3 | "version": "1.0.5", 4 | "exports": { 5 | ".": "./src/index.ts" 6 | }, 7 | "typesVersions": { 8 | "*": { 9 | "*": ["./src/*"] 10 | } 11 | }, 12 | "dependencies": { 13 | "@sa/axios": "workspace:*", 14 | "@sa/utils": "workspace:*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/hooks/src/index.ts: -------------------------------------------------------------------------------- 1 | import useBoolean from './use-boolean'; 2 | import useLoading from './use-loading'; 3 | import useCountDown from './use-count-down'; 4 | import useContext from './use-context'; 5 | import useSvgIconRender from './use-svg-icon-render'; 6 | import useHookTable from './use-table'; 7 | 8 | export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useHookTable }; 9 | 10 | export * from './use-signal'; 11 | export * from './use-table'; 12 | -------------------------------------------------------------------------------- /packages/hooks/src/use-boolean.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | 3 | /** 4 | * Boolean 5 | * 6 | * @param initValue Init value 7 | */ 8 | export default function useBoolean(initValue = false) { 9 | const bool = ref(initValue); 10 | 11 | function setBool(value: boolean) { 12 | bool.value = value; 13 | } 14 | function setTrue() { 15 | setBool(true); 16 | } 17 | function setFalse() { 18 | setBool(false); 19 | } 20 | function toggle() { 21 | setBool(!bool.value); 22 | } 23 | 24 | return { 25 | bool, 26 | setBool, 27 | setTrue, 28 | setFalse, 29 | toggle 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /packages/hooks/src/use-count-down.ts: -------------------------------------------------------------------------------- 1 | import { computed, onScopeDispose, ref } from 'vue'; 2 | import { useRafFn } from '@vueuse/core'; 3 | 4 | /** 5 | * count down 6 | * 7 | * @param seconds - count down seconds 8 | */ 9 | export default function useCountDown(seconds: number) { 10 | const FPS_PER_SECOND = 60; 11 | 12 | const fps = ref(0); 13 | 14 | const count = computed(() => Math.ceil(fps.value / FPS_PER_SECOND)); 15 | 16 | const isCounting = computed(() => fps.value > 0); 17 | 18 | const { pause, resume } = useRafFn( 19 | () => { 20 | if (fps.value > 0) { 21 | fps.value -= 1; 22 | } else { 23 | pause(); 24 | } 25 | }, 26 | { immediate: false } 27 | ); 28 | 29 | function start(updateSeconds: number = seconds) { 30 | fps.value = FPS_PER_SECOND * updateSeconds; 31 | resume(); 32 | } 33 | 34 | function stop() { 35 | fps.value = 0; 36 | pause(); 37 | } 38 | 39 | onScopeDispose(() => { 40 | pause(); 41 | }); 42 | 43 | return { 44 | count, 45 | isCounting, 46 | start, 47 | stop 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /packages/hooks/src/use-loading.ts: -------------------------------------------------------------------------------- 1 | import useBoolean from './use-boolean'; 2 | 3 | /** 4 | * Loading 5 | * 6 | * @param initValue Init value 7 | */ 8 | export default function useLoading(initValue = false) { 9 | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(initValue); 10 | 11 | return { 12 | loading, 13 | startLoading, 14 | endLoading 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /packages/hooks/src/use-svg-icon-render.ts: -------------------------------------------------------------------------------- 1 | import { h } from 'vue'; 2 | import type { Component } from 'vue'; 3 | 4 | /** 5 | * Svg icon render hook 6 | * 7 | * @param SvgIcon Svg icon component 8 | */ 9 | export default function useSvgIconRender(SvgIcon: Component) { 10 | interface IconConfig { 11 | /** Iconify icon name */ 12 | icon?: string; 13 | /** Local icon name */ 14 | localIcon?: string; 15 | /** Icon color */ 16 | color?: string; 17 | /** Icon size */ 18 | fontSize?: number; 19 | } 20 | 21 | type IconStyle = Partial>; 22 | 23 | /** 24 | * Svg icon VNode 25 | * 26 | * @param config 27 | */ 28 | const SvgIconVNode = (config: IconConfig) => { 29 | const { color, fontSize, icon, localIcon } = config; 30 | 31 | const style: IconStyle = {}; 32 | 33 | if (color) { 34 | style.color = color; 35 | } 36 | if (fontSize) { 37 | style.fontSize = `${fontSize}px`; 38 | } 39 | 40 | if (!icon && !localIcon) { 41 | return undefined; 42 | } 43 | 44 | return () => h(SvgIcon, { icon, localIcon, style }); 45 | }; 46 | 47 | return { 48 | SvgIconVNode 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /packages/hooks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "preserve", 5 | "lib": ["DOM", "ESNext"], 6 | "baseUrl": ".", 7 | "module": "ESNext", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "types": ["node"], 11 | "strict": true, 12 | "strictNullChecks": true, 13 | "noUnusedLocals": true, 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/materials/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa/materials", 3 | "version": "1.0.5", 4 | "exports": { 5 | ".": "./src/index.ts" 6 | }, 7 | "typesVersions": { 8 | "*": { 9 | "*": ["./src/*"] 10 | } 11 | }, 12 | "dependencies": { 13 | "@sa/utils": "workspace:*", 14 | "simplebar-vue": "2.4.0" 15 | }, 16 | "devDependencies": { 17 | "typed-css-modules": "0.9.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/materials/src/index.ts: -------------------------------------------------------------------------------- 1 | import AdminLayout, { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './libs/admin-layout'; 2 | import PageTab from './libs/page-tab'; 3 | import SimpleScrollbar from './libs/simple-scrollbar'; 4 | 5 | export { AdminLayout, LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX, PageTab, SimpleScrollbar }; 6 | export * from './types'; 7 | -------------------------------------------------------------------------------- /packages/materials/src/libs/admin-layout/index.module.css: -------------------------------------------------------------------------------- 1 | /* @type */ 2 | 3 | .layout-header, 4 | .layout-header-placement { 5 | height: var(--soy-header-height); 6 | } 7 | 8 | .layout-header { 9 | z-index: var(--soy-header-z-index); 10 | } 11 | 12 | .layout-tab { 13 | top: var(--soy-header-height); 14 | height: var(--soy-tab-height); 15 | z-index: var(--soy-tab-z-index); 16 | } 17 | 18 | .layout-tab-placement { 19 | height: var(--soy-tab-height); 20 | } 21 | 22 | .layout-sider { 23 | width: var(--soy-sider-width); 24 | z-index: var(--soy-sider-z-index); 25 | } 26 | 27 | .layout-mobile-sider { 28 | z-index: var(--soy-sider-z-index); 29 | } 30 | 31 | .layout-mobile-sider-mask { 32 | z-index: var(--soy-mobile-sider-z-index); 33 | } 34 | 35 | .layout-sider_collapsed { 36 | width: var(--soy-sider-collapsed-width); 37 | z-index: var(--soy-sider-z-index); 38 | } 39 | 40 | .layout-footer, 41 | .layout-footer-placement { 42 | height: var(--soy-footer-height); 43 | } 44 | 45 | .layout-footer { 46 | z-index: var(--soy-footer-z-index); 47 | } 48 | 49 | .left-gap { 50 | padding-left: var(--soy-sider-width); 51 | } 52 | 53 | .left-gap_collapsed { 54 | padding-left: var(--soy-sider-collapsed-width); 55 | } 56 | 57 | .sider-padding-top { 58 | padding-top: var(--soy-header-height); 59 | } 60 | 61 | .sider-padding-bottom { 62 | padding-bottom: var(--soy-footer-height); 63 | } 64 | -------------------------------------------------------------------------------- /packages/materials/src/libs/admin-layout/index.module.css.d.ts: -------------------------------------------------------------------------------- 1 | declare const styles: { 2 | readonly 'layout-header': string; 3 | readonly 'layout-header-placement': string; 4 | readonly 'layout-tab': string; 5 | readonly 'layout-tab-placement': string; 6 | readonly 'layout-sider': string; 7 | readonly 'layout-mobile-sider': string; 8 | readonly 'layout-mobile-sider-mask': string; 9 | readonly 'layout-sider_collapsed': string; 10 | readonly 'layout-footer': string; 11 | readonly 'layout-footer-placement': string; 12 | readonly 'left-gap': string; 13 | readonly 'left-gap_collapsed': string; 14 | readonly 'sider-padding-top': string; 15 | readonly 'sider-padding-bottom': string; 16 | }; 17 | 18 | export default styles; 19 | -------------------------------------------------------------------------------- /packages/materials/src/libs/admin-layout/index.ts: -------------------------------------------------------------------------------- 1 | import AdminLayout from './index.vue'; 2 | import { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './shared'; 3 | 4 | export default AdminLayout; 5 | export { LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX }; 6 | -------------------------------------------------------------------------------- /packages/materials/src/libs/admin-layout/shared.ts: -------------------------------------------------------------------------------- 1 | import type { AdminLayoutProps, LayoutCssVars, LayoutCssVarsProps } from '../../types'; 2 | 3 | /** The id of the scroll element of the layout */ 4 | export const LAYOUT_SCROLL_EL_ID = '__SCROLL_EL_ID__'; 5 | 6 | /** The max z-index of the layout */ 7 | export const LAYOUT_MAX_Z_INDEX = 100; 8 | 9 | /** 10 | * Create layout css vars by css vars props 11 | * 12 | * @param props Css vars props 13 | */ 14 | function createLayoutCssVarsByCssVarsProps(props: LayoutCssVarsProps) { 15 | const cssVars: LayoutCssVars = { 16 | '--soy-header-height': `${props.headerHeight}px`, 17 | '--soy-header-z-index': props.headerZIndex, 18 | '--soy-tab-height': `${props.tabHeight}px`, 19 | '--soy-tab-z-index': props.tabZIndex, 20 | '--soy-sider-width': `${props.siderWidth}px`, 21 | '--soy-sider-collapsed-width': `${props.siderCollapsedWidth}px`, 22 | '--soy-sider-z-index': props.siderZIndex, 23 | '--soy-mobile-sider-z-index': props.mobileSiderZIndex, 24 | '--soy-footer-height': `${props.footerHeight}px`, 25 | '--soy-footer-z-index': props.footerZIndex 26 | }; 27 | 28 | return cssVars; 29 | } 30 | 31 | /** 32 | * Create layout css vars 33 | * 34 | * @param props 35 | */ 36 | export function createLayoutCssVars(props: AdminLayoutProps) { 37 | const { mode, isMobile, maxZIndex = LAYOUT_MAX_Z_INDEX, headerHeight, tabHeight, siderWidth, siderCollapsedWidth, footerHeight } = props; 38 | 39 | const headerZIndex = maxZIndex - 3; 40 | const tabZIndex = maxZIndex - 5; 41 | const siderZIndex = mode === 'vertical' || isMobile ? maxZIndex - 1 : maxZIndex - 4; 42 | const mobileSiderZIndex = isMobile ? maxZIndex - 2 : 0; 43 | const footerZIndex = maxZIndex - 5; 44 | 45 | const cssProps: LayoutCssVarsProps = { 46 | headerHeight, 47 | headerZIndex, 48 | tabHeight, 49 | tabZIndex, 50 | siderWidth, 51 | siderZIndex, 52 | mobileSiderZIndex, 53 | siderCollapsedWidth, 54 | footerHeight, 55 | footerZIndex 56 | }; 57 | 58 | return createLayoutCssVarsByCssVarsProps(cssProps); 59 | } 60 | -------------------------------------------------------------------------------- /packages/materials/src/libs/page-tab/button-tab.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /packages/materials/src/libs/page-tab/chrome-tab-bg.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /packages/materials/src/libs/page-tab/chrome-tab.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /packages/materials/src/libs/page-tab/index.module.css.d.ts: -------------------------------------------------------------------------------- 1 | declare const styles: { 2 | readonly 'button-tab': string; 3 | readonly 'button-tab_dark': string; 4 | readonly 'button-tab_active': string; 5 | readonly 'button-tab_active_dark': string; 6 | readonly 'chrome-tab': string; 7 | readonly 'chrome-tab_active': string; 8 | readonly 'chrome-tab__bg': string; 9 | readonly 'chrome-tab_active_dark': string; 10 | readonly 'chrome-tab_dark': string; 11 | readonly 'chrome-tab-divider': string; 12 | readonly 'svg-close': string; 13 | }; 14 | 15 | export default styles; 16 | -------------------------------------------------------------------------------- /packages/materials/src/libs/page-tab/index.ts: -------------------------------------------------------------------------------- 1 | import PageTab from './index.vue'; 2 | 3 | export default PageTab; 4 | -------------------------------------------------------------------------------- /packages/materials/src/libs/page-tab/shared.ts: -------------------------------------------------------------------------------- 1 | import { addColorAlpha, transformColorWithOpacity } from '@sa/color'; 2 | import type { PageTabCssVars, PageTabCssVarsProps } from '../../types'; 3 | 4 | /** The active color of the tab */ 5 | export const ACTIVE_COLOR = '#1890ff'; 6 | 7 | function createCssVars(props: PageTabCssVarsProps) { 8 | const cssVars: PageTabCssVars = { 9 | '--soy-primary-color': props.primaryColor, 10 | '--soy-primary-color1': props.primaryColor1, 11 | '--soy-primary-color2': props.primaryColor2, 12 | '--soy-primary-color-opacity1': props.primaryColorOpacity1, 13 | '--soy-primary-color-opacity2': props.primaryColorOpacity2, 14 | '--soy-primary-color-opacity3': props.primaryColorOpacity3 15 | }; 16 | 17 | return cssVars; 18 | } 19 | 20 | export function createTabCssVars(primaryColor: string) { 21 | const cssProps: PageTabCssVarsProps = { 22 | primaryColor, 23 | primaryColor1: transformColorWithOpacity(primaryColor, 0.1, '#ffffff'), 24 | primaryColor2: transformColorWithOpacity(primaryColor, 0.3, '#000000'), 25 | primaryColorOpacity1: addColorAlpha(primaryColor, 0.1), 26 | primaryColorOpacity2: addColorAlpha(primaryColor, 0.15), 27 | primaryColorOpacity3: addColorAlpha(primaryColor, 0.3) 28 | }; 29 | 30 | return createCssVars(cssProps); 31 | } 32 | -------------------------------------------------------------------------------- /packages/materials/src/libs/page-tab/svg-close.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/materials/src/libs/simple-scrollbar/index.ts: -------------------------------------------------------------------------------- 1 | import SimpleScrollbar from './index.vue'; 2 | 3 | export default SimpleScrollbar; 4 | -------------------------------------------------------------------------------- /packages/materials/src/libs/simple-scrollbar/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/materials/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "preserve", 5 | "lib": ["DOM", "ESNext"], 6 | "baseUrl": ".", 7 | "module": "ESNext", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "types": ["node"], 11 | "strict": true, 12 | "strictNullChecks": true, 13 | "noUnusedLocals": true, 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/ofetch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa/fetch", 3 | "version": "1.0.5", 4 | "exports": { 5 | ".": "./src/index.ts" 6 | }, 7 | "typesVersions": { 8 | "*": { 9 | "*": ["./src/*"] 10 | } 11 | }, 12 | "dependencies": { 13 | "ofetch": "1.4.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/ofetch/src/index.ts: -------------------------------------------------------------------------------- 1 | import { ofetch } from 'ofetch'; 2 | import type { FetchOptions } from 'ofetch'; 3 | 4 | export function createRequest(options: FetchOptions) { 5 | const request = ofetch.create(options); 6 | 7 | return request; 8 | } 9 | 10 | export default createRequest; 11 | -------------------------------------------------------------------------------- /packages/ofetch/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "preserve", 5 | "lib": ["DOM", "ESNext"], 6 | "baseUrl": ".", 7 | "module": "ESNext", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "types": ["node"], 11 | "strict": true, 12 | "strictNullChecks": true, 13 | "noUnusedLocals": true, 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/scripts/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tsx 2 | 3 | import './src/index.ts'; 4 | -------------------------------------------------------------------------------- /packages/scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa/scripts", 3 | "version": "1.0.5", 4 | "bin": { 5 | "sa": "./bin.ts" 6 | }, 7 | "exports": { 8 | ".": "./src/index.ts" 9 | }, 10 | "typesVersions": { 11 | "*": { 12 | "*": ["./src/*"] 13 | } 14 | }, 15 | "devDependencies": { 16 | "@soybeanjs/changelog": "0.3.24", 17 | "bumpp": "10.1.0", 18 | "c12": "3.0.2", 19 | "cac": "6.7.14", 20 | "consola": "3.4.2", 21 | "enquirer": "2.4.1", 22 | "execa": "9.5.2", 23 | "kolorist": "1.8.0", 24 | "npm-check-updates": "17.1.15", 25 | "rimraf": "6.0.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/scripts/src/commands/changelog.ts: -------------------------------------------------------------------------------- 1 | import { generateChangelog, generateTotalChangelog } from '@soybeanjs/changelog'; 2 | import type { ChangelogOption } from '@soybeanjs/changelog'; 3 | 4 | export async function genChangelog(options?: Partial, total = false) { 5 | if (total) { 6 | await generateTotalChangelog(options); 7 | } else { 8 | await generateChangelog(options); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/scripts/src/commands/cleanup.ts: -------------------------------------------------------------------------------- 1 | import { rimraf } from 'rimraf'; 2 | 3 | export async function cleanup(paths: string[]) { 4 | await rimraf(paths, { glob: true }); 5 | } 6 | -------------------------------------------------------------------------------- /packages/scripts/src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from './git-commit'; 2 | export * from './cleanup'; 3 | export * from './update-pkg'; 4 | export * from './changelog'; 5 | export * from './release'; 6 | export * from './router'; 7 | -------------------------------------------------------------------------------- /packages/scripts/src/commands/release.ts: -------------------------------------------------------------------------------- 1 | import { versionBump } from 'bumpp'; 2 | 3 | export async function release(execute = 'pnpm sa changelog', push = true) { 4 | await versionBump({ 5 | files: ['**/package.json', '!**/node_modules'], 6 | execute, 7 | all: true, 8 | tag: true, 9 | commit: 'chore(projects): release v%s', 10 | push 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /packages/scripts/src/commands/update-pkg.ts: -------------------------------------------------------------------------------- 1 | import { execCommand } from '../shared'; 2 | 3 | export async function updatePkg(args: string[] = ['--deep', '-u']) { 4 | execCommand('npx', ['ncu', ...args], { stdio: 'inherit' }); 5 | } 6 | -------------------------------------------------------------------------------- /packages/scripts/src/config/index.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import { loadConfig } from 'c12'; 3 | import type { CliOption } from '../types'; 4 | 5 | const defaultOptions: CliOption = { 6 | cwd: process.cwd(), 7 | cleanupDirs: ['**/dist', '**/package-lock.json', '**/yarn.lock', '**/pnpm-lock.yaml', '**/node_modules', '!node_modules/**'], 8 | ncuCommandArgs: ['--deep', '-u'], 9 | changelogOptions: {}, 10 | gitCommitVerifyIgnores: [ 11 | /^((Merge pull request)|(Merge (.*?) into (.*?)|(Merge branch (.*?)))(?:\r?\n)*$)/m, 12 | /^(Merge tag (.*?))(?:\r?\n)*$/m, 13 | /^(R|r)evert (.*)/, 14 | /^(amend|fixup|squash)!/, 15 | /^(Merged (.*?)(in|into) (.*)|Merged PR (.*): (.*))/, 16 | /^Merge remote-tracking branch(\s*)(.*)/, 17 | /^Automatic merge(.*)/, 18 | /^Auto-merged (.*?) into (.*)/ 19 | ] 20 | }; 21 | 22 | export async function loadCliOptions(overrides?: Partial, cwd = process.cwd()) { 23 | const { config } = await loadConfig>({ 24 | name: 'soybean', 25 | defaults: defaultOptions, 26 | overrides, 27 | cwd, 28 | packageJson: true 29 | }); 30 | 31 | return config as CliOption; 32 | } 33 | -------------------------------------------------------------------------------- /packages/scripts/src/shared/index.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from 'execa'; 2 | 3 | export async function execCommand(cmd: string, args: string[], options?: Options) { 4 | const { execa } = await import('execa'); 5 | const res = await execa(cmd, args, options); 6 | return (res?.stdout as string)?.trim() || ''; 7 | } 8 | -------------------------------------------------------------------------------- /packages/scripts/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { ChangelogOption } from '@soybeanjs/changelog'; 2 | 3 | export interface CliOption { 4 | /** The project root directory */ 5 | cwd: string; 6 | /** 7 | * Cleanup dirs 8 | * 9 | * Glob pattern syntax {@link https://github.com/isaacs/minimatch} 10 | * 11 | * @default 12 | * ```json 13 | * ["** /dist", "** /pnpm-lock.yaml", "** /node_modules", "!node_modules/**"] 14 | * ``` 15 | */ 16 | cleanupDirs: string[]; 17 | /** 18 | * Npm-check-updates command args 19 | * 20 | * @default ['--deep', '-u'] 21 | */ 22 | ncuCommandArgs: string[]; 23 | /** 24 | * Options of generate changelog 25 | * 26 | * @link https://github.com/soybeanjs/changelog 27 | */ 28 | changelogOptions: Partial; 29 | /** The ignore pattern list of git commit verify */ 30 | gitCommitVerifyIgnores: RegExp[]; 31 | } 32 | -------------------------------------------------------------------------------- /packages/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "preserve", 5 | "lib": ["DOM", "ESNext"], 6 | "baseUrl": ".", 7 | "module": "ESNext", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "types": ["node"], 11 | "strict": true, 12 | "strictNullChecks": true, 13 | "noUnusedLocals": true, 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true 17 | }, 18 | "include": ["src/**/*", "typings/**/*"], 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/uno-preset/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa/uno-preset", 3 | "version": "1.0.5", 4 | "exports": { 5 | ".": "./src/index.ts" 6 | }, 7 | "typesVersions": { 8 | "*": { 9 | "*": ["./src/*"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/uno-preset/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "preserve", 5 | "lib": ["DOM", "ESNext"], 6 | "baseUrl": ".", 7 | "module": "ESNext", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "types": ["node"], 11 | "strict": true, 12 | "strictNullChecks": true, 13 | "noUnusedLocals": true, 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sa/utils", 3 | "version": "1.0.5", 4 | "exports": { 5 | ".": "./src/index.ts" 6 | }, 7 | "typesVersions": { 8 | "*": { 9 | "*": ["./src/*"] 10 | } 11 | }, 12 | "dependencies": { 13 | "colord": "2.9.3", 14 | "crypto-js": "4.2.0", 15 | "klona": "2.0.6", 16 | "localforage": "1.10.0", 17 | "nanoid": "5.1.5" 18 | }, 19 | "devDependencies": { 20 | "@types/crypto-js": "4.2.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/utils/src/crypto.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from 'crypto-js'; 2 | 3 | export class Crypto { 4 | /** Secret */ 5 | secret: string; 6 | 7 | constructor(secret: string) { 8 | this.secret = secret; 9 | } 10 | 11 | encrypt(data: T): string { 12 | const dataString = JSON.stringify(data); 13 | const encrypted = CryptoJS.AES.encrypt(dataString, this.secret); 14 | return encrypted.toString(); 15 | } 16 | 17 | decrypt(encrypted: string) { 18 | const decrypted = CryptoJS.AES.decrypt(encrypted, this.secret); 19 | const dataString = decrypted.toString(CryptoJS.enc.Utf8); 20 | try { 21 | return JSON.parse(dataString) as T; 22 | } catch { 23 | // avoid parse error 24 | return null; 25 | } 26 | } 27 | 28 | /** 29 | * sha256加密 30 | * 31 | * @param plaintext - 明文 32 | */ 33 | static sha256(plaintext: string) { 34 | return CryptoJS.SHA256(plaintext).toString(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crypto'; 2 | export * from './storage'; 3 | export * from './nanoid'; 4 | export * from './klona'; 5 | -------------------------------------------------------------------------------- /packages/utils/src/klona.ts: -------------------------------------------------------------------------------- 1 | import { klona as jsonClone } from 'klona/json'; 2 | 3 | export { jsonClone }; 4 | -------------------------------------------------------------------------------- /packages/utils/src/nanoid.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from 'nanoid'; 2 | 3 | export { nanoid }; 4 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "preserve", 5 | "lib": ["DOM", "ESNext"], 6 | "baseUrl": ".", 7 | "module": "ESNext", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "types": ["node"], 11 | "strict": true, 12 | "strictNullChecks": true, 13 | "noUnusedLocals": true, 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/assets/imgs/soybean.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paynezhuang/panis-admin/904e7b67688c6933a11a3486dc80de3a0da7f91e/src/assets/imgs/soybean.jpg -------------------------------------------------------------------------------- /src/assets/svg-icon/activity.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/svg-icon/at-sign.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/svg-icon/cast.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/svg-icon/chrome.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/svg-icon/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/svg-icon/custom-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/svg-icon/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/svg-icon/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/svg-icon/wind.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/advanced/table-column-setting.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/components/common/app-provider.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/components/common/dark-mode-container.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/common/exception-base.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/common/full-screen.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/components/common/lang-switch.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/components/common/menu-toggler.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/components/common/pin-toggler.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/components/common/reload-button.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/common/system-logo.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/common/theme-schema-switch.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/components/custom/better-scroll.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/components/custom/button-icon.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/components/custom/look-forward.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/components/custom/soybean-avatar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/custom/svg-icon.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/constants/business.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paynezhuang/panis-admin/904e7b67688c6933a11a3486dc80de3a0da7f91e/src/constants/business.ts -------------------------------------------------------------------------------- /src/constants/reg.ts: -------------------------------------------------------------------------------- 1 | export const REG_USER_NAME = /^[\u4E00-\u9FA5a-zA-Z0-9_-]{4,16}$/; 2 | 3 | /** Phone reg */ 4 | export const REG_PHONE = /^[1](([3][0-9])|([4][01456789])|([5][012356789])|([6][2567])|([7][0-8])|([8][0-9])|([9][012356789]))[0-9]{8}$/; 5 | 6 | /** 7 | * Password reg 8 | * 9 | * 6-18 characters, including letters, numbers, and underscores 10 | */ 11 | export const REG_PWD = /^\w{6,18}$/; 12 | 13 | /** Email reg */ 14 | export const REG_EMAIL = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/; 15 | 16 | /** Six digit code reg */ 17 | export const REG_CODE_SIX = /^\d{6}$/; 18 | 19 | /** Four digit code reg */ 20 | export const REG_CODE_FOUR = /^\d{4}$/; 21 | 22 | /** Url reg */ 23 | export const REG_URL = 24 | /(((^https?:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)$/; 25 | -------------------------------------------------------------------------------- /src/enum/index.ts: -------------------------------------------------------------------------------- 1 | export enum SetupStoreId { 2 | App = 'app-store', 3 | Theme = 'theme-store', 4 | Auth = 'auth-store', 5 | Route = 'route-store', 6 | Tab = 'tab-store', 7 | Dict = 'dict-store' 8 | } 9 | 10 | export enum DateTimePattern { 11 | Date = 'YYYY-MM-DD', 12 | Time = 'HH:mm:ss', 13 | DateTime = 'YYYY-MM-DD HH:mm:ss' 14 | } 15 | -------------------------------------------------------------------------------- /src/hooks/business/auth.ts: -------------------------------------------------------------------------------- 1 | import { useRoute } from 'vue-router'; 2 | import { useAuthStore } from '@/store/modules/auth'; 3 | 4 | const { VITE_AUTH_BUTTON_MODE = 'route_meta' } = import.meta.env; 5 | 6 | export function useAuth() { 7 | const authStore = useAuthStore(); 8 | 9 | const route = useRoute(); 10 | 11 | const hasAuthModelPermissionMap: Record = { 12 | route_meta: route?.meta.permissions ?? [], 13 | user_info: authStore.userInfo.permissions ?? [] 14 | }; 15 | 16 | function hasAuth(codes: string | string[]) { 17 | if (!authStore.isLogin) { 18 | return false; 19 | } 20 | 21 | const permissions = hasAuthModelPermissionMap[VITE_AUTH_BUTTON_MODE as Env.AuthButtonMode]; 22 | 23 | if (typeof codes === 'string') { 24 | return permissions.includes(codes); 25 | } 26 | 27 | return codes.some(code => permissions.includes(code)); 28 | } 29 | 30 | return { 31 | hasAuth 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/hooks/business/captcha.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue'; 2 | import { useCountDown, useLoading } from '@sa/hooks'; 3 | import { $t } from '@/locales'; 4 | import { REG_PHONE } from '@/constants/reg'; 5 | 6 | export function useCaptcha() { 7 | const { loading, startLoading, endLoading } = useLoading(); 8 | const { count, start, stop, isCounting } = useCountDown(10); 9 | 10 | const label = computed(() => { 11 | let text = $t('page.login.codeLogin.getCode'); 12 | 13 | const countingLabel = $t('page.login.codeLogin.reGetCode', { time: count.value }); 14 | 15 | if (loading.value) { 16 | text = ''; 17 | } 18 | 19 | if (isCounting.value) { 20 | text = countingLabel; 21 | } 22 | 23 | return text; 24 | }); 25 | 26 | function isPhoneValid(phone: string) { 27 | if (phone.trim() === '') { 28 | window.$message?.error?.($t('form.phone.required')); 29 | 30 | return false; 31 | } 32 | 33 | if (!REG_PHONE.test(phone)) { 34 | window.$message?.error?.($t('form.phone.invalid')); 35 | 36 | return false; 37 | } 38 | 39 | return true; 40 | } 41 | 42 | async function getCaptcha(phone: string) { 43 | const valid = isPhoneValid(phone); 44 | 45 | if (!valid || loading.value) { 46 | return; 47 | } 48 | 49 | startLoading(); 50 | 51 | // request 52 | await new Promise(resolve => { 53 | setTimeout(resolve, 500); 54 | }); 55 | 56 | window.$message?.success?.($t('page.login.codeLogin.sendCodeSuccess')); 57 | 58 | start(); 59 | 60 | endLoading(); 61 | } 62 | 63 | return { 64 | label, 65 | start, 66 | stop, 67 | isCounting, 68 | loading, 69 | getCaptcha 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /src/hooks/common/button-auth-dropdown.ts: -------------------------------------------------------------------------------- 1 | import { NButton, NDropdown } from 'naive-ui'; 2 | import { computed, h } from 'vue'; 3 | import { $t } from '@/locales'; 4 | 5 | /** DataTable operation button dropdown */ 6 | export function useButtonAuthDropdown(options: CommonType.ButtonDropdown[]) { 7 | // Convert ButtonDropdown options to NDropdown options 8 | const dropdownOptions = computed(() => 9 | options 10 | .filter(opt => opt.show) 11 | .map(opt => ({ 12 | key: opt.key, 13 | label: opt.label, 14 | icon: opt.icon 15 | })) 16 | ); 17 | 18 | /** Handle dropdown select */ 19 | const handleSelect = (key: T, row: S) => { 20 | const option = options.find(opt => opt.key === key && 'handler' in opt); 21 | option?.handler(key, row); 22 | }; 23 | 24 | /** Render dropdown */ 25 | const renderDropdown = (row: S) => { 26 | return h( 27 | NDropdown, 28 | { 29 | trigger: 'hover', 30 | options: dropdownOptions.value, 31 | size: 'small', 32 | showArrow: true, 33 | onSelect: (key: T) => handleSelect(key, row) 34 | }, 35 | { 36 | default: () => h(NButton, { quaternary: true, type: 'primary' }, { default: () => $t('common.moreOperation') }) 37 | } 38 | ); 39 | }; 40 | return { renderDropdown }; 41 | } 42 | -------------------------------------------------------------------------------- /src/hooks/common/icon.ts: -------------------------------------------------------------------------------- 1 | import { useSvgIconRender } from '@sa/hooks'; 2 | import SvgIcon from '@/components/custom/svg-icon.vue'; 3 | 4 | export function useSvgIcon() { 5 | const { SvgIconVNode } = useSvgIconRender(SvgIcon); 6 | 7 | return { 8 | SvgIconVNode 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/layouts/blank-layout/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/layouts/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { computed, ref, watch } from 'vue'; 2 | import { useRoute } from 'vue-router'; 3 | import { useRouteStore } from '@/store/modules/route'; 4 | 5 | export function useMixMenu() { 6 | const route = useRoute(); 7 | const routeStore = useRouteStore(); 8 | 9 | const activeFirstLevelMenuKey = ref(''); 10 | 11 | function setActiveFirstLevelMenuKey(key: string) { 12 | activeFirstLevelMenuKey.value = key; 13 | } 14 | 15 | function getActiveFirstLevelMenuKey() { 16 | const { hideInMenu, activeMenu } = route.meta; 17 | const name = route.name as string; 18 | 19 | const routeName = (hideInMenu ? activeMenu : name) || name; 20 | 21 | const [firstLevelRouteName] = routeName.split('_'); 22 | 23 | setActiveFirstLevelMenuKey(firstLevelRouteName); 24 | } 25 | 26 | const menus = computed(() => routeStore.menus.find(menu => menu.key === activeFirstLevelMenuKey.value)?.children || []); 27 | 28 | watch( 29 | () => route.name, 30 | () => { 31 | getActiveFirstLevelMenuKey(); 32 | }, 33 | { immediate: true } 34 | ); 35 | 36 | return { 37 | activeFirstLevelMenuKey, 38 | setActiveFirstLevelMenuKey, 39 | getActiveFirstLevelMenuKey, 40 | menus 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/layouts/modules/global-breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/layouts/modules/global-content/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/layouts/modules/global-footer/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/layouts/modules/global-header/components/theme-button.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/layouts/modules/global-logo/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/layouts/modules/global-menu/index.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/layouts/modules/global-menu/modules/horizontal-menu.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/layouts/modules/global-menu/modules/horizontal-mix-menu.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/layouts/modules/global-menu/modules/vertical-menu.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/layouts/modules/global-search/components/search-footer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 24 | 25 | 37 | -------------------------------------------------------------------------------- /src/layouts/modules/global-search/components/search-result.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/layouts/modules/global-search/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/layouts/modules/global-sider/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/layouts/modules/theme-drawer/components/setting-item.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/layouts/modules/theme-drawer/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/layouts/modules/theme-drawer/modules/config-operation.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 |