commit 120a5b4dfdd27f5c3e9234a30d9a5669d5a65dcb Author: hongawen <83944980@qq.com> Date: Thu Mar 26 20:18:20 2026 +0800 初始化 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0552777 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# Editor configuration, see http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.env b/.env new file mode 100644 index 0000000..4f6bab1 --- /dev/null +++ b/.env @@ -0,0 +1,60 @@ +# 应用的基础访问路径,默认是 "/" +# 如果部署在子目录下,结尾必须带 "/",例如 "/admin/",不能写成 "/admin" +VITE_BASE_URL=/ + +VITE_APP_TITLE=研发内部管理系统 + +VITE_APP_DESC=Frontend application for 灿能研发内部管理系统 + +# 图标名称前缀 +VITE_ICON_PREFIX=icon + +# 本地 svg 图标组件前缀,必须包含 VITE_ICON_PREFIX +# 格式为 {VITE_ICON_PREFIX}-{local icon name} +VITE_ICON_LOCAL_PREFIX=icon-local + +# 权限路由模式:static | dynamic +VITE_AUTH_ROUTE_MODE=dynamic + +# 静态权限路由模式下的首页路由 +VITE_ROUTE_HOME=system_user + +# 默认菜单图标 +VITE_MENU_ICON=mdi:menu + +# 开发模式下是否启用 HTTP 代理 +VITE_HTTP_PROXY=Y + +# vue-router 路由模式:hash | history | memory +VITE_ROUTER_HISTORY_MODE=history + +# 后端成功状态码;当返回该 code 时,前端认为请求成功 +VITE_SERVICE_SUCCESS_CODE=0 + +# 后端登出状态码;当返回这些 code 时,前端会登出并跳回登录页 +# 典型场景:token 无效、登录状态失效、账号被踢下线、后端要求强制重新登录 +VITE_SERVICE_LOGOUT_CODES=401,1002023000 + +# 后端弹窗登出状态码;当返回这些 code 时,前端会先弹窗再登出 +# 典型场景:账号被禁用、密码已重置、登录安全策略触发、需要用户先确认后再重新登录 +VITE_SERVICE_MODAL_LOGOUT_CODES=7777,7778 + +# token 过期状态码;当返回这些 code 时,前端会尝试刷新 token 并重发请求 +# 典型场景:accessToken 过期但 refreshToken 仍有效、短期登录凭证失效但允许无感续期 +VITE_SERVICE_EXPIRED_TOKEN_CODES=1002023001 + +# 静态路由模式下定义的超级管理员角色 +VITE_STATIC_SUPER_ROLE=R_SUPER + +# 是否生成 sourcemap +VITE_SOURCE_MAP=N + +# 本地存储前缀,用于区分同域名下的不同系统 +VITE_STORAGE_PREFIX=CN_RDMS_ + +# 是否在终端输出代理地址日志 +VITE_PROXY_LOG=Y + +# 控制是否允许从开发工具中唤起编辑器 +# 这个能力只在开发模式下可用,构建模式下不会生效 +VITE_DEVTOOLS_LAUNCH_EDITOR=code diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..16fb7bd --- /dev/null +++ b/.env.dev @@ -0,0 +1,10 @@ +# 测试/开发环境的后端服务地址 +VITE_SERVICE_BASE_URL=http://localhost:48080 + +# 测试/开发环境下的其他后端服务地址 +VITE_OTHER_SERVICE_BASE_URL= `{ + "demo": "http://localhost:9528" +}` + +# 鏄惁鍦ㄥ紑鍙戠幆澧冨惎鐢?Vue DevTools 娴姩鍏ュ彛 +VITE_DEVTOOLS_ENABLED=N diff --git a/.env.prod b/.env.prod new file mode 100644 index 0000000..b813430 --- /dev/null +++ b/.env.prod @@ -0,0 +1,7 @@ +# 生产环境的后端服务地址 +VITE_SERVICE_BASE_URL=https://mock.apifox.cn/m1/3109515-0-default + +# 生产环境下的其他后端服务地址 +VITE_OTHER_SERVICE_BASE_URL= `{ + "demo": "http://localhost:9529" +}` diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9553ccb --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +"*.vue" eol=lf +"*.js" eol=lf +"*.ts" eol=lf +"*.jsx" eol=lf +"*.tsx" eol=lf +"*.mjs" eol=lf +"*.json" eol=lf +"*.html" eol=lf +"*.css" eol=lf +"*.scss" eol=lf +"*.md" eol=lf +"*.yaml" eol=lf +"*.yml" eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d13bb21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +!.vscode/settings.json +!.vscode/launch.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +package-lock.json +yarn.lock + +.VSCodeCounter + +# Docs +/docs/* +!/docs/frontend-page-resource-manifest.json diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..567bfcc --- /dev/null +++ b/.npmrc @@ -0,0 +1,5 @@ +registry=https://registry.npmmirror.com/ +shamefully-hoist=true +ignore-workspace-root-check=true +link-workspace-packages=true +enable-pre-post-scripts=true diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c3812e0 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,14 @@ +{ + "recommendations": [ + "antfu.unocss", + "dbaeumer.vscode-eslint", + "editorconfig.editorconfig", + "esbenp.prettier-vscode", + "kisstkondoros.vscode-gutter-preview", + "mariusalchimavicius.json-to-ts", + "mhutchie.git-graph", + "sdras.vue-vscode-snippets", + "vue.volar", + "vue.vscode-typescript-vue-plugin" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e59c32b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Vue Debugger", + "url": "http://localhost:9527", + "webRoot": "${workspaceFolder}" + }, + { + "type": "node", + "request": "launch", + "name": "TS Debugger", + "runtimeExecutable": "tsx", + "skipFiles": ["/**", "${workspaceFolder}/node_modules/**"], + "program": "${file}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ee081a6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,31 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports": "never" + }, + "editor.formatOnSave": false, + "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": ["./"] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..20547de --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Soybean + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9207638 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# cn-rdms-web + +这是当前项目的前端工程仓库。 + +原开源模板项目的介绍内容已移除,这个 README 现在只保留当前项目自身所需的信息。 + +## 项目说明 + +待补充。 + +建议后续在这里补充: + +- 项目背景 +- 技术栈 +- 目录结构 +- 本地启动方式 +- 环境变量说明 +- 构建与发布流程 + +## 本地开发 + +```bash +pnpm install +pnpm dev +``` + +## 常用命令 + +```bash +pnpm dev +pnpm build +pnpm build:dev +pnpm typecheck +pnpm lint +``` diff --git a/build/config/cli-helper.ts b/build/config/cli-helper.ts new file mode 100644 index 0000000..d114e85 --- /dev/null +++ b/build/config/cli-helper.ts @@ -0,0 +1,58 @@ +/* eslint-disable */ +import process from 'node:process' +import readline from 'node:readline' + +function clearScreen() { + const repeatCount = process.stdout.rows - 2 + const blank = repeatCount > 0 ? '\n'.repeat(repeatCount) : '' + console.log(blank) + readline.cursorTo(process.stdout, 0, 0) + readline.clearScreenDown(process.stdout) +} + +function formatter(open: string, close: string, replace = open) { + return (input: string) => { + const string = `${input}` + const index = string.indexOf(close, open.length) + return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close + } +} + +function replaceClose(string: string, close: string, replace: string, index: number): string { + const start = string.substring(0, index) + replace + const end = string.substring(index + close.length) + const nextIndex = end.indexOf(close) + return ~nextIndex ? start + replaceClose(end, close, replace, nextIndex) : start + end +} + +function createColors() { + return { + bgBlack: formatter('\x1B[40m', '\x1B[49m'), + bgBlue: formatter('\x1B[44m', '\x1B[49m'), + bgCyan: formatter('\x1B[46m', '\x1B[49m'), + bgGreen: formatter('\x1B[42m', '\x1B[49m'), + bgMagenta: formatter('\x1B[45m', '\x1B[49m'), + bgRed: formatter('\x1B[41m', '\x1B[49m', '\x1B[22m\x1B[1m'), + bgWhite: formatter('\x1B[47m', '\x1B[49m'), + bgYellow: formatter('\x1B[43m', '\x1B[49m'), + black: formatter('\x1B[30m', '\x1B[39m'), + blue: formatter('\x1B[34m', '\x1B[39m'), + bold: formatter('\x1B[1m', '\x1B[22m', '\x1B[22m\x1B[1m'), + cyan: formatter('\x1B[36m', '\x1B[39m'), + dim: formatter('\x1B[2m', '\x1B[22m', '\x1B[22m\x1B[2m'), + gray: formatter('\x1B[90m', '\x1B[39m'), + green: formatter('\x1B[32m', '\x1B[39m'), + hidden: formatter('\x1B[8m', '\x1B[28m'), + inverse: formatter('\x1B[7m', '\x1B[27m'), + italic: formatter('\x1B[3m', '\x1B[23m'), + magenta: formatter('\x1B[35m', '\x1B[39m'), + red: formatter('\x1B[31m', '\x1B[39m'), + reset: (s: string) => `\x1B[0m${s}\x1B[0m`, + strikethrough: formatter('\x1B[9m', '\x1B[29m'), + underline: formatter('\x1B[4m', '\x1B[24m'), + white: formatter('\x1B[37m', '\x1B[39m'), + yellow: formatter('\x1B[33m', '\x1B[39m'), + } +} + +export { clearScreen, createColors } diff --git a/build/config/index.ts b/build/config/index.ts new file mode 100644 index 0000000..8a9621a --- /dev/null +++ b/build/config/index.ts @@ -0,0 +1,2 @@ +export * from './proxy'; +export * from './time'; diff --git a/build/config/proxy.ts b/build/config/proxy.ts new file mode 100644 index 0000000..085c424 --- /dev/null +++ b/build/config/proxy.ts @@ -0,0 +1,57 @@ +import type { ProxyOptions } from 'vite'; +import { bgRed, bgYellow, green, lightBlue } from 'kolorist'; +import { consola } from 'consola'; +import { createServiceConfig } from '../../src/utils/service'; + +/** + * Set http proxy + * + * @param env - The current env + * @param enable - If enable http proxy + */ +export function createViteProxy(env: Env.ImportMeta, enable: boolean) { + const isEnableHttpProxy = enable && env.VITE_HTTP_PROXY === 'Y'; + + if (!isEnableHttpProxy) return undefined; + + const isEnableProxyLog = env.VITE_PROXY_LOG === 'Y'; + + const { baseURL, proxyPattern, other } = createServiceConfig(env); + + const proxy: Record = createProxyItem({ baseURL, proxyPattern }, isEnableProxyLog); + + other.forEach(item => { + Object.assign(proxy, createProxyItem(item, isEnableProxyLog)); + }); + + return proxy; +} + +function createProxyItem(item: App.Service.ServiceConfigItem, enableLog: boolean) { + const proxy: Record = {}; + + proxy[item.proxyPattern] = { + target: item.baseURL, + changeOrigin: true, + configure: (_proxy, options) => { + _proxy.on('proxyReq', (_proxyReq, req, _res) => { + if (!enableLog) return; + + const requestUrl = `${lightBlue('[proxy url]')}: ${bgYellow(` ${req.method} `)} ${green( + `${item.proxyPattern}${req.url}` + )}`; + + const proxyUrl = `${lightBlue('[real request url]')}: ${green(`${options.target}${req.url}`)}`; + + consola.log(`${requestUrl}\n${proxyUrl}`); + }); + _proxy.on('error', (_err, req, _res) => { + if (!enableLog) return; + consola.log(bgRed(`Error: ${req.method} `), green(`${options.target}${req.url}`)); + }); + }, + rewrite: path => path.replace(new RegExp(`^${item.proxyPattern}`), '') + }; + + return proxy; +} diff --git a/build/config/time.ts b/build/config/time.ts new file mode 100644 index 0000000..3b57146 --- /dev/null +++ b/build/config/time.ts @@ -0,0 +1,12 @@ +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; + +export function getBuildTime() { + dayjs.extend(utc); + dayjs.extend(timezone); + + const buildTime = dayjs.tz(Date.now(), 'Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss'); + + return buildTime; +} diff --git a/build/plugins/devtools.ts b/build/plugins/devtools.ts new file mode 100644 index 0000000..349358d --- /dev/null +++ b/build/plugins/devtools.ts @@ -0,0 +1,13 @@ +import VueDevtools from 'vite-plugin-vue-devtools'; + +export function setupDevtoolsPlugin(viteEnv: Env.ImportMeta) { + const { VITE_DEVTOOLS_ENABLED, VITE_DEVTOOLS_LAUNCH_EDITOR } = viteEnv; + + if (VITE_DEVTOOLS_ENABLED !== 'Y') { + return null; + } + + return VueDevtools({ + launchEditor: VITE_DEVTOOLS_LAUNCH_EDITOR + }); +} diff --git a/build/plugins/html.ts b/build/plugins/html.ts new file mode 100644 index 0000000..b94d24f --- /dev/null +++ b/build/plugins/html.ts @@ -0,0 +1,13 @@ +import type { Plugin } from 'vite'; + +export function setupHtmlPlugin(buildTime: string) { + const plugin: Plugin = { + name: 'html-plugin', + apply: 'build', + transformIndexHtml(html) { + return html.replace('', `\n `); + } + }; + + return plugin; +} diff --git a/build/plugins/index.ts b/build/plugins/index.ts new file mode 100644 index 0000000..1243fc2 --- /dev/null +++ b/build/plugins/index.ts @@ -0,0 +1,24 @@ +import type { PluginOption } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import vueJsx from '@vitejs/plugin-vue-jsx'; +import progress from 'vite-plugin-progress'; +import { setupElegantRouter } from './router'; +import { setupUnocss } from './unocss'; +import { setupUnplugin } from './unplugin'; +import { setupHtmlPlugin } from './html'; +import { setupDevtoolsPlugin } from './devtools'; + +export function setupVitePlugins(viteEnv: Env.ImportMeta, buildTime: string) { + const plugins: PluginOption = [ + vue(), + vueJsx(), + setupDevtoolsPlugin(viteEnv), + setupElegantRouter(), + setupUnocss(viteEnv), + ...setupUnplugin(viteEnv), + progress(), + setupHtmlPlugin(buildTime) + ]; + + return plugins; +} diff --git a/build/plugins/router.ts b/build/plugins/router.ts new file mode 100644 index 0000000..8a885d1 --- /dev/null +++ b/build/plugins/router.ts @@ -0,0 +1,79 @@ +import type { RouteMeta } from 'vue-router'; +import ElegantVueRouter from '@elegant-router/vue/vite'; +import type { RouteKey } from '@elegant-router/types'; + +export function setupElegantRouter() { + return ElegantVueRouter({ + layouts: { + base: 'src/layouts/base-layout/index.vue', + blank: 'src/layouts/blank-layout/index.vue' + }, + customRoutes: { + names: ['exception_403', 'exception_404', 'exception_500'] + }, + routePathTransformer(routeName, routePath) { + const key = routeName as RouteKey; + + if (key === 'login') { + const modules: UnionKey.LoginModule[] = ['pwd-login', 'reset-pwd']; + + const moduleReg = modules.join('|'); + + return `/login/:module(${moduleReg})?`; + } + + return routePath; + }, + onRouteMetaGen(routeName) { + const key = routeName as RouteKey; + + const constantRoutes: RouteKey[] = ['login', '403', '404', '500']; + const routeMetaMap: Partial>> = { + system: { + icon: 'carbon:cloud-service-management', + order: 9, + roles: ['R_ADMIN'] + }, + system_menu: { + icon: 'material-symbols:route', + order: 3, + roles: ['R_ADMIN'], + keepAlive: true + }, + system_dict: { + icon: 'mdi:book-open-page-variant-outline', + order: 4, + roles: ['R_ADMIN'], + keepAlive: true + }, + system_role: { + icon: 'carbon:user-role', + order: 2, + roles: ['R_SUPER'] + }, + system_user: { + icon: 'ic:round-manage-accounts', + order: 1, + roles: ['R_ADMIN'] + }, + 'system_user-detail': { + hideInMenu: true, + roles: ['R_ADMIN'], + activeMenu: 'system_user' + } + }; + + const meta: Partial = { + title: key, + i18nKey: `route.${key}` as App.I18n.I18nKey, + ...routeMetaMap[key] + }; + + if (constantRoutes.includes(key)) { + meta.constant = true; + } + + return meta; + } + }); +} diff --git a/build/plugins/unocss.ts b/build/plugins/unocss.ts new file mode 100644 index 0000000..06b41d3 --- /dev/null +++ b/build/plugins/unocss.ts @@ -0,0 +1,32 @@ +import process from 'node:process'; +import path from 'node:path'; +import unocss from '@unocss/vite'; +import presetIcons from '@unocss/preset-icons'; +import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders'; + +export function setupUnocss(viteEnv: Env.ImportMeta) { + const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv; + + const localIconPath = path.join(process.cwd(), 'src/assets/svg-icon'); + + /** The name of the local icon collection */ + const collectionName = VITE_ICON_LOCAL_PREFIX.replace(`${VITE_ICON_PREFIX}-`, ''); + + return unocss({ + presets: [ + presetIcons({ + prefix: `${VITE_ICON_PREFIX}-`, + scale: 1, + extraProperties: { + display: 'inline-block' + }, + collections: { + [collectionName]: FileSystemIconLoader(localIconPath, svg => + svg.replace(/^ + svg.replace(/^", + "singlePageComponentPattern": "layout.$view." + }, + "total": 5, + "items": [ + { + "name": "system_user", + "path": "/system/user", + "component": "view.system_user", + "title": "用户管理", + "routeTitle": "system_user", + "i18nKey": "route.system_user", + "icon": "ic:round-manage-accounts", + "localIcon": null, + "order": 1, + "hideInMenu": false, + "keepAlive": false, + "activeMenu": null, + "multiTab": false, + "fixedIndexInTab": null, + "redirect": null, + "props": null, + "meta": { + "title": "用户管理", + "i18nKey": "route.system_user", + "icon": "ic:round-manage-accounts", + "localIcon": null, + "order": 1, + "keepAlive": false, + "hideInMenu": false, + "activeMenu": null, + "multiTab": false, + "fixedIndexInTab": null + }, + "parentName": "system", + "pageType": "leaf", + "source": "generated" + }, + { + "name": "system_role", + "path": "/system/role", + "component": "view.system_role", + "title": "角色管理", + "routeTitle": "system_role", + "i18nKey": "route.system_role", + "icon": "carbon:user-role", + "localIcon": null, + "order": 2, + "hideInMenu": false, + "keepAlive": false, + "activeMenu": null, + "multiTab": false, + "fixedIndexInTab": null, + "redirect": null, + "props": null, + "meta": { + "title": "角色管理", + "i18nKey": "route.system_role", + "icon": "carbon:user-role", + "localIcon": null, + "order": 2, + "keepAlive": false, + "hideInMenu": false, + "activeMenu": null, + "multiTab": false, + "fixedIndexInTab": null + }, + "parentName": "system", + "pageType": "leaf", + "source": "generated" + }, + { + "name": "system_menu", + "path": "/system/menu", + "component": "view.system_menu", + "title": "菜单管理", + "routeTitle": "system_menu", + "i18nKey": "route.system_menu", + "icon": "material-symbols:route", + "localIcon": null, + "order": 3, + "hideInMenu": false, + "keepAlive": true, + "activeMenu": null, + "multiTab": false, + "fixedIndexInTab": null, + "redirect": null, + "props": null, + "meta": { + "title": "菜单管理", + "i18nKey": "route.system_menu", + "icon": "material-symbols:route", + "localIcon": null, + "order": 3, + "keepAlive": true, + "hideInMenu": false, + "activeMenu": null, + "multiTab": false, + "fixedIndexInTab": null + }, + "parentName": "system", + "pageType": "leaf", + "source": "generated" + }, + { + "name": "system_dict", + "path": "/system/dict", + "component": "view.system_dict", + "title": "字典管理", + "routeTitle": "system_dict", + "i18nKey": "route.system_dict", + "icon": "mdi:book-open-page-variant-outline", + "localIcon": null, + "order": 4, + "hideInMenu": false, + "keepAlive": true, + "activeMenu": null, + "multiTab": false, + "fixedIndexInTab": null, + "redirect": null, + "props": null, + "meta": { + "title": "字典管理", + "i18nKey": "route.system_dict", + "icon": "mdi:book-open-page-variant-outline", + "localIcon": null, + "order": 4, + "keepAlive": true, + "hideInMenu": false, + "activeMenu": null, + "multiTab": false, + "fixedIndexInTab": null + }, + "parentName": "system", + "pageType": "leaf", + "source": "generated" + }, + { + "name": "system_post", + "path": "/system/post", + "component": "view.system_post", + "title": "岗位管理", + "routeTitle": "system_post", + "i18nKey": "route.system_post", + "icon": null, + "localIcon": null, + "order": null, + "hideInMenu": false, + "keepAlive": false, + "activeMenu": null, + "multiTab": false, + "fixedIndexInTab": null, + "redirect": null, + "props": null, + "meta": { + "title": "岗位管理", + "i18nKey": "route.system_post", + "icon": null, + "localIcon": null, + "order": null, + "keepAlive": false, + "hideInMenu": false, + "activeMenu": null, + "multiTab": false, + "fixedIndexInTab": null + }, + "parentName": "system", + "pageType": "leaf", + "source": "generated" + } + ] +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..00537e3 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,24 @@ +import { defineConfig } from '@soybeanjs/eslint-config'; + +export default defineConfig( + { vue: true, unocss: true }, + { + rules: { + 'vue/multi-word-component-names': [ + 'warn', + { + ignores: ['index', 'App', 'Register', '[id]', '[url]'] + } + ], + 'vue/component-name-in-template-casing': [ + 'warn', + 'PascalCase', + { + registeredComponentsOnly: false, + ignores: ['/^icon-/'] + } + ], + 'unocss/order-attributify': 'off' + } + } +); diff --git a/index.html b/index.html new file mode 100644 index 0000000..442f72c --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + + + %VITE_APP_TITLE% + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..1d435b8 --- /dev/null +++ b/package.json @@ -0,0 +1,124 @@ +{ + "name": "cn-rdms-web", + "type": "module", + "version": "1.3.15", + "description": "Frontend application for 灿能研发内部管理系统.", + "author": "灿能电力", + "license": "MIT", + "keywords": [ + "Vue3 admin ", + "vue-admin-template", + "Vite7", + "TypeScript", + "element-plus", + "element-plus-admin", + "UnoCSS" + ], + "engines": { + "node": ">=20.19.0", + "pnpm": ">=8.7.0" + }, + "scripts": { + "build": "vite build --mode prod", + "build:dev": "vite build --mode dev", + "cleanup": "sa cleanup", + "commit": "sa git-commit", + "commit:zh": "sa git-commit -l=zh-cn", + "dev": "vite --mode dev", + "dev:prod": "vite --mode prod", + "create-route": "sa gen-route", + "gen:page-resource-manifest": "node --experimental-strip-types scripts/generate-page-resource-manifest.mjs", + "gen-route": "node --experimental-strip-types scripts/generate-elegant-router.mjs", + "lint": "eslint . --fix", + "prepare": "simple-git-hooks", + "preview": "vite preview", + "release": "sa release", + "typecheck": "vue-tsc --noEmit --skipLibCheck", + "update-pkg": "sa update-pkg" + }, + "dependencies": { + "@antv/data-set": "0.11.8", + "@antv/g2": "5.4.0", + "@antv/g6": "5.0.49", + "@better-scroll/core": "2.5.1", + "@iconify/vue": "5.0.0", + "@sa/axios": "workspace:*", + "@sa/color": "workspace:*", + "@sa/hooks": "workspace:*", + "@sa/materials": "workspace:*", + "@sa/utils": "workspace:*", + "@visactor/vchart": "2.0.4", + "@visactor/vchart-theme": "1.12.2", + "@visactor/vtable-editors": "1.19.8", + "@visactor/vtable-gantt": "1.19.8", + "@visactor/vue-vtable": "1.19.8", + "@vueuse/components": "13.9.0", + "@vueuse/core": "13.9.0", + "clipboard": "2.0.11", + "dayjs": "1.11.18", + "defu": "^6.1.4", + "dhtmlx-gantt": "9.0.14", + "dompurify": "3.2.6", + "echarts": "6.0.0", + "element-plus": "^2.11.1", + "jsbarcode": "3.12.1", + "json5": "2.2.3", + "nprogress": "0.2.0", + "pinia": "3.0.3", + "pinyin-pro": "3.27.0", + "print-js": "1.6.0", + "swiper": "11.2.10", + "tailwind-merge": "3.3.1", + "typeit": "8.8.7", + "vditor": "3.11.2", + "vue": "3.5.20", + "vue-draggable-plus": "0.6.0", + "vue-i18n": "11.1.11", + "vue-pdf-embed": "2.1.3", + "vue-router": "4.5.1", + "wangeditor": "4.7.15", + "xgplayer": "3.0.23", + "xlsx": "0.18.5" + }, + "devDependencies": { + "@amap/amap-jsapi-types": "0.0.15", + "@elegant-router/vue": "0.3.8", + "@iconify/json": "2.2.380", + "@sa/scripts": "workspace:*", + "@sa/uno-preset": "workspace:*", + "@soybeanjs/eslint-config": "1.7.1", + "@types/bmapgl": "0.0.7", + "@types/dompurify": "3.2.0", + "@types/node": "24.3.0", + "@types/nprogress": "0.2.3", + "@unocss/eslint-config": "66.5.0", + "@unocss/preset-icons": "66.5.0", + "@unocss/preset-uno": "66.5.0", + "@unocss/transformer-directives": "66.5.0", + "@unocss/transformer-variant-group": "66.5.0", + "@unocss/vite": "66.5.0", + "@vitejs/plugin-vue": "6.0.1", + "@vitejs/plugin-vue-jsx": "5.1.1", + "consola": "3.4.2", + "eslint": "9.34.0", + "eslint-plugin-vue": "10.4.0", + "kolorist": "1.8.0", + "sass": "1.91.0", + "simple-git-hooks": "2.13.1", + "tsx": "4.20.5", + "typescript": "5.8.3", + "unplugin-icons": "22.2.0", + "unplugin-vue-components": "29.0.0", + "vite": "7.1.4", + "vite-plugin-progress": "0.0.7", + "vite-plugin-svg-icons": "2.0.1", + "vite-plugin-vue-devtools": "8.0.1", + "vue-eslint-parser": "10.2.0", + "vue-tsc": "3.0.6" + }, + "simple-git-hooks": { + "commit-msg": "pnpm sa git-commit-verify", + "pre-commit": "pnpm typecheck && pnpm lint && git diff --exit-code" + }, + "website": "" +} diff --git a/packages/axios/package.json b/packages/axios/package.json new file mode 100644 index 0000000..75779ef --- /dev/null +++ b/packages/axios/package.json @@ -0,0 +1,21 @@ +{ + "name": "@sa/axios", + "version": "1.3.15", + "exports": { + ".": "./src/index.ts" + }, + "typesVersions": { + "*": { + "*": ["./src/*"] + } + }, + "dependencies": { + "@sa/utils": "workspace:*", + "axios": "1.11.0", + "axios-retry": "4.5.0", + "qs": "6.14.0" + }, + "devDependencies": { + "@types/qs": "6.14.0" + } +} diff --git a/packages/axios/src/constant.ts b/packages/axios/src/constant.ts new file mode 100644 index 0000000..d6c5a33 --- /dev/null +++ b/packages/axios/src/constant.ts @@ -0,0 +1,5 @@ +/** request id key */ +export const REQUEST_ID_KEY = 'X-Request-Id'; + +/** the backend error code key */ +export const BACKEND_ERROR_CODE = 'BACKEND_ERROR'; diff --git a/packages/axios/src/index.ts b/packages/axios/src/index.ts new file mode 100644 index 0000000..08af572 --- /dev/null +++ b/packages/axios/src/index.ts @@ -0,0 +1,179 @@ +import axios, { AxiosError } from 'axios'; +import type { AxiosResponse, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios'; +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, + FlatRequestInstance, + MappedType, + RequestInstance, + RequestOption, + ResponseType +} from './type'; + +function createCommonRequest< + ResponseData, + ApiData = ResponseData, + State extends Record = Record +>(axiosConfig?: CreateAxiosDefaults, options?: Partial>) { + const opts = createDefaultOptions(options); + + const axiosConf = createAxiosConfig(axiosConfig); + const instance = axios.create(axiosConf); + + const abortControllerMap = new Map(); + + // config axios retry + const retryOptions = createRetryOptions(axiosConf); + axiosRetry(instance, retryOptions); + + instance.interceptors.request.use(conf => { + const config: InternalAxiosRequestConfig = { ...conf }; + + // set request id + const requestId = nanoid(); + config.headers.set(REQUEST_ID_KEY, requestId); + + // config abort controller + if (!config.signal) { + const abortController = new AbortController(); + config.signal = abortController.signal; + abortControllerMap.set(requestId, abortController); + } + + // handle config by hook + const handledConfig = opts.onRequest?.(config) || config; + + return handledConfig; + }); + + instance.interceptors.response.use( + async response => { + const responseType: ResponseType = (response.config?.responseType as ResponseType) || 'json'; + + await transformResponse(response); + + if (responseType !== 'json' || opts.isBackendSuccess(response)) { + return Promise.resolve(response); + } + + const fail = await opts.onBackendFail(response, instance); + if (fail) { + return fail; + } + + const backendError = new AxiosError( + 'the backend request error', + BACKEND_ERROR_CODE, + response.config, + response.request, + response + ); + + await opts.onError(backendError); + + return Promise.reject(backendError); + }, + async (error: AxiosError) => { + await opts.onError(error); + + return Promise.reject(error); + } + ); + + function cancelAllRequest() { + abortControllerMap.forEach(abortController => { + abortController.abort(); + }); + abortControllerMap.clear(); + } + + return { + instance, + opts, + cancelAllRequest + }; +} + +/** + * create a request instance + * + * @param axiosConfig axios config + * @param options request options + */ +export function createRequest>( + axiosConfig?: CreateAxiosDefaults, + options?: Partial> +) { + const { instance, opts, cancelAllRequest } = createCommonRequest(axiosConfig, options); + + const request: RequestInstance = async function request< + T extends ApiData = ApiData, + R extends ResponseType = 'json' + >(config: CustomAxiosRequestConfig) { + const response: AxiosResponse = await instance(config); + + const responseType = response.config?.responseType || 'json'; + + if (responseType === 'json') { + return opts.transform(response); + } + + return response.data as MappedType; + } as RequestInstance; + + request.cancelAllRequest = cancelAllRequest; + request.state = {} as State; + + return request; +} + +/** + * create a flat request instance + * + * The response data is a flat object: { data: any, error: AxiosError } + * + * @param axiosConfig axios config + * @param options request options + */ +export function createFlatRequest>( + axiosConfig?: CreateAxiosDefaults, + options?: Partial> +) { + const { instance, opts, cancelAllRequest } = createCommonRequest(axiosConfig, options); + + const flatRequest: FlatRequestInstance = async function flatRequest< + T extends ApiData = ApiData, + R extends ResponseType = 'json' + >(config: CustomAxiosRequestConfig) { + try { + const response: AxiosResponse = await instance(config); + + const responseType = response.config?.responseType || 'json'; + + if (responseType === 'json') { + const data = await opts.transform(response); + + return { data, error: null, response }; + } + + return { data: response.data as MappedType, error: null, response }; + } catch (error) { + return { data: null, error, response: (error as AxiosError).response }; + } + } as FlatRequestInstance; + + flatRequest.cancelAllRequest = cancelAllRequest; + flatRequest.state = { + ...opts.defaultState + } as State; + + return flatRequest; +} + +export { BACKEND_ERROR_CODE, REQUEST_ID_KEY }; +export type * from './type'; +export type { CreateAxiosDefaults, AxiosError }; diff --git a/packages/axios/src/options.ts b/packages/axios/src/options.ts new file mode 100644 index 0000000..e786639 --- /dev/null +++ b/packages/axios/src/options.ts @@ -0,0 +1,60 @@ +import type { CreateAxiosDefaults } from 'axios'; +import type { IAxiosRetryConfig } from 'axios-retry'; +import { stringify } from 'qs'; +import { isHttpSuccess } from './shared'; +import type { RequestOption } from './type'; + +export function createDefaultOptions< + ResponseData, + ApiData = ResponseData, + State extends Record = Record +>(options?: Partial>) { + const opts: RequestOption = { + 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 () => {}, + onError: async () => {} + }; + + if (options?.transform) { + opts.transform = options.transform; + } else { + opts.transform = options?.transformBackendResponse || opts.transform; + } + + Object.assign(opts, options); + + return opts; +} + +export function createRetryOptions(config?: Partial) { + const retryConfig: IAxiosRetryConfig = { + retries: 0 + }; + + Object.assign(retryConfig, config); + + return retryConfig; +} + +export function createAxiosConfig(config?: Partial) { + const TEN_SECONDS = 10 * 1000; + + const axiosConfig: CreateAxiosDefaults = { + timeout: TEN_SECONDS, + headers: { + 'Content-Type': 'application/json' + }, + validateStatus: isHttpSuccess, + paramsSerializer: params => { + return stringify(params); + } + }; + + Object.assign(axiosConfig, config); + + return axiosConfig; +} diff --git a/packages/axios/src/shared.ts b/packages/axios/src/shared.ts new file mode 100644 index 0000000..1bb68f0 --- /dev/null +++ b/packages/axios/src/shared.ts @@ -0,0 +1,79 @@ +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'; + + return contentType; +} + +/** + * check if http status is success + * + * @param status + */ +export function isHttpSuccess(status: number) { + const isSuccessCode = status >= 200 && status < 300; + return isSuccessCode || status === 304; +} + +/** + * is response json + * + * @param response axios response + */ +export function isResponseJson(response: AxiosResponse) { + const { responseType } = response.config; + + 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 {} +} diff --git a/packages/axios/src/type.ts b/packages/axios/src/type.ts new file mode 100644 index 0000000..846950c --- /dev/null +++ b/packages/axios/src/type.ts @@ -0,0 +1,127 @@ +import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; + +export type ContentType = + | 'text/html' + | 'text/plain' + | 'multipart/form-data' + | 'application/json' + | 'application/x-www-form-urlencoded' + | 'application/octet-stream'; + +export type ResponseTransform = (input: Input) => Output | Promise; + +export interface RequestOption< + ResponseData, + ApiData = ResponseData, + State extends Record = Record +> { + /** + * The default state + */ + defaultState?: State; + /** + * transform the response data to the api data + * + * @param response Axios response + */ + transform: ResponseTransform, 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, ApiData>; + /** + * The hook before request + * + * For example: You can add header token in this hook + * + * @param config Axios config + */ + onRequest: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise; + /** + * The hook to check backend response is success or not + * + * @param response Axios response + */ + isBackendSuccess: (response: AxiosResponse) => boolean; + /** + * The hook after backend request fail + * + * For example: You can handle the expired token in this hook + * + * @param response Axios response + * @param instance Axios instance + */ + onBackendFail: ( + response: AxiosResponse, + instance: AxiosInstance + ) => Promise | Promise; + /** + * The hook to handle error + * + * For example: You can show error message in this hook + * + * @param error + */ + onError: (error: AxiosError) => void | Promise; +} + +interface ResponseMap { + blob: Blob; + text: string; + arrayBuffer: ArrayBuffer; + stream: ReadableStream; + document: Document; +} +export type ResponseType = keyof ResponseMap | 'json'; + +export type MappedType = R extends keyof ResponseMap + ? ResponseMap[R] + : JsonType; + +export type CustomAxiosRequestConfig = Omit & { + responseType?: R; +}; + +export interface RequestInstanceCommon> { + /** + * cancel all request + * + * if the request provide abort controller sign from config, it will not collect in the abort controller map + */ + cancelAllRequest: () => void; + /** you can set custom state in the request instance */ + state: State; +} + +/** The request instance */ +export interface RequestInstance> extends RequestInstanceCommon { + ( + config: CustomAxiosRequestConfig + ): Promise>; +} + +export type FlatResponseSuccessData = { + data: ApiData; + error: null; + response: AxiosResponse; +}; + +export type FlatResponseFailData = { + data: null; + error: AxiosError; + response: AxiosResponse; +}; + +export type FlatResponseData = + | FlatResponseSuccessData + | FlatResponseFailData; + +export interface FlatRequestInstance> + extends RequestInstanceCommon { + ( + config: CustomAxiosRequestConfig + ): Promise>>; +} diff --git a/packages/axios/tsconfig.json b/packages/axios/tsconfig.json new file mode 100644 index 0000000..5823ed5 --- /dev/null +++ b/packages/axios/tsconfig.json @@ -0,0 +1,20 @@ +{ + "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"] +} diff --git a/packages/color/package.json b/packages/color/package.json new file mode 100644 index 0000000..151c79c --- /dev/null +++ b/packages/color/package.json @@ -0,0 +1,16 @@ +{ + "name": "@sa/color", + "version": "1.3.15", + "exports": { + ".": "./src/index.ts" + }, + "typesVersions": { + "*": { + "*": ["./src/*"] + } + }, + "dependencies": { + "@sa/utils": "workspace:*", + "colord": "2.9.3" + } +} diff --git a/packages/color/src/constant/index.ts b/packages/color/src/constant/index.ts new file mode 100644 index 0000000..cefbcdb --- /dev/null +++ b/packages/color/src/constant/index.ts @@ -0,0 +1,2 @@ +export * from './name'; +export * from './palette'; diff --git a/packages/color/src/constant/name.ts b/packages/color/src/constant/name.ts new file mode 100644 index 0000000..36648c3 --- /dev/null +++ b/packages/color/src/constant/name.ts @@ -0,0 +1,1579 @@ +export const colorNames: [hex: string, name: string][] = [ + ['#000000', 'Black'], + ['#000080', 'Navy Blue'], + ['#0000c8', 'Dark Blue'], + ['#0000ff', 'Blue'], + ['#000741', 'Stratos'], + ['#001b1c', 'Swamp'], + ['#002387', 'Resolution Blue'], + ['#002900', 'Deep Fir'], + ['#002e20', 'Burnham'], + ['#002fa7', 'International Klein Blue'], + ['#003153', 'Prussian Blue'], + ['#003366', 'Midnight Blue'], + ['#003399', 'Smalt'], + ['#003532', 'Deep Teal'], + ['#003e40', 'Cyprus'], + ['#004620', 'Kaitoke Green'], + ['#0047ab', 'Cobalt'], + ['#004816', 'Crusoe'], + ['#004950', 'Sherpa Blue'], + ['#0056a7', 'Endeavour'], + ['#00581a', 'Camarone'], + ['#0066cc', 'Science Blue'], + ['#0066ff', 'Blue Ribbon'], + ['#00755e', 'Tropical Rain Forest'], + ['#0076a3', 'Allports'], + ['#007ba7', 'Deep Cerulean'], + ['#007ec7', 'Lochmara'], + ['#007fff', 'Azure Radiance'], + ['#008080', 'Teal'], + ['#0095b6', 'Bondi Blue'], + ['#009dc4', 'Pacific Blue'], + ['#00a693', 'Persian Green'], + ['#00a86b', 'Jade'], + ['#00cc99', 'Caribbean Green'], + ['#00cccc', "Robin's Egg Blue"], + ['#00ff00', 'Green'], + ['#00ff7f', 'Spring Green'], + ['#00ffff', 'Cyan Aqua'], + ['#010d1a', 'Blue Charcoal'], + ['#011635', 'Midnight'], + ['#011d13', 'Holly'], + ['#012731', 'Daintree'], + ['#01361c', 'Cardin Green'], + ['#01371a', 'County Green'], + ['#013e62', 'Astronaut Blue'], + ['#013f6a', 'Regal Blue'], + ['#014b43', 'Aqua Deep'], + ['#015e85', 'Orient'], + ['#016162', 'Blue Stone'], + ['#016d39', 'Fun Green'], + ['#01796f', 'Pine Green'], + ['#017987', 'Blue Lagoon'], + ['#01826b', 'Deep Sea'], + ['#01a368', 'Green Haze'], + ['#022d15', 'English Holly'], + ['#02402c', 'Sherwood Green'], + ['#02478e', 'Congress Blue'], + ['#024e46', 'Evening Sea'], + ['#026395', 'Bahama Blue'], + ['#02866f', 'Observatory'], + ['#02a4d3', 'Cerulean'], + ['#03163c', 'Tangaroa'], + ['#032b52', 'Green Vogue'], + ['#036a6e', 'Mosque'], + ['#041004', 'Midnight Moss'], + ['#041322', 'Black Pearl'], + ['#042e4c', 'Blue Whale'], + ['#044022', 'Zuccini'], + ['#044259', 'Teal Blue'], + ['#051040', 'Deep Cove'], + ['#051657', 'Gulf Blue'], + ['#055989', 'Venice Blue'], + ['#056f57', 'Watercourse'], + ['#062a78', 'Catalina Blue'], + ['#063537', 'Tiber'], + ['#069b81', 'Gossamer'], + ['#06a189', 'Niagara'], + ['#073a50', 'Tarawera'], + ['#080110', 'Jaguar'], + ['#081910', 'Black Bean'], + ['#082567', 'Deep Sapphire'], + ['#088370', 'Elf Green'], + ['#08e8de', 'Bright Turquoise'], + ['#092256', 'Downriver'], + ['#09230f', 'Palm Green'], + ['#09255d', 'Madison'], + ['#093624', 'Bottle Green'], + ['#095859', 'Deep Sea Green'], + ['#097f4b', 'Salem'], + ['#0a001c', 'Black Russian'], + ['#0a480d', 'Dark Fern'], + ['#0a6906', 'Japanese Laurel'], + ['#0a6f75', 'Atoll'], + ['#0b0b0b', 'Cod Gray'], + ['#0b0f08', 'Marshland'], + ['#0b1107', 'Gordons Green'], + ['#0b1304', 'Black Forest'], + ['#0b6207', 'San Felix'], + ['#0bda51', 'Malachite'], + ['#0c0b1d', 'Ebony'], + ['#0c0d0f', 'Woodsmoke'], + ['#0c1911', 'Racing Green'], + ['#0c7a79', 'Surfie Green'], + ['#0c8990', 'Blue Chill'], + ['#0d0332', 'Black Rock'], + ['#0d1117', 'Bunker'], + ['#0d1c19', 'Aztec'], + ['#0d2e1c', 'Bush'], + ['#0e0e18', 'Cinder'], + ['#0e2a30', 'Firefly'], + ['#0f2d9e', 'Torea Bay'], + ['#10121d', 'Vulcan'], + ['#101405', 'Green Waterloo'], + ['#105852', 'Eden'], + ['#110c6c', 'Arapawa'], + ['#120a8f', 'Ultramarine'], + ['#123447', 'Elephant'], + ['#126b40', 'Jewel'], + ['#130000', 'Diesel'], + ['#130a06', 'Asphalt'], + ['#13264d', 'Blue Zodiac'], + ['#134f19', 'Parsley'], + ['#140600', 'Nero'], + ['#1450aa', 'Tory Blue'], + ['#151f4c', 'Bunting'], + ['#1560bd', 'Denim'], + ['#15736b', 'Genoa'], + ['#161928', 'Mirage'], + ['#161d10', 'Hunter Green'], + ['#162a40', 'Big Stone'], + ['#163222', 'Celtic'], + ['#16322c', 'Timber Green'], + ['#163531', 'Gable Green'], + ['#171f04', 'Pine Tree'], + ['#175579', 'Chathams Blue'], + ['#182d09', 'Deep Forest Green'], + ['#18587a', 'Blumine'], + ['#19330e', 'Palm Leaf'], + ['#193751', 'Nile Blue'], + ['#1959a8', 'Fun Blue'], + ['#1a1a68', 'Lucky Point'], + ['#1ab385', 'Mountain Meadow'], + ['#1b0245', 'Tolopea'], + ['#1b1035', 'Haiti'], + ['#1b127b', 'Deep Koamaru'], + ['#1b1404', 'Acadia'], + ['#1b2f11', 'Seaweed'], + ['#1b3162', 'Biscay'], + ['#1b659d', 'Matisse'], + ['#1c1208', 'Crowshead'], + ['#1c1e13', 'Rangoon Green'], + ['#1c39bb', 'Persian Blue'], + ['#1c402e', 'Everglade'], + ['#1c7c7d', 'Elm'], + ['#1d6142', 'Green Pea'], + ['#1e0f04', 'Creole'], + ['#1e1609', 'Karaka'], + ['#1e1708', 'El Paso'], + ['#1e385b', 'Cello'], + ['#1e433c', 'Te Papa Green'], + ['#1e90ff', 'Dodger Blue'], + ['#1e9ab0', 'Eastern Blue'], + ['#1f120f', 'Night Rider'], + ['#1fc2c2', 'Java'], + ['#20208d', 'Jacksons Purple'], + ['#202e54', 'Cloud Burst'], + ['#204852', 'Blue Dianne'], + ['#211a0e', 'Eternity'], + ['#220878', 'Deep Blue'], + ['#228b22', 'Forest Green'], + ['#233418', 'Mallard'], + ['#240a40', 'Violet'], + ['#240c02', 'Kilamanjaro'], + ['#242a1d', 'Log Cabin'], + ['#242e16', 'Black Olive'], + ['#24500f', 'Green House'], + ['#251607', 'Graphite'], + ['#251706', 'Cannon Black'], + ['#251f4f', 'Port Gore'], + ['#25272c', 'Shark'], + ['#25311c', 'Green Kelp'], + ['#2596d1', 'Curious Blue'], + ['#260368', 'Paua'], + ['#26056a', 'Paris M'], + ['#261105', 'Wood Bark'], + ['#261414', 'Gondola'], + ['#262335', 'Steel Gray'], + ['#26283b', 'Ebony Clay'], + ['#273a81', 'Bay Of Many'], + ['#27504b', 'Plantation'], + ['#278a5b', 'Eucalyptus'], + ['#281e15', 'Oil'], + ['#283a77', 'Astronaut'], + ['#286acd', 'Mariner'], + ['#290c5e', 'Violent Violet'], + ['#292130', 'Bastille'], + ['#292319', 'Zeus'], + ['#292937', 'Charade'], + ['#297b9a', 'Jelly Bean'], + ['#29ab87', 'Jungle Green'], + ['#2a0359', 'Cherry Pie'], + ['#2a140e', 'Coffee Bean'], + ['#2a2630', 'Baltic Sea'], + ['#2a380b', 'Turtle Green'], + ['#2a52be', 'Cerulean Blue'], + ['#2b0202', 'Sepia Black'], + ['#2b194f', 'Valhalla'], + ['#2b3228', 'Heavy Metal'], + ['#2c0e8c', 'Blue Gem'], + ['#2c1632', 'Revolver'], + ['#2c2133', 'Bleached Cedar'], + ['#2c8c84', 'Lochinvar'], + ['#2d2510', 'Mikado'], + ['#2d383a', 'Outer Space'], + ['#2d569b', 'St Tropaz'], + ['#2e0329', 'Jacaranda'], + ['#2e1905', 'Jacko Bean'], + ['#2e3222', 'Rangitoto'], + ['#2e3f62', 'Rhino'], + ['#2e8b57', 'Sea Green'], + ['#2ebfd4', 'Scooter'], + ['#2f270e', 'Onion'], + ['#2f3cb3', 'Governor Bay'], + ['#2f519e', 'Sapphire'], + ['#2f5a57', 'Spectra'], + ['#2f6168', 'Casal'], + ['#300529', 'Melanzane'], + ['#301f1e', 'Cocoa Brown'], + ['#302a0f', 'Woodrush'], + ['#304b6a', 'San Juan'], + ['#30d5c8', 'Turquoise'], + ['#311c17', 'Eclipse'], + ['#314459', 'Pickled Bluewood'], + ['#315ba1', 'Azure'], + ['#31728d', 'Calypso'], + ['#317d82', 'Paradiso'], + ['#32127a', 'Persian Indigo'], + ['#32293a', 'Blackcurrant'], + ['#323232', 'Mine Shaft'], + ['#325d52', 'Stromboli'], + ['#327c14', 'Bilbao'], + ['#327da0', 'Astral'], + ['#33036b', 'Christalle'], + ['#33292f', 'Thunder'], + ['#33cc99', 'Shamrock'], + ['#341515', 'Tamarind'], + ['#350036', 'Mardi Gras'], + ['#350e42', 'Valentino'], + ['#350e57', 'Jagger'], + ['#353542', 'Tuna'], + ['#354e8c', 'Chambray'], + ['#363050', 'Martinique'], + ['#363534', 'Tuatara'], + ['#363c0d', 'Waiouru'], + ['#36747d', 'Ming'], + ['#368716', 'La Palma'], + ['#370202', 'Chocolate'], + ['#371d09', 'Clinker'], + ['#37290e', 'Brown Tumbleweed'], + ['#373021', 'Birch'], + ['#377475', 'Oracle'], + ['#380474', 'Blue Diamond'], + ['#381a51', 'Grape'], + ['#383533', 'Dune'], + ['#384555', 'Oxford Blue'], + ['#384910', 'Clover'], + ['#394851', 'Limed Spruce'], + ['#396413', 'Dell'], + ['#3a0020', 'Toledo'], + ['#3a2010', 'Sambuca'], + ['#3a2a6a', 'Jacarta'], + ['#3a686c', 'William'], + ['#3a6a47', 'Killarney'], + ['#3ab09e', 'Keppel'], + ['#3b000b', 'Temptress'], + ['#3b0910', 'Aubergine'], + ['#3b1f1f', 'Jon'], + ['#3b2820', 'Treehouse'], + ['#3b7a57', 'Amazon'], + ['#3b91b4', 'Boston Blue'], + ['#3c0878', 'Windsor'], + ['#3c1206', 'Rebel'], + ['#3c1f76', 'Meteorite'], + ['#3c2005', 'Dark Ebony'], + ['#3c3910', 'Camouflage'], + ['#3c4151', 'Bright Gray'], + ['#3c4443', 'Cape Cod'], + ['#3c493a', 'Lunar Green'], + ['#3d0c02', 'Bean '], + ['#3d2b1f', 'Bistre'], + ['#3d7d52', 'Goblin'], + ['#3e0480', 'Kingfisher Daisy'], + ['#3e1c14', 'Cedar'], + ['#3e2b23', 'English Walnut'], + ['#3e2c1c', 'Black Marlin'], + ['#3e3a44', 'Ship Gray'], + ['#3eabbf', 'Pelorous'], + ['#3f2109', 'Bronze'], + ['#3f2500', 'Cola'], + ['#3f3002', 'Madras'], + ['#3f307f', 'Minsk'], + ['#3f4c3a', 'Cabbage Pont'], + ['#3f583b', 'Tom Thumb'], + ['#3f5d53', 'Mineral Green'], + ['#3fc1aa', 'Puerto Rico'], + ['#3fff00', 'Harlequin'], + ['#401801', 'Brown Pod'], + ['#40291d', 'Cork'], + ['#403b38', 'Masala'], + ['#403d19', 'Thatch Green'], + ['#405169', 'Fiord'], + ['#40826d', 'Viridian'], + ['#40a860', 'Chateau Green'], + ['#410056', 'Ripe Plum'], + ['#411f10', 'Paco'], + ['#412010', 'Deep Oak'], + ['#413c37', 'Merlin'], + ['#414257', 'Gun Powder'], + ['#414c7d', 'East Bay'], + ['#4169e1', 'Royal Blue'], + ['#41aa78', 'Ocean Green'], + ['#420303', 'Burnt Maroon'], + ['#423921', 'Lisbon Brown'], + ['#427977', 'Faded Jade'], + ['#431560', 'Scarlet Gum'], + ['#433120', 'Iroko'], + ['#433e37', 'Armadillo'], + ['#434c59', 'River Bed'], + ['#436a0d', 'Green Leaf'], + ['#44012d', 'Barossa'], + ['#441d00', 'Morocco Brown'], + ['#444954', 'Mako'], + ['#454936', 'Kelp'], + ['#456cac', 'San Marino'], + ['#45b1e8', 'Picton Blue'], + ['#460b41', 'Loulou'], + ['#462425', 'Crater Brown'], + ['#465945', 'Gray Asparagus'], + ['#4682b4', 'Steel Blue'], + ['#480404', 'Rustic Red'], + ['#480607', 'Bulgarian Rose'], + ['#480656', 'Clairvoyant'], + ['#481c1c', 'Cocoa Bean'], + ['#483131', 'Woody Brown'], + ['#483c32', 'Taupe'], + ['#49170c', 'Van Cleef'], + ['#492615', 'Brown Derby'], + ['#49371b', 'Metallic Bronze'], + ['#495400', 'Verdun Green'], + ['#496679', 'Blue Bayoux'], + ['#497183', 'Bismark'], + ['#4a2a04', 'Bracken'], + ['#4a3004', 'Deep Bronze'], + ['#4a3c30', 'Mondo'], + ['#4a4244', 'Tundora'], + ['#4a444b', 'Gravel'], + ['#4a4e5a', 'Trout'], + ['#4b0082', 'Pigment Indigo'], + ['#4b5d52', 'Nandor'], + ['#4c3024', 'Saddle'], + ['#4c4f56', 'Abbey'], + ['#4d0135', 'Blackberry'], + ['#4d0a18', 'Cab Sav'], + ['#4d1e01', 'Indian Tan'], + ['#4d282d', 'Cowboy'], + ['#4d282e', 'Livid Brown'], + ['#4d3833', 'Rock'], + ['#4d3d14', 'Punga'], + ['#4d400f', 'Bronzetone'], + ['#4d5328', 'Woodland'], + ['#4e0606', 'Mahogany'], + ['#4e2a5a', 'Bossanova'], + ['#4e3b41', 'Matterhorn'], + ['#4e420c', 'Bronze Olive'], + ['#4e4562', 'Mulled Wine'], + ['#4e6649', 'Axolotl'], + ['#4e7f9e', 'Wedgewood'], + ['#4eabd1', 'Shakespeare'], + ['#4f1c70', 'Honey Flower'], + ['#4f2398', 'Daisy Bush'], + ['#4f69c6', 'Indigo'], + ['#4f7942', 'Fern Green'], + ['#4f9d5d', 'Fruit Salad'], + ['#4fa83d', 'Apple'], + ['#504351', 'Mortar'], + ['#507096', 'Kashmir Blue'], + ['#507672', 'Cutty Sark'], + ['#50c878', 'Emerald'], + ['#514649', 'Emperor'], + ['#516e3d', 'Chalet Green'], + ['#517c66', 'Como'], + ['#51808f', 'Smalt Blue'], + ['#52001f', 'Castro'], + ['#520c17', 'Maroon Oak'], + ['#523c94', 'Gigas'], + ['#533455', 'Voodoo'], + ['#534491', 'Victoria'], + ['#53824b', 'Hippie Green'], + ['#541012', 'Heath'], + ['#544333', 'Judge Gray'], + ['#54534d', 'Fuscous Gray'], + ['#549019', 'Vida Loca'], + ['#55280c', 'Cioccolato'], + ['#555b10', 'Saratoga'], + ['#556d56', 'Finlandia'], + ['#5590d9', 'Havelock Blue'], + ['#56b4be', 'Fountain Blue'], + ['#578363', 'Spring Leaves'], + ['#583401', 'Saddle Brown'], + ['#585562', 'Scarpa Flow'], + ['#587156', 'Cactus'], + ['#589aaf', 'Hippie Blue'], + ['#591d35', 'Wine Berry'], + ['#592804', 'Brown Bramble'], + ['#593737', 'Congo Brown'], + ['#594433', 'Millbrook'], + ['#5a6e9c', 'Waikawa Gray'], + ['#5a87a0', 'Horizon'], + ['#5b3013', 'Jambalaya'], + ['#5c0120', 'Bordeaux'], + ['#5c0536', 'Mulberry Wood'], + ['#5c2e01', 'Carnaby Tan'], + ['#5c5d75', 'Comet'], + ['#5d1e0f', 'Redwood'], + ['#5d4c51', 'Don Juan'], + ['#5d5c58', 'Chicago'], + ['#5d5e37', 'Verdigris'], + ['#5d7747', 'Dingley'], + ['#5da19f', 'Breaker Bay'], + ['#5e483e', 'Kabul'], + ['#5e5d3b', 'Hemlock'], + ['#5f3d26', 'Irish Coffee'], + ['#5f5f6e', 'Mid Gray'], + ['#5f6672', 'Shuttle Gray'], + ['#5fa777', 'Aqua Forest'], + ['#5fb3ac', 'Tradewind'], + ['#604913', 'Horses Neck'], + ['#605b73', 'Smoky'], + ['#606e68', 'Corduroy'], + ['#6093d1', 'Danube'], + ['#612718', 'Espresso'], + ['#614051', 'Eggplant'], + ['#615d30', 'Costa Del Sol'], + ['#61845f', 'Glade Green'], + ['#622f30', 'Buccaneer'], + ['#623f2d', 'Quincy'], + ['#624e9a', 'Butterfly Bush'], + ['#625119', 'West Coast'], + ['#626649', 'Finch'], + ['#639a8f', 'Patina'], + ['#63b76c', 'Fern'], + ['#6456b7', 'Blue Violet'], + ['#646077', 'Dolphin'], + ['#646463', 'Storm Dust'], + ['#646a54', 'Siam'], + ['#646e75', 'Nevada'], + ['#6495ed', 'Cornflower Blue'], + ['#64ccdb', 'Viking'], + ['#65000b', 'Rosewood'], + ['#651a14', 'Cherrywood'], + ['#652dc1', 'Purple Heart'], + ['#657220', 'Fern Frond'], + ['#65745d', 'Willow Grove'], + ['#65869f', 'Hoki'], + ['#660045', 'Pompadour'], + ['#660099', 'Purple'], + ['#66023c', 'Tyrian Purple'], + ['#661010', 'Dark Tan'], + ['#66b58f', 'Silver Tree'], + ['#66ff00', 'Bright Green'], + ['#66ff66', 'Screamin Green'], + ['#67032d', 'Black Rose'], + ['#675fa6', 'Scampi'], + ['#676662', 'Ironside Gray'], + ['#678975', 'Viridian Green'], + ['#67a712', 'Christi'], + ['#683600', 'Nutmeg Wood Finish'], + ['#685558', 'Zambezi'], + ['#685e6e', 'Salt Box'], + ['#692545', 'Tawny Port'], + ['#692d54', 'Finn'], + ['#695f62', 'Scorpion'], + ['#697e9a', 'Lynch'], + ['#6a442e', 'Spice'], + ['#6a5d1b', 'Himalaya'], + ['#6a6051', 'Soya Bean'], + ['#6b2a14', 'Hairy Heath'], + ['#6b3fa0', 'Royal Purple'], + ['#6b4e31', 'Shingle Fawn'], + ['#6b5755', 'Dorado'], + ['#6b8ba2', 'Bermuda Gray'], + ['#6b8e23', 'Olive Drab'], + ['#6c3082', 'Eminence'], + ['#6cdae7', 'Turquoise Blue'], + ['#6d0101', 'Lonestar'], + ['#6d5e54', 'Pine Cone'], + ['#6d6c6c', 'Dove Gray'], + ['#6d9292', 'Juniper'], + ['#6d92a1', 'Gothic'], + ['#6e0902', 'Red Oxide'], + ['#6e1d14', 'Moccaccino'], + ['#6e4826', 'Pickled Bean'], + ['#6e4b26', 'Dallas'], + ['#6e6d57', 'Kokoda'], + ['#6e7783', 'Pale Sky'], + ['#6f440c', 'Cafe Royale'], + ['#6f6a61', 'Flint'], + ['#6f8e63', 'Highland'], + ['#6f9d02', 'Limeade'], + ['#6fd0c5', 'Downy'], + ['#701c1c', 'Persian Plum'], + ['#704214', 'Sepia'], + ['#704a07', 'Antique Bronze'], + ['#704f50', 'Ferra'], + ['#706555', 'Coffee'], + ['#708090', 'Slate Gray'], + ['#711a00', 'Cedar Wood Finish'], + ['#71291d', 'Metallic Copper'], + ['#714693', 'Affair'], + ['#714ab2', 'Studio'], + ['#715d47', 'Tobacco Brown'], + ['#716338', 'Yellow Metal'], + ['#716b56', 'Peat'], + ['#716e10', 'Olivetone'], + ['#717486', 'Storm Gray'], + ['#718080', 'Sirocco'], + ['#71d9e2', 'Aquamarine Blue'], + ['#72010f', 'Venetian Red'], + ['#724a2f', 'Old Copper'], + ['#726d4e', 'Go Ben'], + ['#727b89', 'Raven'], + ['#731e8f', 'Seance'], + ['#734a12', 'Raw Umber'], + ['#736c9f', 'Kimberly'], + ['#736d58', 'Crocodile'], + ['#737829', 'Crete'], + ['#738678', 'Xanadu'], + ['#74640d', 'Spicy Mustard'], + ['#747d63', 'Limed Ash'], + ['#747d83', 'Rolling Stone'], + ['#748881', 'Blue Smoke'], + ['#749378', 'Laurel'], + ['#74c365', 'Mantis'], + ['#755a57', 'Russett'], + ['#7563a8', 'Deluge'], + ['#76395d', 'Cosmic'], + ['#7666c6', 'Blue Marguerite'], + ['#76bd17', 'Lima'], + ['#76d7ea', 'Sky Blue'], + ['#770f05', 'Dark Burgundy'], + ['#771f1f', 'Crown Of Thorns'], + ['#773f1a', 'Walnut'], + ['#776f61', 'Pablo'], + ['#778120', 'Pacifika'], + ['#779e86', 'Oxley'], + ['#77dd77', 'Pastel Green'], + ['#780109', 'Japanese Maple'], + ['#782d19', 'Mocha'], + ['#782f16', 'Peanut'], + ['#78866b', 'Camouflage Green'], + ['#788a25', 'Wasabi'], + ['#788bba', 'Ship Cove'], + ['#78a39c', 'Sea Nymph'], + ['#795d4c', 'Roman Coffee'], + ['#796878', 'Old Lavender'], + ['#796989', 'Rum'], + ['#796a78', 'Fedora'], + ['#796d62', 'Sandstone'], + ['#79deec', 'Spray'], + ['#7a013a', 'Siren'], + ['#7a58c1', 'Fuchsia Blue'], + ['#7a7a7a', 'Boulder'], + ['#7a89b8', 'Wild Blue Yonder'], + ['#7ac488', 'De York'], + ['#7b3801', 'Red Beech'], + ['#7b3f00', 'Cinnamon'], + ['#7b6608', 'Yukon Gold'], + ['#7b7874', 'Tapa'], + ['#7b7c94', 'Waterloo '], + ['#7b8265', 'Flax Smoke'], + ['#7b9f80', 'Amulet'], + ['#7ba05b', 'Asparagus'], + ['#7c1c05', 'Kenyan Copper'], + ['#7c7631', 'Pesto'], + ['#7c778a', 'Topaz'], + ['#7c7b7a', 'Concord'], + ['#7c7b82', 'Jumbo'], + ['#7c881a', 'Trendy Green'], + ['#7ca1a6', 'Gumbo'], + ['#7cb0a1', 'Acapulco'], + ['#7cb7bb', 'Neptune'], + ['#7d2c14', 'Pueblo'], + ['#7da98d', 'Bay Leaf'], + ['#7dc8f7', 'Malibu'], + ['#7dd8c6', 'Bermuda'], + ['#7e3a15', 'Copper Canyon'], + ['#7f1734', 'Claret'], + ['#7f3a02', 'Peru Tan'], + ['#7f626d', 'Falcon'], + ['#7f7589', 'Mobster'], + ['#7f76d3', 'Moody Blue'], + ['#7fff00', 'Chartreuse'], + ['#7fffd4', 'Aquamarine'], + ['#800000', 'Maroon'], + ['#800b47', 'Rose Bud Cherry'], + ['#801818', 'Falu Red'], + ['#80341f', 'Red Robin'], + ['#803790', 'Vivid Violet'], + ['#80461b', 'Russet'], + ['#807e79', 'Friar Gray'], + ['#808000', 'Olive'], + ['#808080', 'Gray'], + ['#80b3ae', 'Gulf Stream'], + ['#80b3c4', 'Glacier'], + ['#80ccea', 'Seagull'], + ['#81422c', 'Nutmeg'], + ['#816e71', 'Spicy Pink'], + ['#817377', 'Empress'], + ['#819885', 'Spanish Green'], + ['#826f65', 'Sand Dune'], + ['#828685', 'Gunsmoke'], + ['#828f72', 'Battleship Gray'], + ['#831923', 'Merlot'], + ['#837050', 'Shadow'], + ['#83aa5d', 'Chelsea Cucumber'], + ['#83d0c6', 'Monte Carlo'], + ['#843179', 'Plum'], + ['#84a0a0', 'Granny Smith'], + ['#8581d9', 'Chetwode Blue'], + ['#858470', 'Bandicoot'], + ['#859faf', 'Bali Hai'], + ['#85c4cc', 'Half Baked'], + ['#860111', 'Red Devil'], + ['#863c3c', 'Lotus'], + ['#86483c', 'Ironstone'], + ['#864d1e', 'Bull Shot'], + ['#86560a', 'Rusty Nail'], + ['#868974', 'Bitter'], + ['#86949f', 'Regent Gray'], + ['#871550', 'Disco'], + ['#87756e', 'Americano'], + ['#877c7b', 'Hurricane'], + ['#878d91', 'Oslo Gray'], + ['#87ab39', 'Sushi'], + ['#885342', 'Spicy Mix'], + ['#886221', 'Kumera'], + ['#888387', 'Suva Gray'], + ['#888d65', 'Avocado'], + ['#893456', 'Camelot'], + ['#893843', 'Solid Pink'], + ['#894367', 'Cannon Pink'], + ['#897d6d', 'Makara'], + ['#8a3324', 'Burnt Umber'], + ['#8a73d6', 'True V'], + ['#8a8360', 'Clay Creek'], + ['#8a8389', 'Monsoon'], + ['#8a8f8a', 'Stack'], + ['#8ab9f1', 'Jordy Blue'], + ['#8b00ff', 'Electric Violet'], + ['#8b0723', 'Monarch'], + ['#8b6b0b', 'Corn Harvest'], + ['#8b8470', 'Olive Haze'], + ['#8b847e', 'Schooner'], + ['#8b8680', 'Natural Gray'], + ['#8b9c90', 'Mantle'], + ['#8b9fee', 'Portage'], + ['#8ba690', 'Envy'], + ['#8ba9a5', 'Cascade'], + ['#8be6d8', 'Riptide'], + ['#8c055e', 'Cardinal Pink'], + ['#8c472f', 'Mule Fawn'], + ['#8c5738', 'Potters Clay'], + ['#8c6495', 'Trendy Pink'], + ['#8d0226', 'Paprika'], + ['#8d3d38', 'Sanguine Brown'], + ['#8d3f3f', 'Tosca'], + ['#8d7662', 'Cement'], + ['#8d8974', 'Granite Green'], + ['#8d90a1', 'Manatee'], + ['#8da8cc', 'Polo Blue'], + ['#8e0000', 'Red Berry'], + ['#8e4d1e', 'Rope'], + ['#8e6f70', 'Opium'], + ['#8e775e', 'Domino'], + ['#8e8190', 'Mamba'], + ['#8eabc1', 'Nepal'], + ['#8f021c', 'Pohutukawa'], + ['#8f3e33', 'El Salva'], + ['#8f4b0e', 'Korma'], + ['#8f8176', 'Squirrel'], + ['#8fd6b4', 'Vista Blue'], + ['#900020', 'Burgundy'], + ['#901e1e', 'Old Brick'], + ['#907874', 'Hemp'], + ['#907b71', 'Almond Frost'], + ['#908d39', 'Sycamore'], + ['#92000a', 'Sangria'], + ['#924321', 'Cumin'], + ['#926f5b', 'Beaver'], + ['#928573', 'Stonewall'], + ['#928590', 'Venus'], + ['#9370db', 'Medium Purple'], + ['#93ccea', 'Cornflower'], + ['#93dfb8', 'Algae Green'], + ['#944747', 'Copper Rust'], + ['#948771', 'Arrowtown'], + ['#950015', 'Scarlett'], + ['#956387', 'Strikemaster'], + ['#959396', 'Mountain Mist'], + ['#960018', 'Carmine'], + ['#964b00', 'Brown'], + ['#967059', 'Leather'], + ['#9678b6', "Purple Mountain's Majesty"], + ['#967bb6', 'Lavender Purple'], + ['#96a8a1', 'Pewter'], + ['#96bbab', 'Summer Green'], + ['#97605d', 'Au Chico'], + ['#9771b5', 'Wisteria'], + ['#97cd2d', 'Atlantis'], + ['#983d61', 'Vin Rouge'], + ['#9874d3', 'Lilac Bush'], + ['#98777b', 'Bazaar'], + ['#98811b', 'Hacienda'], + ['#988d77', 'Pale Oyster'], + ['#98ff98', 'Mint Green'], + ['#990066', 'Fresh Eggplant'], + ['#991199', 'Violet Eggplant'], + ['#991613', 'Tamarillo'], + ['#991b07', 'Totem Pole'], + ['#996666', 'Copper Rose'], + ['#9966cc', 'Amethyst'], + ['#997a8d', 'Mountbatten Pink'], + ['#9999cc', 'Blue Bell'], + ['#9a3820', 'Prairie Sand'], + ['#9a6e61', 'Toast'], + ['#9a9577', 'Gurkha'], + ['#9ab973', 'Olivine'], + ['#9ac2b8', 'Shadow Green'], + ['#9b4703', 'Oregon'], + ['#9b9e8f', 'Lemon Grass'], + ['#9c3336', 'Stiletto'], + ['#9d5616', 'Hawaiian Tan'], + ['#9dacb7', 'Gull Gray'], + ['#9dc209', 'Pistachio'], + ['#9de093', 'Granny Smith Apple'], + ['#9de5ff', 'Anakiwa'], + ['#9e5302', 'Chelsea Gem'], + ['#9e5b40', 'Sepia Skin'], + ['#9ea587', 'Sage'], + ['#9ea91f', 'Citron'], + ['#9eb1cd', 'Rock Blue'], + ['#9edee0', 'Morning Glory'], + ['#9f381d', 'Cognac'], + ['#9f821c', 'Reef Gold'], + ['#9f9f9c', 'Star Dust'], + ['#9fa0b1', 'Santas Gray'], + ['#9fd7d3', 'Sinbad'], + ['#9fdd8c', 'Feijoa'], + ['#a02712', 'Tabasco'], + ['#a1750d', 'Buttered Rum'], + ['#a1adb5', 'Hit Gray'], + ['#a1c50a', 'Citrus'], + ['#a1dad7', 'Aqua Island'], + ['#a1e9de', 'Water Leaf'], + ['#a2006d', 'Flirt'], + ['#a23b6c', 'Rouge'], + ['#a26645', 'Cape Palliser'], + ['#a2aab3', 'Gray Chateau'], + ['#a2aeab', 'Edward'], + ['#a3807b', 'Pharlap'], + ['#a397b4', 'Amethyst Smoke'], + ['#a3e3ed', 'Blizzard Blue'], + ['#a4a49d', 'Delta'], + ['#a4a6d3', 'Wistful'], + ['#a4af6e', 'Green Smoke'], + ['#a50b5e', 'Jazzberry Jam'], + ['#a59b91', 'Zorba'], + ['#a5cb0c', 'Bahia'], + ['#a62f20', 'Roof Terracotta'], + ['#a65529', 'Paarl'], + ['#a68b5b', 'Barley Corn'], + ['#a69279', 'Donkey Brown'], + ['#a6a29a', 'Dawn'], + ['#a72525', 'Mexican Red'], + ['#a7882c', 'Luxor Gold'], + ['#a85307', 'Rich Gold'], + ['#a86515', 'Reno Sand'], + ['#a86b6b', 'Coral Tree'], + ['#a8989b', 'Dusty Gray'], + ['#a899e6', 'Dull Lavender'], + ['#a8a589', 'Tallow'], + ['#a8ae9c', 'Bud'], + ['#a8af8e', 'Locust'], + ['#a8bd9f', 'Norway'], + ['#a8e3bd', 'Chinook'], + ['#a9a491', 'Gray Olive'], + ['#a9acb6', 'Aluminium'], + ['#a9b2c3', 'Cadet Blue'], + ['#a9b497', 'Schist'], + ['#a9bdbf', 'Tower Gray'], + ['#a9bef2', 'Perano'], + ['#a9c6c2', 'Opal'], + ['#aa375a', 'Night Shadz'], + ['#aa4203', 'Fire'], + ['#aa8b5b', 'Muesli'], + ['#aa8d6f', 'Sandal'], + ['#aaa5a9', 'Shady Lady'], + ['#aaa9cd', 'Logan'], + ['#aaabb7', 'Spun Pearl'], + ['#aad6e6', 'Regent St Blue'], + ['#aaf0d1', 'Magic Mint'], + ['#ab0563', 'Lipstick'], + ['#ab3472', 'Royal Heath'], + ['#ab917a', 'Sandrift'], + ['#aba0d9', 'Cold Purple'], + ['#aba196', 'Bronco'], + ['#ac8a56', 'Limed Oak'], + ['#ac91ce', 'East Side'], + ['#ac9e22', 'Lemon Ginger'], + ['#aca494', 'Napa'], + ['#aca586', 'Hillary'], + ['#aca59f', 'Cloudy'], + ['#acacac', 'Silver Chalice'], + ['#acb78e', 'Swamp Green'], + ['#accbb1', 'Spring Rain'], + ['#acdd4d', 'Conifer'], + ['#ace1af', 'Celadon'], + ['#ad781b', 'Mandalay'], + ['#adbed1', 'Casper'], + ['#addfad', 'Moss Green'], + ['#ade6c4', 'Padua'], + ['#adff2f', 'Green Yellow'], + ['#ae4560', 'Hippie Pink'], + ['#ae6020', 'Desert'], + ['#ae809e', 'Bouquet'], + ['#af4035', 'Medium Carmine'], + ['#af4d43', 'Apple Blossom'], + ['#af593e', 'Brown Rust'], + ['#af8751', 'Driftwood'], + ['#af8f2c', 'Alpine'], + ['#af9f1c', 'Lucky'], + ['#afa09e', 'Martini'], + ['#afb1b8', 'Bombay'], + ['#afbdd9', 'Pigeon Post'], + ['#b04c6a', 'Cadillac'], + ['#b05d54', 'Matrix'], + ['#b05e81', 'Tapestry'], + ['#b06608', 'Mai Tai'], + ['#b09a95', 'Del Rio'], + ['#b0e0e6', 'Powder Blue'], + ['#b0e313', 'Inch Worm'], + ['#b10000', 'Bright Red'], + ['#b14a0b', 'Vesuvius'], + ['#b1610b', 'Pumpkin Skin'], + ['#b16d52', 'Santa Fe'], + ['#b19461', 'Teak'], + ['#b1e2c1', 'Fringy Flower'], + ['#b1f4e7', 'Ice Cold'], + ['#b20931', 'Shiraz'], + ['#b2a1ea', 'Biloba Flower'], + ['#b32d29', 'Tall Poppy'], + ['#b35213', 'Fiery Orange'], + ['#b38007', 'Hot Toddy'], + ['#b3af95', 'Taupe Gray'], + ['#b3c110', 'La Rioja'], + ['#b43332', 'Well Read'], + ['#b44668', 'Blush'], + ['#b4cfd3', 'Jungle Mist'], + ['#b57281', 'Turkish Rose'], + ['#b57edc', 'Lavender'], + ['#b5a27f', 'Mongoose'], + ['#b5b35c', 'Olive Green'], + ['#b5d2ce', 'Jet Stream'], + ['#b5ecdf', 'Cruise'], + ['#b6316c', 'Hibiscus'], + ['#b69d98', 'Thatch'], + ['#b6b095', 'Heathered Gray'], + ['#b6baa4', 'Eagle'], + ['#b6d1ea', 'Spindle'], + ['#b6d3bf', 'Gum Leaf'], + ['#b7410e', 'Rust'], + ['#b78e5c', 'Muddy Waters'], + ['#b7a214', 'Sahara'], + ['#b7a458', 'Husk'], + ['#b7b1b1', 'Nobel'], + ['#b7c3d0', 'Heather'], + ['#b7f0be', 'Madang'], + ['#b81104', 'Milano Red'], + ['#b87333', 'Copper'], + ['#b8b56a', 'Gimblet'], + ['#b8c1b1', 'Green Spring'], + ['#b8c25d', 'Celery'], + ['#b8e0f9', 'Sail'], + ['#b94e48', 'Chestnut'], + ['#b95140', 'Crail'], + ['#b98d28', 'Marigold'], + ['#b9c46a', 'Wild Willow'], + ['#b9c8ac', 'Rainee'], + ['#ba0101', 'Guardsman Red'], + ['#ba450c', 'Rock Spray'], + ['#ba6f1e', 'Bourbon'], + ['#ba7f03', 'Pirate Gold'], + ['#bab1a2', 'Nomad'], + ['#bac7c9', 'Submarine'], + ['#baeef9', 'Charlotte'], + ['#bb3385', 'Medium Red Violet'], + ['#bb8983', 'Brandy Rose'], + ['#bbd009', 'Rio Grande'], + ['#bbd7c1', 'Surf'], + ['#bcc9c2', 'Powder Ash'], + ['#bd5e2e', 'Tuscany'], + ['#bd978e', 'Quicksand'], + ['#bdb1a8', 'Silk'], + ['#bdb2a1', 'Malta'], + ['#bdb3c7', 'Chatelle'], + ['#bdbbd7', 'Lavender Gray'], + ['#bdbdc6', 'French Gray'], + ['#bdc8b3', 'Clay Ash'], + ['#bdc9ce', 'Loblolly'], + ['#bdedfd', 'French Pass'], + ['#bea6c3', 'London Hue'], + ['#beb5b7', 'Pink Swan'], + ['#bede0d', 'Fuego'], + ['#bf5500', 'Rose Of Sharon'], + ['#bfb8b0', 'Tide'], + ['#bfbed8', 'Blue Haze'], + ['#bfc1c2', 'Silver Sand'], + ['#bfc921', 'Key Lime Pie'], + ['#bfdbe2', 'Ziggurat'], + ['#bfff00', 'Lime'], + ['#c02b18', 'Thunderbird'], + ['#c04737', 'Mojo'], + ['#c08081', 'Old Rose'], + ['#c0c0c0', 'Silver'], + ['#c0d3b9', 'Pale Leaf'], + ['#c0d8b6', 'Pixie Green'], + ['#c1440e', 'Tia Maria'], + ['#c154c1', 'Fuchsia Pink'], + ['#c1a004', 'Buddha Gold'], + ['#c1b7a4', 'Bison Hide'], + ['#c1bab0', 'Tea'], + ['#c1becd', 'Gray Suit'], + ['#c1d7b0', 'Sprout'], + ['#c1f07c', 'Sulu'], + ['#c26b03', 'Indochine'], + ['#c2955d', 'Twine'], + ['#c2bdb6', 'Cotton Seed'], + ['#c2cac4', 'Pumice'], + ['#c2e8e5', 'Jagged Ice'], + ['#c32148', 'Maroon Flush'], + ['#c3b091', 'Indian Khaki'], + ['#c3bfc1', 'Pale Slate'], + ['#c3c3bd', 'Gray Nickel'], + ['#c3cde6', 'Periwinkle Gray'], + ['#c3d1d1', 'Tiara'], + ['#c3ddf9', 'Tropical Blue'], + ['#c41e3a', 'Cardinal'], + ['#c45655', 'Fuzzy Wuzzy Brown'], + ['#c45719', 'Orange Roughy'], + ['#c4c4bc', 'Mist Gray'], + ['#c4d0b0', 'Coriander'], + ['#c4f4eb', 'Mint Tulip'], + ['#c54b8c', 'Mulberry'], + ['#c59922', 'Nugget'], + ['#c5994b', 'Tussock'], + ['#c5dbca', 'Sea Mist'], + ['#c5e17a', 'Yellow Green'], + ['#c62d42', 'Brick Red'], + ['#c6726b', 'Contessa'], + ['#c69191', 'Oriental Pink'], + ['#c6a84b', 'Roti'], + ['#c6c3b5', 'Ash'], + ['#c6c8bd', 'Kangaroo'], + ['#c6e610', 'Las Palmas'], + ['#c7031e', 'Monza'], + ['#c71585', 'Red Violet'], + ['#c7bca2', 'Coral Reef'], + ['#c7c1ff', 'Melrose'], + ['#c7c4bf', 'Cloud'], + ['#c7c9d5', 'Ghost'], + ['#c7cd90', 'Pine Glade'], + ['#c7dde5', 'Botticelli'], + ['#c88a65', 'Antique Brass'], + ['#c8a2c8', 'Lilac'], + ['#c8a528', 'Hokey Pokey'], + ['#c8aabf', 'Lily'], + ['#c8b568', 'Laser'], + ['#c8e3d7', 'Edgewater'], + ['#c96323', 'Piper'], + ['#c99415', 'Pizza'], + ['#c9a0dc', 'Light Wisteria'], + ['#c9b29b', 'Rodeo Dust'], + ['#c9b35b', 'Sundance'], + ['#c9b93b', 'Earls Green'], + ['#c9c0bb', 'Silver Rust'], + ['#c9d9d2', 'Conch'], + ['#c9ffa2', 'Reef'], + ['#c9ffe5', 'Aero Blue'], + ['#ca3435', 'Flush Mahogany'], + ['#cabb48', 'Turmeric'], + ['#cadcd4', 'Paris White'], + ['#cae00d', 'Bitter Lemon'], + ['#cae6da', 'Skeptic'], + ['#cb8fa9', 'Viola'], + ['#cbcab6', 'Foggy Gray'], + ['#cbd3b0', 'Green Mist'], + ['#cbdbd6', 'Nebula'], + ['#cc3333', 'Persian Red'], + ['#cc5500', 'Burnt Orange'], + ['#cc7722', 'Ochre'], + ['#cc8899', 'Puce'], + ['#cccaa8', 'Thistle Green'], + ['#ccccff', 'Periwinkle'], + ['#ccff00', 'Electric Lime'], + ['#cd5700', 'Tenn'], + ['#cd5c5c', 'Chestnut Rose'], + ['#cd8429', 'Brandy Punch'], + ['#cdf4ff', 'Onahau'], + ['#ceb98f', 'Sorrell Brown'], + ['#cebaba', 'Cold Turkey'], + ['#cec291', 'Yuma'], + ['#cec7a7', 'Chino'], + ['#cfa39d', 'Eunry'], + ['#cfb53b', 'Old Gold'], + ['#cfdccf', 'Tasman'], + ['#cfe5d2', 'Surf Crest'], + ['#cff9f3', 'Humming Bird'], + ['#cffaf4', 'Scandal'], + ['#d05f04', 'Red Stage'], + ['#d06da1', 'Hopbush'], + ['#d07d12', 'Meteor'], + ['#d0bef8', 'Perfume'], + ['#d0c0e5', 'Prelude'], + ['#d0f0c0', 'Tea Green'], + ['#d18f1b', 'Geebung'], + ['#d1bea8', 'Vanilla'], + ['#d1c6b4', 'Soft Amber'], + ['#d1d2ca', 'Celeste'], + ['#d1d2dd', 'Mischka'], + ['#d1e231', 'Pear'], + ['#d2691e', 'Hot Cinnamon'], + ['#d27d46', 'Raw Sienna'], + ['#d29eaa', 'Careys Pink'], + ['#d2b48c', 'Tan'], + ['#d2da97', 'Deco'], + ['#d2f6de', 'Blue Romance'], + ['#d2f8b0', 'Gossip'], + ['#d3cbba', 'Sisal'], + ['#d3cdc5', 'Swirl'], + ['#d47494', 'Charm'], + ['#d4b6af', 'Clam Shell'], + ['#d4bf8d', 'Straw'], + ['#d4c4a8', 'Akaroa'], + ['#d4cd16', 'Bird Flower'], + ['#d4d7d9', 'Iron'], + ['#d4dfe2', 'Geyser'], + ['#d4e2fc', 'Hawkes Blue'], + ['#d54600', 'Grenadier'], + ['#d591a4', 'Can Can'], + ['#d59a6f', 'Whiskey'], + ['#d5d195', 'Winter Hazel'], + ['#d5f6e3', 'Granny Apple'], + ['#d69188', 'My Pink'], + ['#d6c562', 'Tacha'], + ['#d6cef6', 'Moon Raker'], + ['#d6d6d1', 'Quill Gray'], + ['#d6ffdb', 'Snowy Mint'], + ['#d7837f', 'New York Pink'], + ['#d7c498', 'Pavlova'], + ['#d7d0ff', 'Fog'], + ['#d84437', 'Valencia'], + ['#d87c63', 'Japonica'], + ['#d8bfd8', 'Thistle'], + ['#d8c2d5', 'Maverick'], + ['#d8fcfa', 'Foam'], + ['#d94972', 'Cabaret'], + ['#d99376', 'Burning Sand'], + ['#d9b99b', 'Cameo'], + ['#d9d6cf', 'Timberwolf'], + ['#d9dcc1', 'Tana'], + ['#d9e4f5', 'Link Water'], + ['#d9f7ff', 'Mabel'], + ['#da3287', 'Cerise'], + ['#da5b38', 'Flame Pea'], + ['#da6304', 'Bamboo'], + ['#da6a41', 'Red Damask'], + ['#da70d6', 'Orchid'], + ['#da8a67', 'Copperfield'], + ['#daa520', 'Golden Grass'], + ['#daecd6', 'Zanah'], + ['#daf4f0', 'Iceberg'], + ['#dafaff', 'Oyster Bay'], + ['#db5079', 'Cranberry'], + ['#db9690', 'Petite Orchid'], + ['#db995e', 'Di Serria'], + ['#dbdbdb', 'Alto'], + ['#dbfff8', 'Frosted Mint'], + ['#dc143c', 'Crimson'], + ['#dc4333', 'Punch'], + ['#dcb20c', 'Galliano'], + ['#dcb4bc', 'Blossom'], + ['#dcd747', 'Wattle'], + ['#dcd9d2', 'Westar'], + ['#dcddcc', 'Moon Mist'], + ['#dcedb4', 'Caper'], + ['#dcf0ea', 'Swans Down'], + ['#ddd6d5', 'Swiss Coffee'], + ['#ddf9f1', 'White Ice'], + ['#de3163', 'Cerise Red'], + ['#de6360', 'Roman'], + ['#dea681', 'Tumbleweed'], + ['#deba13', 'Gold Tips'], + ['#dec196', 'Brandy'], + ['#decbc6', 'Wafer'], + ['#ded4a4', 'Sapling'], + ['#ded717', 'Barberry'], + ['#dee5c0', 'Beryl Green'], + ['#def5ff', 'Pattens Blue'], + ['#df73ff', 'Heliotrope'], + ['#dfbe6f', 'Apache'], + ['#dfcd6f', 'Chenin'], + ['#dfcfdb', 'Lola'], + ['#dfecda', 'Willow Brook'], + ['#dfff00', 'Chartreuse Yellow'], + ['#e0b0ff', 'Mauve'], + ['#e0b646', 'Anzac'], + ['#e0b974', 'Harvest Gold'], + ['#e0c095', 'Calico'], + ['#e0ffff', 'Baby Blue'], + ['#e16865', 'Sunglo'], + ['#e1bc64', 'Equator'], + ['#e1c0c8', 'Pink Flare'], + ['#e1e6d6', 'Periglacial Blue'], + ['#e1ead4', 'Kidnapper'], + ['#e1f6e8', 'Tara'], + ['#e25465', 'Mandy'], + ['#e2725b', 'Terracotta'], + ['#e28913', 'Golden Bell'], + ['#e292c0', 'Shocking'], + ['#e29418', 'Dixie'], + ['#e29cd2', 'Light Orchid'], + ['#e2d8ed', 'Snuff'], + ['#e2ebed', 'Mystic'], + ['#e2f3ec', 'Apple Green'], + ['#e30b5c', 'Razzmatazz'], + ['#e32636', 'Alizarin Crimson'], + ['#e34234', 'Cinnabar'], + ['#e3bebe', 'Cavern Pink'], + ['#e3f5e1', 'Peppermint'], + ['#e3f988', 'Mindaro'], + ['#e47698', 'Deep Blush'], + ['#e49b0f', 'Gamboge'], + ['#e4c2d5', 'Melanie'], + ['#e4cfde', 'Twilight'], + ['#e4d1c0', 'Bone'], + ['#e4d422', 'Sunflower'], + ['#e4d5b7', 'Grain Brown'], + ['#e4d69b', 'Zombie'], + ['#e4f6e7', 'Frostee'], + ['#e4ffd1', 'Snow Flurry'], + ['#e52b50', 'Amaranth'], + ['#e5841b', 'Zest'], + ['#e5ccc9', 'Dust Storm'], + ['#e5d7bd', 'Stark White'], + ['#e5d8af', 'Hampton'], + ['#e5e0e1', 'Bon Jour'], + ['#e5e5e5', 'Mercury'], + ['#e5f9f6', 'Polar'], + ['#e64e03', 'Trinidad'], + ['#e6be8a', 'Gold Sand'], + ['#e6bea5', 'Cashmere'], + ['#e6d7b9', 'Double Spanish White'], + ['#e6e4d4', 'Satin Linen'], + ['#e6f2ea', 'Harp'], + ['#e6f8f3', 'Off Green'], + ['#e6ffe9', 'Hint Of Green'], + ['#e6ffff', 'Tranquil'], + ['#e77200', 'Mango Tango'], + ['#e7730a', 'Christine'], + ['#e79f8c', 'Tonys Pink'], + ['#e79fc4', 'Kobi'], + ['#e7bcb4', 'Rose Fog'], + ['#e7bf05', 'Corn'], + ['#e7cd8c', 'Putty'], + ['#e7ece6', 'Gray Nurse'], + ['#e7f8ff', 'Lily White'], + ['#e7feff', 'Bubbles'], + ['#e89928', 'Fire Bush'], + ['#e8b9b3', 'Shilo'], + ['#e8e0d5', 'Pearl Bush'], + ['#e8ebe0', 'Green White'], + ['#e8f1d4', 'Chrome White'], + ['#e8f2eb', 'Gin'], + ['#e8f5f2', 'Aqua Squeeze'], + ['#e96e00', 'Clementine'], + ['#e97451', 'Burnt Sienna'], + ['#e97c07', 'Tahiti Gold'], + ['#e9cecd', 'Oyster Pink'], + ['#e9d75a', 'Confetti'], + ['#e9e3e3', 'Ebb'], + ['#e9f8ed', 'Ottoman'], + ['#e9fffd', 'Clear Day'], + ['#ea88a8', 'Carissma'], + ['#eaae69', 'Porsche'], + ['#eab33b', 'Tulip Tree'], + ['#eac674', 'Rob Roy'], + ['#eadab8', 'Raffia'], + ['#eae8d4', 'White Rock'], + ['#eaf6ee', 'Panache'], + ['#eaf6ff', 'Solitude'], + ['#eaf9f5', 'Aqua Spring'], + ['#eafffe', 'Dew'], + ['#eb9373', 'Apricot'], + ['#ebc2af', 'Zinnwaldite'], + ['#eca927', 'Fuel Yellow'], + ['#ecc54e', 'Ronchi'], + ['#ecc7ee', 'French Lilac'], + ['#eccdb9', 'Just Right'], + ['#ece090', 'Wild Rice'], + ['#ecebbd', 'Fall Green'], + ['#ecebce', 'Aths Special'], + ['#ecf245', 'Starship'], + ['#ed0a3f', 'Red Ribbon'], + ['#ed7a1c', 'Tango'], + ['#ed9121', 'Carrot Orange'], + ['#ed989e', 'Sea Pink'], + ['#edb381', 'Tacao'], + ['#edc9af', 'Desert Sand'], + ['#edcdab', 'Pancho'], + ['#eddcb1', 'Chamois'], + ['#edea99', 'Primrose'], + ['#edf5dd', 'Frost'], + ['#edf5f5', 'Aqua Haze'], + ['#edf6ff', 'Zumthor'], + ['#edf9f1', 'Narvik'], + ['#edfc84', 'Honeysuckle'], + ['#ee82ee', 'Lavender Magenta'], + ['#eec1be', 'Beauty Bush'], + ['#eed794', 'Chalky'], + ['#eed9c4', 'Almond'], + ['#eedc82', 'Flax'], + ['#eededa', 'Bizarre'], + ['#eee3ad', 'Double Colonial White'], + ['#eeeee8', 'Cararra'], + ['#eeef78', 'Manz'], + ['#eef0c8', 'Tahuna Sands'], + ['#eef0f3', 'Athens Gray'], + ['#eef3c3', 'Tusk'], + ['#eef4de', 'Loafer'], + ['#eef6f7', 'Catskill White'], + ['#eefdff', 'Twilight Blue'], + ['#eeff9a', 'Jonquil'], + ['#eeffe2', 'Rice Flower'], + ['#ef863f', 'Jaffa'], + ['#efefef', 'Gallery'], + ['#eff2f3', 'Porcelain'], + ['#f091a9', 'Mauvelous'], + ['#f0d52d', 'Golden Dream'], + ['#f0db7d', 'Golden Sand'], + ['#f0dc82', 'Buff'], + ['#f0e2ec', 'Prim'], + ['#f0e68c', 'Khaki'], + ['#f0eefd', 'Selago'], + ['#f0eeff', 'Titan White'], + ['#f0f8ff', 'Alice Blue'], + ['#f0fcea', 'Feta'], + ['#f18200', 'Gold Drop'], + ['#f19bab', 'Wewak'], + ['#f1e788', 'Sahara Sand'], + ['#f1e9d2', 'Parchment'], + ['#f1e9ff', 'Blue Chalk'], + ['#f1eec1', 'Mint Julep'], + ['#f1f1f1', 'Seashell'], + ['#f1f7f2', 'Saltpan'], + ['#f1ffad', 'Tidal'], + ['#f1ffc8', 'Chiffon'], + ['#f2552a', 'Flamingo'], + ['#f28500', 'Tangerine'], + ['#f2c3b2', 'Mandys Pink'], + ['#f2f2f2', 'Concrete'], + ['#f2fafa', 'Black Squeeze'], + ['#f34723', 'Pomegranate'], + ['#f3ad16', 'Buttercup'], + ['#f3d69d', 'New Orleans'], + ['#f3d9df', 'Vanilla Ice'], + ['#f3e7bb', 'Sidecar'], + ['#f3e9e5', 'Dawn Pink'], + ['#f3edcf', 'Wheatfield'], + ['#f3fb62', 'Canary'], + ['#f3fbd4', 'Orinoco'], + ['#f3ffd8', 'Carla'], + ['#f400a1', 'Hollywood Cerise'], + ['#f4a460', 'Sandy brown'], + ['#f4c430', 'Saffron'], + ['#f4d81c', 'Ripe Lemon'], + ['#f4ebd3', 'Janna'], + ['#f4f2ee', 'Pampas'], + ['#f4f4f4', 'Wild Sand'], + ['#f4f8ff', 'Zircon'], + ['#f57584', 'Froly'], + ['#f5c85c', 'Cream Can'], + ['#f5c999', 'Manhattan'], + ['#f5d5a0', 'Maize'], + ['#f5deb3', 'Wheat'], + ['#f5e7a2', 'Sandwisp'], + ['#f5e7e2', 'Pot Pourri'], + ['#f5e9d3', 'Albescent White'], + ['#f5edef', 'Soft Peach'], + ['#f5f3e5', 'Ecru White'], + ['#f5f5dc', 'Beige'], + ['#f5fb3d', 'Golden Fizz'], + ['#f5ffbe', 'Australian Mint'], + ['#f64a8a', 'French Rose'], + ['#f653a6', 'Brilliant Rose'], + ['#f6a4c9', 'Illusion'], + ['#f6f0e6', 'Merino'], + ['#f6f7f7', 'Black Haze'], + ['#f6ffdc', 'Spring Sun'], + ['#f7468a', 'Violet Red'], + ['#f77703', 'Chilean Fire'], + ['#f77fbe', 'Persian Pink'], + ['#f7b668', 'Rajah'], + ['#f7c8da', 'Azalea'], + ['#f7dbe6', 'We Peep'], + ['#f7f2e1', 'Quarter Spanish White'], + ['#f7f5fa', 'Whisper'], + ['#f7faf7', 'Snow Drift'], + ['#f8b853', 'Casablanca'], + ['#f8c3df', 'Chantilly'], + ['#f8d9e9', 'Cherub'], + ['#f8db9d', 'Marzipan'], + ['#f8dd5c', 'Energy Yellow'], + ['#f8e4bf', 'Givry'], + ['#f8f0e8', 'White Linen'], + ['#f8f4ff', 'Magnolia'], + ['#f8f6f1', 'Spring Wood'], + ['#f8f7dc', 'Coconut Cream'], + ['#f8f7fc', 'White Lilac'], + ['#f8f8f7', 'Desert Storm'], + ['#f8f99c', 'Texas'], + ['#f8facd', 'Corn Field'], + ['#f8fdd3', 'Mimosa'], + ['#f95a61', 'Carnation'], + ['#f9bf58', 'Saffron Mango'], + ['#f9e0ed', 'Carousel Pink'], + ['#f9e4bc', 'Dairy Cream'], + ['#f9e663', 'Portica'], + ['#f9eaf3', 'Amour'], + ['#f9f8e4', 'Rum Swizzle'], + ['#f9ff8b', 'Dolly'], + ['#f9fff6', 'Sugar Cane'], + ['#fa7814', 'Ecstasy'], + ['#fa9d5a', 'Tan Hide'], + ['#fad3a2', 'Corvette'], + ['#fadfad', 'Peach Yellow'], + ['#fae600', 'Turbo'], + ['#faeab9', 'Astra'], + ['#faeccc', 'Champagne'], + ['#faf0e6', 'Linen'], + ['#faf3f0', 'Fantasy'], + ['#faf7d6', 'Citrine White'], + ['#fafafa', 'Alabaster'], + ['#fafde4', 'Hint Of Yellow'], + ['#faffa4', 'Milan'], + ['#fb607f', 'Brink Pink'], + ['#fb8989', 'Geraldine'], + ['#fba0e3', 'Lavender Rose'], + ['#fba129', 'Sea Buckthorn'], + ['#fbac13', 'Sun'], + ['#fbaed2', 'Lavender Pink'], + ['#fbb2a3', 'Rose Bud'], + ['#fbbeda', 'Cupid'], + ['#fbcce7', 'Classic Rose'], + ['#fbceb1', 'Apricot Peach'], + ['#fbe7b2', 'Banana Mania'], + ['#fbe870', 'Marigold Yellow'], + ['#fbe96c', 'Festival'], + ['#fbea8c', 'Sweet Corn'], + ['#fbec5d', 'Candy Corn'], + ['#fbf9f9', 'Hint Of Red'], + ['#fbffba', 'Shalimar'], + ['#fc0fc0', 'Shocking Pink'], + ['#fc80a5', 'Tickle Me Pink'], + ['#fc9c1d', 'Tree Poppy'], + ['#fcc01e', 'Lightning Yellow'], + ['#fcd667', 'Goldenrod'], + ['#fcd917', 'Candlelight'], + ['#fcda98', 'Cherokee'], + ['#fcf4d0', 'Double Pearl Lusta'], + ['#fcf4dc', 'Pearl Lusta'], + ['#fcf8f7', 'Vista White'], + ['#fcfbf3', 'Bianca'], + ['#fcfeda', 'Moon Glow'], + ['#fcffe7', 'China Ivory'], + ['#fcfff9', 'Ceramic'], + ['#fd0e35', 'Torch Red'], + ['#fd5b78', 'Wild Watermelon'], + ['#fd7b33', 'Crusta'], + ['#fd7c07', 'Sorbus'], + ['#fd9fa2', 'Sweet Pink'], + ['#fdd5b1', 'Light Apricot'], + ['#fdd7e4', 'Pig Pink'], + ['#fde1dc', 'Cinderella'], + ['#fde295', 'Golden Glow'], + ['#fde910', 'Lemon'], + ['#fdf5e6', 'Old Lace'], + ['#fdf6d3', 'Half Colonial White'], + ['#fdf7ad', 'Drover'], + ['#fdfeb8', 'Pale Prim'], + ['#fdffd5', 'Cumulus'], + ['#fe28a2', 'Persian Rose'], + ['#fe4c40', 'Sunset Orange'], + ['#fe6f5e', 'Bittersweet'], + ['#fe9d04', 'California'], + ['#fea904', 'Yellow Sea'], + ['#febaad', 'Melon'], + ['#fed33c', 'Bright Sun'], + ['#fed85d', 'Dandelion'], + ['#fedb8d', 'Salomie'], + ['#fee5ac', 'Cape Honey'], + ['#feebf3', 'Remy'], + ['#feefce', 'Oasis'], + ['#fef0ec', 'Bridesmaid'], + ['#fef2c7', 'Beeswax'], + ['#fef3d8', 'Bleach White'], + ['#fef4cc', 'Pipi'], + ['#fef4db', 'Half Spanish White'], + ['#fef4f8', 'Wisp Pink'], + ['#fef5f1', 'Provincial Pink'], + ['#fef7de', 'Half Dutch White'], + ['#fef8e2', 'Solitaire'], + ['#fef8ff', 'White Pointer'], + ['#fef9e3', 'Off Yellow'], + ['#fefced', 'Orange White'], + ['#ff0000', 'Red'], + ['#ff007f', 'Rose'], + ['#ff00cc', 'Purple Pizzazz'], + ['#ff00ff', 'Magenta Fuchsia'], + ['#ff2400', 'Scarlet'], + ['#ff3399', 'Wild Strawberry'], + ['#ff33cc', 'Razzle Dazzle Rose'], + ['#ff355e', 'Radical Red'], + ['#ff3f34', 'Red Orange'], + ['#ff4040', 'Coral Red'], + ['#ff4d00', 'Vermilion'], + ['#ff4f00', 'International Orange'], + ['#ff6037', 'Outrageous Orange'], + ['#ff6600', 'Blaze Orange'], + ['#ff66ff', 'Pink Flamingo'], + ['#ff681f', 'Orange'], + ['#ff69b4', 'Hot Pink'], + ['#ff6b53', 'Persimmon'], + ['#ff6fff', 'Blush Pink'], + ['#ff7034', 'Burning Orange'], + ['#ff7518', 'Pumpkin'], + ['#ff7d07', 'Flamenco'], + ['#ff7f00', 'Flush Orange'], + ['#ff7f50', 'Coral'], + ['#ff8c69', 'Salmon'], + ['#ff9000', 'Pizazz'], + ['#ff910f', 'West Side'], + ['#ff91a4', 'Pink Salmon'], + ['#ff9933', 'Neon Carrot'], + ['#ff9966', 'Atomic Tangerine'], + ['#ff9980', 'Vivid Tangerine'], + ['#ff9e2c', 'Sunshade'], + ['#ffa000', 'Orange Peel'], + ['#ffa194', 'Mona Lisa'], + ['#ffa500', 'Web Orange'], + ['#ffa6c9', 'Carnation Pink'], + ['#ffab81', 'Hit Pink'], + ['#ffae42', 'Yellow Orange'], + ['#ffb0ac', 'Cornflower Lilac'], + ['#ffb1b3', 'Sundown'], + ['#ffb31f', 'My Sin'], + ['#ffb555', 'Texas Rose'], + ['#ffb7d5', 'Cotton Candy'], + ['#ffb97b', 'Macaroni And Cheese'], + ['#ffba00', 'Selective Yellow'], + ['#ffbd5f', 'Koromiko'], + ['#ffbf00', 'Amber'], + ['#ffc0a8', 'Wax Flower'], + ['#ffc0cb', 'Pink'], + ['#ffc3c0', 'Your Pink'], + ['#ffc901', 'Supernova'], + ['#ffcba4', 'Flesh'], + ['#ffcc33', 'Sunglow'], + ['#ffcc5c', 'Golden Tainoi'], + ['#ffcc99', 'Peach Orange'], + ['#ffcd8c', 'Chardonnay'], + ['#ffd1dc', 'Pastel Pink'], + ['#ffd2b7', 'Romantic'], + ['#ffd38c', 'Grandis'], + ['#ffd700', 'Gold'], + ['#ffd800', 'School Bus Yellow'], + ['#ffd8d9', 'Cosmos'], + ['#ffdb58', 'Mustard'], + ['#ffdcd6', 'Peach Schnapps'], + ['#ffddaf', 'Caramel'], + ['#ffddcd', 'Tuft Bush'], + ['#ffddcf', 'Watusi'], + ['#ffddf4', 'Pink Lace'], + ['#ffdead', 'Navajo White'], + ['#ffdeb3', 'Frangipani'], + ['#ffe1df', 'Pippin'], + ['#ffe1f2', 'Pale Rose'], + ['#ffe2c5', 'Negroni'], + ['#ffe5a0', 'Cream Brulee'], + ['#ffe5b4', 'Peach'], + ['#ffe6c7', 'Tequila'], + ['#ffe772', 'Kournikova'], + ['#ffeac8', 'Sandy Beach'], + ['#ffead4', 'Karry'], + ['#ffec13', 'Broom'], + ['#ffedbc', 'Colonial White'], + ['#ffeed8', 'Derby'], + ['#ffefa1', 'Vis Vis'], + ['#ffefc1', 'Egg White'], + ['#ffefd5', 'Papaya Whip'], + ['#ffefec', 'Fair Pink'], + ['#fff0db', 'Peach Cream'], + ['#fff0f5', 'Lavender Blush'], + ['#fff14f', 'Gorse'], + ['#fff1b5', 'Buttermilk'], + ['#fff1d8', 'Pink Lady'], + ['#fff1ee', 'Forget Me Not'], + ['#fff1f9', 'Tutu'], + ['#fff39d', 'Picasso'], + ['#fff3f1', 'Chardon'], + ['#fff46e', 'Paris Daisy'], + ['#fff4ce', 'Barley White'], + ['#fff4dd', 'Egg Sour'], + ['#fff4e0', 'Sazerac'], + ['#fff4e8', 'Serenade'], + ['#fff4f3', 'Chablis'], + ['#fff5ee', 'Seashell Peach'], + ['#fff5f3', 'Sauvignon'], + ['#fff6d4', 'Milk Punch'], + ['#fff6df', 'Varden'], + ['#fff6f5', 'Rose White'], + ['#fff8d1', 'Baja White'], + ['#fff9e2', 'Gin Fizz'], + ['#fff9e6', 'Early Dawn'], + ['#fffacd', 'Lemon Chiffon'], + ['#fffaf4', 'Bridal Heath'], + ['#fffbdc', 'Scotch Mist'], + ['#fffbf9', 'Soapstone'], + ['#fffc99', 'Witch Haze'], + ['#fffcea', 'Buttery White'], + ['#fffcee', 'Island Spice'], + ['#fffdd0', 'Cream'], + ['#fffde6', 'Chilean Heath'], + ['#fffde8', 'Travertine'], + ['#fffdf3', 'Orchid White'], + ['#fffdf4', 'Quarter Pearl Lusta'], + ['#fffee1', 'Half And Half'], + ['#fffeec', 'Apricot White'], + ['#fffef0', 'Rice Cake'], + ['#fffef6', 'Black White'], + ['#fffefd', 'Romance'], + ['#ffff00', 'Yellow'], + ['#ffff66', 'Laser Lemon'], + ['#ffff99', 'Pale Canary'], + ['#ffffb4', 'Portafino'], + ['#fffff0', 'Ivory'], + ['#ffffff', 'White'] +]; + +/** + * Map Of hex color values to color names + * + * - key: hex value + * - value: color name + */ +export const colorNameMap = colorNames.reduce>((acc, [hex, name]) => { + acc[hex] = name; + return acc; +}, {}); diff --git a/packages/color/src/constant/palette.ts b/packages/color/src/constant/palette.ts new file mode 100644 index 0000000..9f62664 --- /dev/null +++ b/packages/color/src/constant/palette.ts @@ -0,0 +1,356 @@ +import type { ColorPaletteFamily } from '../types'; + +export const colorPalettes: ColorPaletteFamily[] = [ + { + name: 'Slate', + palettes: [ + { hex: '#f8fafc', number: 50 }, + { hex: '#f1f5f9', number: 100 }, + { hex: '#e2e8f0', number: 200 }, + { hex: '#cbd5e1', number: 300 }, + { hex: '#94a3b8', number: 400 }, + { hex: '#64748b', number: 500 }, + { hex: '#475569', number: 600 }, + { hex: '#334155', number: 700 }, + { hex: '#1e293b', number: 800 }, + { hex: '#0f172a', number: 900 }, + { hex: '#020617', number: 950 } + ] + }, + { + name: 'Gray', + palettes: [ + { hex: '#f9fafb', number: 50 }, + { hex: '#f3f4f6', number: 100 }, + { hex: '#e5e7eb', number: 200 }, + { hex: '#d1d5db', number: 300 }, + { hex: '#9ca3af', number: 400 }, + { hex: '#6b7280', number: 500 }, + { hex: '#4b5563', number: 600 }, + { hex: '#374151', number: 700 }, + { hex: '#1f2937', number: 800 }, + { hex: '#111827', number: 900 }, + { hex: '#030712', number: 950 } + ] + }, + { + name: 'Zinc', + palettes: [ + { hex: '#fafafa', number: 50 }, + { hex: '#f4f4f5', number: 100 }, + { hex: '#e4e4e7', number: 200 }, + { hex: '#d4d4d8', number: 300 }, + { hex: '#a1a1aa', number: 400 }, + { hex: '#71717a', number: 500 }, + { hex: '#52525b', number: 600 }, + { hex: '#3f3f46', number: 700 }, + { hex: '#27272a', number: 800 }, + { hex: '#18181b', number: 900 }, + { hex: '#09090b', number: 950 } + ] + }, + { + name: 'Neutral', + palettes: [ + { hex: '#fafafa', number: 50 }, + { hex: '#f5f5f5', number: 100 }, + { hex: '#e5e5e5', number: 200 }, + { hex: '#d4d4d4', number: 300 }, + { hex: '#a3a3a3', number: 400 }, + { hex: '#737373', number: 500 }, + { hex: '#525252', number: 600 }, + { hex: '#404040', number: 700 }, + { hex: '#262626', number: 800 }, + { hex: '#171717', number: 900 }, + { hex: '#0a0a0a', number: 950 } + ] + }, + { + name: 'Stone', + palettes: [ + { hex: '#fafaf9', number: 50 }, + { hex: '#f5f5f4', number: 100 }, + { hex: '#e7e5e4', number: 200 }, + { hex: '#d6d3d1', number: 300 }, + { hex: '#a8a29e', number: 400 }, + { hex: '#78716c', number: 500 }, + { hex: '#57534e', number: 600 }, + { hex: '#44403c', number: 700 }, + { hex: '#292524', number: 800 }, + { hex: '#1c1917', number: 900 }, + { hex: '#0c0a09', number: 950 } + ] + }, + { + name: 'Red', + palettes: [ + { hex: '#fef2f2', number: 50 }, + { hex: '#fee2e2', number: 100 }, + { hex: '#fecaca', number: 200 }, + { hex: '#fca5a5', number: 300 }, + { hex: '#f87171', number: 400 }, + { hex: '#ef4444', number: 500 }, + { hex: '#dc2626', number: 600 }, + { hex: '#b91c1c', number: 700 }, + { hex: '#991b1b', number: 800 }, + { hex: '#7f1d1d', number: 900 }, + { hex: '#450a0a', number: 950 } + ] + }, + { + name: 'Orange', + palettes: [ + { hex: '#fff7ed', number: 50 }, + { hex: '#ffedd5', number: 100 }, + { hex: '#fed7aa', number: 200 }, + { hex: '#fdba74', number: 300 }, + { hex: '#fb923c', number: 400 }, + { hex: '#f97316', number: 500 }, + { hex: '#ea580c', number: 600 }, + { hex: '#c2410c', number: 700 }, + { hex: '#9a3412', number: 800 }, + { hex: '#7c2d12', number: 900 }, + { hex: '#431407', number: 950 } + ] + }, + { + name: 'Amber', + palettes: [ + { hex: '#fffbeb', number: 50 }, + { hex: '#fef3c7', number: 100 }, + { hex: '#fde68a', number: 200 }, + { hex: '#fcd34d', number: 300 }, + { hex: '#fbbf24', number: 400 }, + { hex: '#f59e0b', number: 500 }, + { hex: '#d97706', number: 600 }, + { hex: '#b45309', number: 700 }, + { hex: '#92400e', number: 800 }, + { hex: '#78350f', number: 900 }, + { hex: '#451a03', number: 950 } + ] + }, + { + name: 'Yellow', + palettes: [ + { hex: '#fefce8', number: 50 }, + { hex: '#fef9c3', number: 100 }, + { hex: '#fef08a', number: 200 }, + { hex: '#fde047', number: 300 }, + { hex: '#facc15', number: 400 }, + { hex: '#eab308', number: 500 }, + { hex: '#ca8a04', number: 600 }, + { hex: '#a16207', number: 700 }, + { hex: '#854d0e', number: 800 }, + { hex: '#713f12', number: 900 }, + { hex: '#422006', number: 950 } + ] + }, + { + name: 'Lime', + palettes: [ + { hex: '#f7fee7', number: 50 }, + { hex: '#ecfccb', number: 100 }, + { hex: '#d9f99d', number: 200 }, + { hex: '#bef264', number: 300 }, + { hex: '#a3e635', number: 400 }, + { hex: '#84cc16', number: 500 }, + { hex: '#65a30d', number: 600 }, + { hex: '#4d7c0f', number: 700 }, + { hex: '#3f6212', number: 800 }, + { hex: '#365314', number: 900 }, + { hex: '#1a2e05', number: 950 } + ] + }, + { + name: 'Green', + palettes: [ + { hex: '#f0fdf4', number: 50 }, + { hex: '#dcfce7', number: 100 }, + { hex: '#bbf7d0', number: 200 }, + { hex: '#86efac', number: 300 }, + { hex: '#4ade80', number: 400 }, + { hex: '#22c55e', number: 500 }, + { hex: '#16a34a', number: 600 }, + { hex: '#15803d', number: 700 }, + { hex: '#166534', number: 800 }, + { hex: '#14532d', number: 900 }, + { hex: '#052e16', number: 950 } + ] + }, + { + name: 'Emerald', + palettes: [ + { hex: '#ecfdf5', number: 50 }, + { hex: '#d1fae5', number: 100 }, + { hex: '#a7f3d0', number: 200 }, + { hex: '#6ee7b7', number: 300 }, + { hex: '#34d399', number: 400 }, + { hex: '#10b981', number: 500 }, + { hex: '#059669', number: 600 }, + { hex: '#047857', number: 700 }, + { hex: '#065f46', number: 800 }, + { hex: '#064e3b', number: 900 }, + { hex: '#022c22', number: 950 } + ] + }, + { + name: 'Teal', + palettes: [ + { hex: '#f0fdfa', number: 50 }, + { hex: '#ccfbf1', number: 100 }, + { hex: '#99f6e4', number: 200 }, + { hex: '#5eead4', number: 300 }, + { hex: '#2dd4bf', number: 400 }, + { hex: '#14b8a6', number: 500 }, + { hex: '#0d9488', number: 600 }, + { hex: '#0f766e', number: 700 }, + { hex: '#115e59', number: 800 }, + { hex: '#134e4a', number: 900 }, + { hex: '#042f2e', number: 950 } + ] + }, + { + name: 'Cyan', + palettes: [ + { hex: '#ecfeff', number: 50 }, + { hex: '#cffafe', number: 100 }, + { hex: '#a5f3fc', number: 200 }, + { hex: '#67e8f9', number: 300 }, + { hex: '#22d3ee', number: 400 }, + { hex: '#06b6d4', number: 500 }, + { hex: '#0891b2', number: 600 }, + { hex: '#0e7490', number: 700 }, + { hex: '#155e75', number: 800 }, + { hex: '#164e63', number: 900 }, + { hex: '#083344', number: 950 } + ] + }, + { + name: 'Sky', + palettes: [ + { hex: '#f0f9ff', number: 50 }, + { hex: '#e0f2fe', number: 100 }, + { hex: '#bae6fd', number: 200 }, + { hex: '#7dd3fc', number: 300 }, + { hex: '#38bdf8', number: 400 }, + { hex: '#0ea5e9', number: 500 }, + { hex: '#0284c7', number: 600 }, + { hex: '#0369a1', number: 700 }, + { hex: '#075985', number: 800 }, + { hex: '#0c4a6e', number: 900 }, + { hex: '#082f49', number: 950 } + ] + }, + { + name: 'Blue', + palettes: [ + { hex: '#eff6ff', number: 50 }, + { hex: '#dbeafe', number: 100 }, + { hex: '#bfdbfe', number: 200 }, + { hex: '#93c5fd', number: 300 }, + { hex: '#60a5fa', number: 400 }, + { hex: '#3b82f6', number: 500 }, + { hex: '#2563eb', number: 600 }, + { hex: '#1d4ed8', number: 700 }, + { hex: '#1e40af', number: 800 }, + { hex: '#1e3a8a', number: 900 }, + { hex: '#172554', number: 950 } + ] + }, + { + name: 'Indigo', + palettes: [ + { hex: '#eef2ff', number: 50 }, + { hex: '#e0e7ff', number: 100 }, + { hex: '#c7d2fe', number: 200 }, + { hex: '#a5b4fc', number: 300 }, + { hex: '#818cf8', number: 400 }, + { hex: '#6366f1', number: 500 }, + { hex: '#4f46e5', number: 600 }, + { hex: '#4338ca', number: 700 }, + { hex: '#3730a3', number: 800 }, + { hex: '#312e81', number: 900 }, + { hex: '#1e1b4b', number: 950 } + ] + }, + { + name: 'Violet', + palettes: [ + { hex: '#f5f3ff', number: 50 }, + { hex: '#ede9fe', number: 100 }, + { hex: '#ddd6fe', number: 200 }, + { hex: '#c4b5fd', number: 300 }, + { hex: '#a78bfa', number: 400 }, + { hex: '#8b5cf6', number: 500 }, + { hex: '#7c3aed', number: 600 }, + { hex: '#6d28d9', number: 700 }, + { hex: '#5b21b6', number: 800 }, + { hex: '#4c1d95', number: 900 }, + { hex: '#2e1065', number: 950 } + ] + }, + { + name: 'Purple', + palettes: [ + { hex: '#faf5ff', number: 50 }, + { hex: '#f3e8ff', number: 100 }, + { hex: '#e9d5ff', number: 200 }, + { hex: '#d8b4fe', number: 300 }, + { hex: '#c084fc', number: 400 }, + { hex: '#a855f7', number: 500 }, + { hex: '#9333ea', number: 600 }, + { hex: '#7e22ce', number: 700 }, + { hex: '#6b21a8', number: 800 }, + { hex: '#581c87', number: 900 }, + { hex: '#3b0764', number: 950 } + ] + }, + { + name: 'Fuchsia', + palettes: [ + { hex: '#fdf4ff', number: 50 }, + { hex: '#fae8ff', number: 100 }, + { hex: '#f5d0fe', number: 200 }, + { hex: '#f0abfc', number: 300 }, + { hex: '#e879f9', number: 400 }, + { hex: '#d946ef', number: 500 }, + { hex: '#c026d3', number: 600 }, + { hex: '#a21caf', number: 700 }, + { hex: '#86198f', number: 800 }, + { hex: '#701a75', number: 900 }, + { hex: '#4a044e', number: 950 } + ] + }, + { + name: 'Pink', + palettes: [ + { hex: '#fdf2f8', number: 50 }, + { hex: '#fce7f3', number: 100 }, + { hex: '#fbcfe8', number: 200 }, + { hex: '#f9a8d4', number: 300 }, + { hex: '#f472b6', number: 400 }, + { hex: '#ec4899', number: 500 }, + { hex: '#db2777', number: 600 }, + { hex: '#be185d', number: 700 }, + { hex: '#9d174d', number: 800 }, + { hex: '#831843', number: 900 }, + { hex: '#500724', number: 950 } + ] + }, + { + name: 'Rose', + palettes: [ + { hex: '#fff1f2', number: 50 }, + { hex: '#ffe4e6', number: 100 }, + { hex: '#fecdd3', number: 200 }, + { hex: '#fda4af', number: 300 }, + { hex: '#fb7185', number: 400 }, + { hex: '#f43f5e', number: 500 }, + { hex: '#e11d48', number: 600 }, + { hex: '#be123c', number: 700 }, + { hex: '#9f1239', number: 800 }, + { hex: '#881337', number: 900 }, + { hex: '#4c0519', number: 950 } + ] + } +]; diff --git a/packages/color/src/index.ts b/packages/color/src/index.ts new file mode 100644 index 0000000..42a4d54 --- /dev/null +++ b/packages/color/src/index.ts @@ -0,0 +1,7 @@ +import { colorPalettes } from './constant'; + +export * from './palette'; +export * from './shared'; +export { colorPalettes }; + +export * from './types'; diff --git a/packages/color/src/palette/antd.ts b/packages/color/src/palette/antd.ts new file mode 100644 index 0000000..c3d5b48 --- /dev/null +++ b/packages/color/src/palette/antd.ts @@ -0,0 +1,176 @@ +import type { AnyColor, HsvColor } from 'colord'; +import { getHex, getHsv, isValidColor, mixColor } from '../shared'; +import type { ColorIndex } from '../types'; + +/** Hue step */ +const hueStep = 2; +/** Saturation step, light color part */ +const saturationStep = 16; +/** Saturation step, dark color part */ +const saturationStep2 = 5; +/** Brightness step, light color part */ +const brightnessStep1 = 5; +/** Brightness step, dark color part */ +const brightnessStep2 = 15; +/** Light color count, main color up */ +const lightColorCount = 5; +/** Dark color count, main color down */ +const darkColorCount = 4; + +/** + * Get AntD palette color by index + * + * @param color - Color + * @param index - The color index of color palette (the main color index is 6) + * @returns Hex color + */ +export function getAntDPaletteColorByIndex(color: AnyColor, index: ColorIndex): string { + if (!isValidColor(color)) { + throw new Error('invalid input color value'); + } + + if (index === 6) { + return getHex(color); + } + + const isLight = index < 6; + const hsv = getHsv(color); + const i = isLight ? lightColorCount + 1 - index : index - lightColorCount - 1; + + const newHsv: HsvColor = { + h: getHue(hsv, i, isLight), + s: getSaturation(hsv, i, isLight), + v: getValue(hsv, i, isLight) + }; + + return getHex(newHsv); +} + +/** Map of dark color index and opacity */ +const darkColorMap = [ + { index: 7, opacity: 0.15 }, + { index: 6, opacity: 0.25 }, + { index: 5, opacity: 0.3 }, + { index: 5, opacity: 0.45 }, + { index: 5, opacity: 0.65 }, + { index: 5, opacity: 0.85 }, + { index: 5, opacity: 0.9 }, + { index: 4, opacity: 0.93 }, + { index: 3, opacity: 0.95 }, + { index: 2, opacity: 0.97 }, + { index: 1, opacity: 0.98 } +]; + +/** + * Get AntD color palette + * + * @param color - Color + * @param darkTheme - Dark theme + * @param darkThemeMixColor - Dark theme mix color (default: #141414) + */ +export function getAntDColorPalette(color: AnyColor, darkTheme = false, darkThemeMixColor = '#141414'): string[] { + const indexes: ColorIndex[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; + + const patterns = indexes.map(index => getAntDPaletteColorByIndex(color, index)); + + if (darkTheme) { + const darkPatterns = darkColorMap.map(({ index, opacity }) => { + const darkColor = mixColor(darkThemeMixColor, patterns[index], opacity); + + return darkColor; + }); + + return darkPatterns.map(item => getHex(item)); + } + + return patterns; +} + +/** + * Get hue + * + * @param hsv - Hsv format color + * @param i - The relative distance from 6 + * @param isLight - Is light color + */ +function getHue(hsv: HsvColor, i: number, isLight: boolean) { + let hue: number; + + const hsvH = Math.round(hsv.h); + + if (hsvH >= 60 && hsvH <= 240) { + hue = isLight ? hsvH - hueStep * i : hsvH + hueStep * i; + } else { + hue = isLight ? hsvH + hueStep * i : hsvH - hueStep * i; + } + + if (hue < 0) { + hue += 360; + } + + if (hue >= 360) { + hue -= 360; + } + + return hue; +} + +/** + * Get saturation + * + * @param hsv - Hsv format color + * @param i - The relative distance from 6 + * @param isLight - Is light color + */ +function getSaturation(hsv: HsvColor, i: number, isLight: boolean) { + if (hsv.h === 0 && hsv.s === 0) { + return hsv.s; + } + + let saturation: number; + + if (isLight) { + saturation = hsv.s - saturationStep * i; + } else if (i === darkColorCount) { + saturation = hsv.s + saturationStep; + } else { + saturation = hsv.s + saturationStep2 * i; + } + + if (saturation > 100) { + saturation = 100; + } + + if (isLight && i === lightColorCount && saturation > 10) { + saturation = 10; + } + + if (saturation < 6) { + saturation = 6; + } + + return saturation; +} + +/** + * Get value of hsv + * + * @param hsv - Hsv format color + * @param i - The relative distance from 6 + * @param isLight - Is light color + */ +function getValue(hsv: HsvColor, i: number, isLight: boolean) { + let value: number; + + if (isLight) { + value = hsv.v + brightnessStep1 * i; + } else { + value = hsv.v - brightnessStep2 * i; + } + + if (value > 100) { + value = 100; + } + + return value; +} diff --git a/packages/color/src/palette/index.ts b/packages/color/src/palette/index.ts new file mode 100644 index 0000000..ba3b853 --- /dev/null +++ b/packages/color/src/palette/index.ts @@ -0,0 +1,45 @@ +import type { AnyColor } from 'colord'; +import { getHex } from '../shared'; +import type { ColorPaletteNumber } from '../types'; +import { getRecommendedColorPalette } from './recommend'; +import { getAntDColorPalette } from './antd'; + +/** + * get color palette by provided color + * + * @param color + * @param recommended whether to get recommended color palette (the provided color may not be the main color) + */ +export function getColorPalette(color: AnyColor, recommended = false) { + const colorMap = new Map(); + + if (recommended) { + const colorPalette = getRecommendedColorPalette(getHex(color)); + colorPalette.palettes.forEach(palette => { + colorMap.set(palette.number, palette.hex); + }); + } else { + const colors = getAntDColorPalette(color); + + const colorNumbers: ColorPaletteNumber[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]; + + colorNumbers.forEach((number, index) => { + colorMap.set(number, colors[index]); + }); + } + + return colorMap; +} + +/** + * get color palette color by number + * + * @param color the provided color + * @param number the color palette number + * @param recommended whether to get recommended color palette (the provided color may not be the main color) + */ +export function getPaletteColorByNumber(color: AnyColor, number: ColorPaletteNumber, recommended = false) { + const colorMap = getColorPalette(color, recommended); + + return colorMap.get(number as ColorPaletteNumber)!; +} diff --git a/packages/color/src/palette/recommend.ts b/packages/color/src/palette/recommend.ts new file mode 100644 index 0000000..904cae2 --- /dev/null +++ b/packages/color/src/palette/recommend.ts @@ -0,0 +1,152 @@ +import { getColorName, getDeltaE, getHsl, isValidColor, transformHslToHex } from '../shared'; +import { colorPalettes } from '../constant'; +import type { + ColorPalette, + ColorPaletteFamily, + ColorPaletteFamilyWithNearestPalette, + ColorPaletteMatch, + ColorPaletteNumber +} from '../types'; + +/** + * get recommended color palette by provided color + * + * @param color the provided color + */ +export function getRecommendedColorPalette(color: string) { + const colorPaletteFamily = getRecommendedColorPaletteFamily(color); + + const colorMap = new Map(); + + colorPaletteFamily.palettes.forEach(palette => { + colorMap.set(palette.number, palette); + }); + + const mainColor = colorMap.get(500)!; + const matchColor = colorPaletteFamily.palettes.find(palette => palette.hex === color)!; + + const colorPalette: ColorPaletteMatch = { + ...colorPaletteFamily, + colorMap, + main: mainColor, + match: matchColor + }; + + return colorPalette; +} + +/** + * get recommended palette color by provided color + * + * @param color the provided color + * @param number the color palette number + */ +export function getRecommendedPaletteColorByNumber(color: string, number: ColorPaletteNumber) { + const colorPalette = getRecommendedColorPalette(color); + + const { hex } = colorPalette.colorMap.get(number)!; + + return hex; +} + +/** + * get color palette family by provided color and color name + * + * @param color the provided color + */ +export function getRecommendedColorPaletteFamily(color: string) { + if (!isValidColor(color)) { + throw new Error('Invalid color, please check color value!'); + } + + let colorName = getColorName(color); + + colorName = colorName.toLowerCase().replace(/\s/g, '-'); + + const { h: h1, s: s1 } = getHsl(color); + + const { nearestLightnessPalette, palettes } = getNearestColorPaletteFamily(color, colorPalettes); + + const { number, hex } = nearestLightnessPalette; + + const { h: h2, s: s2 } = getHsl(hex); + + const deltaH = h1 - h2; + + const sRatio = s1 / s2; + + const colorPaletteFamily: ColorPaletteFamily = { + name: colorName, + palettes: palettes.map(palette => { + let hexValue = color; + + const isSame = number === palette.number; + + if (!isSame) { + const { h: h3, s: s3, l } = getHsl(palette.hex); + + const newH = deltaH < 0 ? h3 + deltaH : h3 - deltaH; + const newS = s3 * sRatio; + + hexValue = transformHslToHex({ + h: newH, + s: newS, + l + }); + } + + return { + hex: hexValue, + number: palette.number + }; + }) + }; + + return colorPaletteFamily; +} + +/** + * get nearest color palette family + * + * @param color color + * @param families color palette families + */ +function getNearestColorPaletteFamily(color: string, families: ColorPaletteFamily[]) { + const familyWithConfig = families.map(family => { + const palettes = family.palettes.map(palette => { + return { + ...palette, + delta: getDeltaE(color, palette.hex) + }; + }); + + const nearestPalette = palettes.reduce((prev, curr) => (prev.delta < curr.delta ? prev : curr)); + + return { + ...family, + palettes, + nearestPalette + }; + }); + + const nearestPaletteFamily = familyWithConfig.reduce((prev, curr) => + prev.nearestPalette.delta < curr.nearestPalette.delta ? prev : curr + ); + + const { l } = getHsl(color); + + const paletteFamily: ColorPaletteFamilyWithNearestPalette = { + ...nearestPaletteFamily, + nearestLightnessPalette: nearestPaletteFamily.palettes.reduce((prev, curr) => { + const { l: prevLightness } = getHsl(prev.hex); + const { l: currLightness } = getHsl(curr.hex); + + const deltaPrev = Math.abs(prevLightness - l); + const deltaCurr = Math.abs(currLightness - l); + + return deltaPrev < deltaCurr ? prev : curr; + }) + }; + + return paletteFamily; +} diff --git a/packages/color/src/shared/colord.ts b/packages/color/src/shared/colord.ts new file mode 100644 index 0000000..4c52330 --- /dev/null +++ b/packages/color/src/shared/colord.ts @@ -0,0 +1,93 @@ +import { colord, extend } from 'colord'; +import namesPlugin from 'colord/plugins/names'; +import mixPlugin from 'colord/plugins/mix'; +import labPlugin from 'colord/plugins/lab'; +import type { AnyColor, HslColor, RgbColor } from 'colord'; + +extend([namesPlugin, mixPlugin, labPlugin]); + +export function isValidColor(color: AnyColor) { + return colord(color).isValid(); +} + +export function getHex(color: AnyColor) { + return colord(color).toHex(); +} + +export function getRgb(color: AnyColor) { + return colord(color).toRgb(); +} + +export function getHsl(color: AnyColor) { + return colord(color).toHsl(); +} + +export function getHsv(color: AnyColor) { + return colord(color).toHsv(); +} + +export function getDeltaE(color1: AnyColor, color2: AnyColor) { + return colord(color1).delta(color2); +} + +export function transformHslToHex(color: HslColor) { + return colord(color).toHex(); +} + +/** + * Add color alpha + * + * @param color - Color + * @param alpha - Alpha (0 - 1) + */ +export function addColorAlpha(color: AnyColor, alpha: number) { + return colord(color).alpha(alpha).toHex(); +} + +/** + * Mix color + * + * @param firstColor - First color + * @param secondColor - Second color + * @param ratio - The ratio of the second color (0 - 1) + */ +export function mixColor(firstColor: AnyColor, secondColor: AnyColor, ratio: number) { + return colord(firstColor).mix(secondColor, ratio).toHex(); +} + +/** + * Transform color with opacity to similar color without opacity + * + * @param color - Color + * @param alpha - Alpha (0 - 1) + * @param bgColor Background color (usually white or black) + */ +export function transformColorWithOpacity(color: AnyColor, alpha: number, bgColor = '#ffffff') { + const originColor = addColorAlpha(color, alpha); + const { r: oR, g: oG, b: oB } = colord(originColor).toRgb(); + + const { r: bgR, g: bgG, b: bgB } = colord(bgColor).toRgb(); + + function calRgb(or: number, bg: number, al: number) { + return bg + (or - bg) * al; + } + + const resultRgb: RgbColor = { + r: calRgb(oR, bgR, alpha), + g: calRgb(oG, bgG, alpha), + b: calRgb(oB, bgB, alpha) + }; + + return colord(resultRgb).toHex(); +} + +/** + * Is white color + * + * @param color - Color + */ +export function isWhiteColor(color: AnyColor) { + return colord(color).isEqual('#ffffff'); +} + +export { colord }; diff --git a/packages/color/src/shared/index.ts b/packages/color/src/shared/index.ts new file mode 100644 index 0000000..ae293ce --- /dev/null +++ b/packages/color/src/shared/index.ts @@ -0,0 +1,2 @@ +export * from './colord'; +export * from './name'; diff --git a/packages/color/src/shared/name.ts b/packages/color/src/shared/name.ts new file mode 100644 index 0000000..866bd33 --- /dev/null +++ b/packages/color/src/shared/name.ts @@ -0,0 +1,49 @@ +import { colorNames } from '../constant'; +import { getHex, getHsl, getRgb } from './colord'; + +/** + * Get color name + * + * @param color + */ +export function getColorName(color: string) { + const hex = getHex(color); + const rgb = getRgb(color); + const hsl = getHsl(color); + + let ndf = 0; + let ndf1 = 0; + let ndf2 = 0; + let cl = -1; + let df = -1; + + let name = ''; + + colorNames.some((item, index) => { + const [hexValue, colorName] = item; + + const match = hex === hexValue; + + if (match) { + name = colorName; + } else { + const { r, g, b } = getRgb(hexValue); + const { h, s, l } = getHsl(hexValue); + + ndf1 = (rgb.r - r) ** 2 + (rgb.g - g) ** 2 + (rgb.b - b) ** 2; + ndf2 = (hsl.h - h) ** 2 + (hsl.s - s) ** 2 + (hsl.l - l) ** 2; + + ndf = ndf1 + ndf2 * 2; + if (df < 0 || df > ndf) { + df = ndf; + cl = index; + } + } + + return match; + }); + + name = colorNames[cl][1]; + + return name; +} diff --git a/packages/color/src/types/index.ts b/packages/color/src/types/index.ts new file mode 100644 index 0000000..3a63bc8 --- /dev/null +++ b/packages/color/src/types/index.ts @@ -0,0 +1,58 @@ +/** + * the color palette number + * + * the main color number is 500 + */ +export type ColorPaletteNumber = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950; + +/** the color palette */ +export type ColorPalette = { + /** the color hex value */ + hex: string; + /** + * the color number + * + * - 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950 + */ + number: ColorPaletteNumber; +}; + +/** the color palette family */ +export type ColorPaletteFamily = { + /** the color palette family name */ + name: string; + /** the color palettes */ + palettes: ColorPalette[]; +}; + +/** the color palette with delta */ +export type ColorPaletteWithDelta = ColorPalette & { + delta: number; +}; + +/** the color palette family with nearest palette */ +export type ColorPaletteFamilyWithNearestPalette = ColorPaletteFamily & { + nearestPalette: ColorPaletteWithDelta; + nearestLightnessPalette: ColorPaletteWithDelta; +}; + +/** the color palette match */ +export type ColorPaletteMatch = ColorPaletteFamily & { + /** the color map of the palette */ + colorMap: Map; + /** + * the main color of the palette + * + * which number is 500 + */ + main: ColorPalette; + /** the match color of the palette */ + match: ColorPalette; +}; + +/** + * The color index of color palette + * + * From left to right, the color is from light to dark, 6 is main color + */ +export type ColorIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11; diff --git a/packages/color/tsconfig.json b/packages/color/tsconfig.json new file mode 100644 index 0000000..5823ed5 --- /dev/null +++ b/packages/color/tsconfig.json @@ -0,0 +1,20 @@ +{ + "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"] +} diff --git a/packages/hooks/package.json b/packages/hooks/package.json new file mode 100644 index 0000000..598a093 --- /dev/null +++ b/packages/hooks/package.json @@ -0,0 +1,16 @@ +{ + "name": "@sa/hooks", + "version": "1.3.15", + "exports": { + ".": "./src/index.ts" + }, + "typesVersions": { + "*": { + "*": ["./src/*"] + } + }, + "dependencies": { + "@sa/axios": "workspace:*", + "@sa/utils": "workspace:*" + } +} diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts new file mode 100644 index 0000000..f976351 --- /dev/null +++ b/packages/hooks/src/index.ts @@ -0,0 +1,11 @@ +import useBoolean from './use-boolean'; +import useLoading from './use-loading'; +import useCountDown from './use-count-down'; +import useContext from './use-context'; +import useSvgIconRender from './use-svg-icon-render'; +import useTable from './use-table'; + +export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useTable }; + +export * from './use-signal'; +export type * from './use-table'; diff --git a/packages/hooks/src/use-boolean.ts b/packages/hooks/src/use-boolean.ts new file mode 100644 index 0000000..a60d45a --- /dev/null +++ b/packages/hooks/src/use-boolean.ts @@ -0,0 +1,31 @@ +import { ref } from 'vue'; + +/** + * Boolean + * + * @param initValue Init value + */ +export default function useBoolean(initValue = false) { + const bool = ref(initValue); + + function setBool(value: boolean) { + bool.value = value; + } + function setTrue() { + setBool(true); + } + function setFalse() { + setBool(false); + } + function toggle() { + setBool(!bool.value); + } + + return { + bool, + setBool, + setTrue, + setFalse, + toggle + }; +} diff --git a/packages/hooks/src/use-context.ts b/packages/hooks/src/use-context.ts new file mode 100644 index 0000000..001d8aa --- /dev/null +++ b/packages/hooks/src/use-context.ts @@ -0,0 +1,96 @@ +import { inject, provide } from 'vue'; +import type { InjectionKey } from 'vue'; + +/** + * Use context + * + * @example + * ```ts + * // there are three vue files: A.vue, B.vue, C.vue, and A.vue is the parent component of B.vue and C.vue + * + * // context.ts + * import { ref } from 'vue'; + * import { useContext } from '@sa/hooks'; + * + * export const { setupStore, useStore } = useContext('demo', () => { + * const count = ref(0); + * + * function increment() { + * count.value++; + * } + * + * function decrement() { + * count.value--; + * } + * + * return { + * count, + * increment, + * decrement + * }; + * }) + * ``` // A.vue + * ```vue + * + * + * ``` // B.vue + * ```vue + * + * + * ```; + * + * // C.vue is same as B.vue + * + * @param contextName Context name + * @param fn Context function + */ +export default function useContext any>(contextName: string, fn: T) { + type Context = ReturnType; + + const { useProvide, useInject: useStore } = createContext(contextName); + + function setupStore(...args: Parameters) { + const context: Context = fn(...args); + return useProvide(context); + } + + return { + /** Setup store in the parent component */ + setupStore, + /** Use store in the child component */ + useStore + }; +} + +/** Create context */ +function createContext(contextName: string) { + const injectKey: InjectionKey = Symbol(contextName); + + function useProvide(context: T) { + provide(injectKey, context); + + return context; + } + + function useInject() { + return inject(injectKey) as T; + } + + return { + useProvide, + useInject + }; +} diff --git a/packages/hooks/src/use-count-down.ts b/packages/hooks/src/use-count-down.ts new file mode 100644 index 0000000..4f95b73 --- /dev/null +++ b/packages/hooks/src/use-count-down.ts @@ -0,0 +1,68 @@ +import { computed, onScopeDispose, ref } from 'vue'; +import { useRafFn } from '@vueuse/core'; + +/** + * A hook for implementing a countdown timer. It uses `requestAnimationFrame` for smooth and accurate timing, + * independent of the screen refresh rate. + * + * @param initialSeconds - The total number of seconds for the countdown. + */ +export default function useCountDown(initialSeconds: number) { + const remainingSeconds = ref(0); + + const count = computed(() => Math.ceil(remainingSeconds.value)); + + const isCounting = computed(() => remainingSeconds.value > 0); + + const { pause, resume } = useRafFn( + ({ delta }) => { + // delta: milliseconds elapsed since the last frame. + + // If countdown already reached zero or below, ensure it's 0 and stop. + if (remainingSeconds.value <= 0) { + remainingSeconds.value = 0; + pause(); + return; + } + + // Calculate seconds passed since the last frame. + const secondsPassed = delta / 1000; + remainingSeconds.value -= secondsPassed; + + // If countdown has finished after decrementing. + if (remainingSeconds.value <= 0) { + remainingSeconds.value = 0; + pause(); + } + }, + { immediate: false } // The timer does not start automatically. + ); + + /** + * Starts the countdown. + * + * @param [updatedSeconds=initialSeconds] - Optionally, start with a new duration. Default is `initialSeconds` + */ + function start(updatedSeconds: number = initialSeconds) { + remainingSeconds.value = updatedSeconds; + resume(); + } + + /** Stops the countdown and resets the remaining time to 0. */ + function stop() { + remainingSeconds.value = 0; + pause(); + } + + // Ensure the rAF loop is cleaned up when the component is unmounted. + onScopeDispose(() => { + pause(); + }); + + return { + count, + isCounting, + start, + stop + }; +} diff --git a/packages/hooks/src/use-loading.ts b/packages/hooks/src/use-loading.ts new file mode 100644 index 0000000..b8f89ad --- /dev/null +++ b/packages/hooks/src/use-loading.ts @@ -0,0 +1,16 @@ +import useBoolean from './use-boolean'; + +/** + * Loading + * + * @param initValue Init value + */ +export default function useLoading(initValue = false) { + const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(initValue); + + return { + loading, + startLoading, + endLoading + }; +} diff --git a/packages/hooks/src/use-request.ts b/packages/hooks/src/use-request.ts new file mode 100644 index 0000000..219ac07 --- /dev/null +++ b/packages/hooks/src/use-request.ts @@ -0,0 +1,79 @@ +import { ref } from 'vue'; +import type { Ref } from 'vue'; +import { createFlatRequest } from '@sa/axios'; +import type { + AxiosError, + CreateAxiosDefaults, + CustomAxiosRequestConfig, + MappedType, + RequestInstanceCommon, + RequestOption, + ResponseType +} from '@sa/axios'; +import useLoading from './use-loading'; + +export type HookRequestInstanceResponseSuccessData = { + data: Ref; + error: Ref; +}; + +export type HookRequestInstanceResponseFailData = { + data: Ref; + error: Ref>; +}; + +export type HookRequestInstanceResponseData = { + loading: Ref; +} & (HookRequestInstanceResponseSuccessData | HookRequestInstanceResponseFailData); + +export interface HookRequestInstance> + extends RequestInstanceCommon { + ( + config: CustomAxiosRequestConfig + ): HookRequestInstanceResponseData>; +} + +/** + * create a hook request instance + * + * @param axiosConfig + * @param options + */ +export default function createHookRequest>( + axiosConfig?: CreateAxiosDefaults, + options?: Partial> +) { + const request = createFlatRequest(axiosConfig, options); + + const hookRequest: HookRequestInstance = function hookRequest< + T extends ApiData = ApiData, + R extends ResponseType = 'json' + >(config: CustomAxiosRequestConfig) { + const { loading, startLoading, endLoading } = useLoading(); + + const data = ref(null) as Ref>; + const error = ref(null) as Ref | null>; + + startLoading(); + + request(config).then(res => { + if (res.data) { + data.value = res.data as MappedType; + } else { + error.value = res.error; + } + + endLoading(); + }); + + return { + loading, + data, + error + }; + } as HookRequestInstance; + + hookRequest.cancelAllRequest = request.cancelAllRequest; + + return hookRequest; +} diff --git a/packages/hooks/src/use-signal.ts b/packages/hooks/src/use-signal.ts new file mode 100644 index 0000000..fa9d626 --- /dev/null +++ b/packages/hooks/src/use-signal.ts @@ -0,0 +1,144 @@ +import { computed, ref, shallowRef, triggerRef } from 'vue'; +import type { + ComputedGetter, + DebuggerOptions, + Ref, + ShallowRef, + WritableComputedOptions, + WritableComputedRef +} from 'vue'; + +type Updater = (value: T) => T; +type Mutator = (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 { + (): Readonly; + /** + * 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): 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): void; + /** + * Get the reference of the signal + * + * Sometimes it can be useful to make `v-model` work with the signal + * + * ```vue + * ; + * + * + * ``` + */ + getRef(): Readonly>>; +} + +export interface ReadonlySignal { + (): Readonly; +} + +export interface SignalOptions { + /** + * Whether to use `ref` to store the value + * + * @default false use `sharedRef` to store the value + */ + useRef?: boolean; +} + +export function useSignal(initialValue: T, options?: SignalOptions): Signal { + const { useRef } = options || {}; + + const state = useRef ? (ref(initialValue) as Ref) : shallowRef(initialValue); + + return createSignal(state); +} + +export function useComputed(getter: ComputedGetter, debugOptions?: DebuggerOptions): ReadonlySignal; +export function useComputed(options: WritableComputedOptions, debugOptions?: DebuggerOptions): Signal; +export function useComputed( + getterOrOptions: ComputedGetter | WritableComputedOptions, + debugOptions?: DebuggerOptions +) { + const isGetter = typeof getterOrOptions === 'function'; + + const computedValue = computed(getterOrOptions as any, debugOptions); + + if (isGetter) { + return () => computedValue.value as ReadonlySignal; + } + + return createSignal(computedValue); +} + +function createSignal(state: ShallowRef | WritableComputedRef): Signal { + const signal = () => state.value; + + signal.set = (value: T) => { + state.value = value; + }; + + signal.update = (updater: Updater) => { + state.value = updater(state.value); + }; + + signal.mutate = (mutator: Mutator) => { + mutator(state.value); + triggerRef(state); + }; + + signal.getRef = () => state as Readonly>>; + + return signal; +} diff --git a/packages/hooks/src/use-svg-icon-render.ts b/packages/hooks/src/use-svg-icon-render.ts new file mode 100644 index 0000000..62c2206 --- /dev/null +++ b/packages/hooks/src/use-svg-icon-render.ts @@ -0,0 +1,50 @@ +import { h } from 'vue'; +import type { Component } from 'vue'; + +/** + * Svg icon render hook + * + * @param SvgIcon Svg icon component + */ +export default function useSvgIconRender(SvgIcon: Component) { + interface IconConfig { + /** Iconify icon name */ + icon?: string; + /** Local icon name */ + localIcon?: string; + /** Icon color */ + color?: string; + /** Icon size */ + fontSize?: number; + } + + type IconStyle = Partial>; + + /** + * Svg icon VNode + * + * @param config + */ + const SvgIconVNode = (config: IconConfig) => { + const { color, fontSize, icon, localIcon } = config; + + const style: IconStyle = {}; + + if (color) { + style.color = color; + } + if (fontSize) { + style.fontSize = `${fontSize}px`; + } + + if (!icon && !localIcon) { + return undefined; + } + + return () => h(SvgIcon, { icon, localIcon, style }); + }; + + return { + SvgIconVNode + }; +} diff --git a/packages/hooks/src/use-table.ts b/packages/hooks/src/use-table.ts new file mode 100644 index 0000000..7a01035 --- /dev/null +++ b/packages/hooks/src/use-table.ts @@ -0,0 +1,132 @@ +import { computed, ref } from 'vue'; +import type { Ref, VNodeChild } from 'vue'; +import useBoolean from './use-boolean'; +import useLoading from './use-loading'; + +export interface PaginationData { + data: T[]; + pageNum: number; + pageSize: number; + total: number; +} + +type GetApiData = Pagination extends true ? PaginationData : ApiData[]; + +type Transform = ( + response: ResponseData +) => GetApiData; + +export type TableColumnCheckTitle = string | ((...args: any) => VNodeChild); + +export type TableColumnCheck = { + prop: string; + label: TableColumnCheckTitle; + checked: boolean; + visible: boolean; +}; + +export interface UseTableOptions { + /** + * api function to get table data + */ + api: () => Promise; + /** + * whether to enable pagination + */ + pagination?: Pagination; + /** + * transform api response to table data + */ + transform: Transform; + /** + * columns factory + */ + columns: () => Column[]; + /** + * get column checks + */ + getColumnChecks: (columns: Column[]) => TableColumnCheck[]; + /** + * get columns + */ + getColumns: (columns: Column[], checks: TableColumnCheck[]) => Column[]; + /** + * callback when response fetched + */ + onFetched?: (data: GetApiData) => void | Promise; + /** + * whether to get data immediately + * + * @default true + */ + immediate?: boolean; +} + +export default function useTable( + options: UseTableOptions +) { + const { loading, startLoading, endLoading } = useLoading(); + const { bool: empty, setBool: setEmpty } = useBoolean(); + + const { api, pagination, transform, columns, getColumnChecks, getColumns, onFetched, immediate = true } = options; + + const data = ref([]) as Ref; + + const columnChecks = ref(getColumnChecks(columns())) as Ref; + + const $columns = computed(() => getColumns(columns(), columnChecks.value)); + + function reloadColumns() { + const checkMap = new Map(columnChecks.value.map(col => [col.prop, col.checked])); + + const defaultChecks = getColumnChecks(columns()); + + columnChecks.value = defaultChecks.map(col => ({ + ...col, + checked: checkMap.get(col.prop) ?? col.checked + })); + } + + async function getData() { + try { + startLoading(); + + const response = await api(); + + const transformed = transform(response); + + data.value = getTableData(transformed, pagination); + + setEmpty(data.value.length === 0); + + await onFetched?.(transformed); + } finally { + endLoading(); + } + } + + if (immediate) { + getData(); + } + + return { + loading, + empty, + data, + columns: $columns, + columnChecks, + reloadColumns, + getData + }; +} + +function getTableData( + data: GetApiData, + pagination?: Pagination +) { + if (pagination) { + return (data as PaginationData).data; + } + + return data as ApiData[]; +} diff --git a/packages/hooks/tsconfig.json b/packages/hooks/tsconfig.json new file mode 100644 index 0000000..5823ed5 --- /dev/null +++ b/packages/hooks/tsconfig.json @@ -0,0 +1,20 @@ +{ + "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"] +} diff --git a/packages/materials/package.json b/packages/materials/package.json new file mode 100644 index 0000000..35000f8 --- /dev/null +++ b/packages/materials/package.json @@ -0,0 +1,19 @@ +{ + "name": "@sa/materials", + "version": "1.3.15", + "exports": { + ".": "./src/index.ts" + }, + "typesVersions": { + "*": { + "*": ["./src/*"] + } + }, + "dependencies": { + "@sa/utils": "workspace:*", + "simplebar-vue": "2.4.2" + }, + "devDependencies": { + "typed-css-modules": "0.9.1" + } +} diff --git a/packages/materials/src/index.ts b/packages/materials/src/index.ts new file mode 100644 index 0000000..f6ca01d --- /dev/null +++ b/packages/materials/src/index.ts @@ -0,0 +1,6 @@ +import AdminLayout, { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './libs/admin-layout'; +import PageTab from './libs/page-tab'; +import SimpleScrollbar from './libs/simple-scrollbar'; + +export { AdminLayout, LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX, PageTab, SimpleScrollbar }; +export * from './types'; diff --git a/packages/materials/src/libs/admin-layout/index.module.css b/packages/materials/src/libs/admin-layout/index.module.css new file mode 100644 index 0000000..e5c8ac8 --- /dev/null +++ b/packages/materials/src/libs/admin-layout/index.module.css @@ -0,0 +1,63 @@ +/* @type */ + +.layout-header, +.layout-header-placement { + height: var(--soy-header-height); +} + +.layout-header { + z-index: var(--soy-header-z-index); +} + +.layout-tab { + top: var(--soy-header-height); + height: var(--soy-tab-height); + z-index: var(--soy-tab-z-index); +} + +.layout-tab-placement { + height: var(--soy-tab-height); +} + +.layout-sider { + width: var(--soy-sider-width); + z-index: var(--soy-sider-z-index); +} + +.layout-mobile-sider { + z-index: var(--soy-sider-z-index); +} + +.layout-mobile-sider-mask { + z-index: var(--soy-mobile-sider-z-index); +} + +.layout-sider_collapsed { + width: var(--soy-sider-collapsed-width); + z-index: var(--soy-sider-z-index); +} + +.layout-footer, +.layout-footer-placement { + height: var(--soy-footer-height); +} + +.layout-footer { + z-index: var(--soy-footer-z-index); +} + +.left-gap { + padding-left: var(--soy-sider-width); +} + +.left-gap_collapsed { + padding-left: var(--soy-sider-collapsed-width); +} + +.sider-padding-top { + padding-top: var(--soy-header-height); +} + +.sider-padding-bottom { + padding-bottom: var(--soy-footer-height); +} diff --git a/packages/materials/src/libs/admin-layout/index.module.css.d.ts b/packages/materials/src/libs/admin-layout/index.module.css.d.ts new file mode 100644 index 0000000..c326c84 --- /dev/null +++ b/packages/materials/src/libs/admin-layout/index.module.css.d.ts @@ -0,0 +1,18 @@ +declare const styles: { + readonly 'layout-header': string; + readonly 'layout-header-placement': string; + readonly 'layout-tab': string; + readonly 'layout-tab-placement': string; + readonly 'layout-sider': string; + readonly 'layout-mobile-sider': string; + readonly 'layout-mobile-sider-mask': string; + readonly 'layout-sider_collapsed': string; + readonly 'layout-footer': string; + readonly 'layout-footer-placement': string; + readonly 'left-gap': string; + readonly 'left-gap_collapsed': string; + readonly 'sider-padding-top': string; + readonly 'sider-padding-bottom': string; +}; + +export default styles; diff --git a/packages/materials/src/libs/admin-layout/index.ts b/packages/materials/src/libs/admin-layout/index.ts new file mode 100644 index 0000000..0687362 --- /dev/null +++ b/packages/materials/src/libs/admin-layout/index.ts @@ -0,0 +1,5 @@ +import AdminLayout from './index.vue'; +import { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './shared'; + +export default AdminLayout; +export { LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX }; diff --git a/packages/materials/src/libs/admin-layout/index.vue b/packages/materials/src/libs/admin-layout/index.vue new file mode 100644 index 0000000..bb03185 --- /dev/null +++ b/packages/materials/src/libs/admin-layout/index.vue @@ -0,0 +1,238 @@ + + + + + diff --git a/packages/materials/src/libs/admin-layout/shared.ts b/packages/materials/src/libs/admin-layout/shared.ts new file mode 100644 index 0000000..940451e --- /dev/null +++ b/packages/materials/src/libs/admin-layout/shared.ts @@ -0,0 +1,68 @@ +import type { AdminLayoutProps, LayoutCssVars, LayoutCssVarsProps } from '../../types'; + +/** The id of the scroll element of the layout */ +export const LAYOUT_SCROLL_EL_ID = '__SCROLL_EL_ID__'; + +/** The max z-index of the layout */ +export const LAYOUT_MAX_Z_INDEX = 100; + +/** + * Create layout css vars by css vars props + * + * @param props Css vars props + */ +function createLayoutCssVarsByCssVarsProps(props: LayoutCssVarsProps) { + const cssVars: LayoutCssVars = { + '--soy-header-height': `${props.headerHeight}px`, + '--soy-header-z-index': props.headerZIndex, + '--soy-tab-height': `${props.tabHeight}px`, + '--soy-tab-z-index': props.tabZIndex, + '--soy-sider-width': `${props.siderWidth}px`, + '--soy-sider-collapsed-width': `${props.siderCollapsedWidth}px`, + '--soy-sider-z-index': props.siderZIndex, + '--soy-mobile-sider-z-index': props.mobileSiderZIndex, + '--soy-footer-height': `${props.footerHeight}px`, + '--soy-footer-z-index': props.footerZIndex + }; + + return cssVars; +} + +/** + * Create layout css vars + * + * @param props + */ +export function createLayoutCssVars(props: AdminLayoutProps) { + const { + mode, + isMobile, + maxZIndex = LAYOUT_MAX_Z_INDEX, + headerHeight, + tabHeight, + siderWidth, + siderCollapsedWidth, + footerHeight + } = props; + + const headerZIndex = maxZIndex - 3; + const tabZIndex = maxZIndex - 5; + const siderZIndex = mode === 'vertical' || isMobile ? maxZIndex - 1 : maxZIndex - 4; + const mobileSiderZIndex = isMobile ? maxZIndex - 2 : 0; + const footerZIndex = maxZIndex - 5; + + const cssProps: LayoutCssVarsProps = { + headerHeight, + headerZIndex, + tabHeight, + tabZIndex, + siderWidth, + siderZIndex, + mobileSiderZIndex, + siderCollapsedWidth, + footerHeight, + footerZIndex + }; + + return createLayoutCssVarsByCssVarsProps(cssProps); +} diff --git a/packages/materials/src/libs/page-tab/button-tab.vue b/packages/materials/src/libs/page-tab/button-tab.vue new file mode 100644 index 0000000..0826e71 --- /dev/null +++ b/packages/materials/src/libs/page-tab/button-tab.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/packages/materials/src/libs/page-tab/chrome-tab-bg.vue b/packages/materials/src/libs/page-tab/chrome-tab-bg.vue new file mode 100644 index 0000000..151e03a --- /dev/null +++ b/packages/materials/src/libs/page-tab/chrome-tab-bg.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/packages/materials/src/libs/page-tab/chrome-tab.vue b/packages/materials/src/libs/page-tab/chrome-tab.vue new file mode 100644 index 0000000..e4509b0 --- /dev/null +++ b/packages/materials/src/libs/page-tab/chrome-tab.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/packages/materials/src/libs/page-tab/index.module.css b/packages/materials/src/libs/page-tab/index.module.css new file mode 100644 index 0000000..abf56a5 --- /dev/null +++ b/packages/materials/src/libs/page-tab/index.module.css @@ -0,0 +1,97 @@ +/* @type */ + +.button-tab { + border-color: #e5e7eb; +} + +.button-tab_dark { + border-color: #ffffff3d; +} + +.button-tab:hover { + color: var(--soy-primary-color); + border-color: var(--soy-primary-color-opacity3); +} + +.button-tab_active { + color: var(--soy-primary-color); + border-color: var(--soy-primary-color-opacity3); + background-color: var(--soy-primary-color-opacity1); +} + +.button-tab_active_dark { + background-color: var(--soy-primary-color-opacity2); +} + +.button-tab .svg-close:hover { + font-size: 12px; + color: #ffffff; + background-color: var(--soy-primary-color); +} + +.button-tab_dark .svg-close:hover { + color: #000000; +} + +.chrome-tab:hover { + z-index: 9; +} + +.chrome-tab_active { + z-index: 10; + color: var(--soy-primary-color); +} + +.chrome-tab__bg { + color: transparent; +} + +.chrome-tab_active .chrome-tab__bg { + color: var(--soy-primary-color1); +} + +.chrome-tab_active_dark .chrome-tab__bg { + color: var(--soy-primary-color2); +} + +.chrome-tab:hover .chrome-tab__bg { + color: #dee1e6; +} + +.chrome-tab_active:hover .chrome-tab__bg { + color: var(--soy-primary-color1); +} + +.chrome-tab_dark:hover .chrome-tab__bg { + color: #333333; +} + +.chrome-tab_active_dark:hover .chrome-tab__bg { + color: var(--soy-primary-color2); +} + +.chrome-tab .svg-close:hover { + font-size: 12px; + color: #ffffff; + background-color: #9ca3af; +} + +.chrome-tab_active .svg-close:hover { + background-color: var(--soy-primary-color); +} + +.chrome-tab_dark .svg-close:hover { + color: #000000; +} + +.chrome-tab_active .chrome-tab-divider { + opacity: 0; +} + +.chrome-tab:hover .chrome-tab-divider { + opacity: 0; +} + +.chrome-tab_dark .chrome-tab-divider { + background-color: rgba(255, 255, 255, 0.9); +} diff --git a/packages/materials/src/libs/page-tab/index.module.css.d.ts b/packages/materials/src/libs/page-tab/index.module.css.d.ts new file mode 100644 index 0000000..ab45c0a --- /dev/null +++ b/packages/materials/src/libs/page-tab/index.module.css.d.ts @@ -0,0 +1,15 @@ +declare const styles: { + readonly 'button-tab': string; + readonly 'button-tab_dark': string; + readonly 'button-tab_active': string; + readonly 'button-tab_active_dark': string; + readonly 'chrome-tab': string; + readonly 'chrome-tab_active': string; + readonly 'chrome-tab__bg': string; + readonly 'chrome-tab_active_dark': string; + readonly 'chrome-tab_dark': string; + readonly 'chrome-tab-divider': string; + readonly 'svg-close': string; +}; + +export default styles; diff --git a/packages/materials/src/libs/page-tab/index.ts b/packages/materials/src/libs/page-tab/index.ts new file mode 100644 index 0000000..b402adf --- /dev/null +++ b/packages/materials/src/libs/page-tab/index.ts @@ -0,0 +1,3 @@ +import PageTab from './index.vue'; + +export default PageTab; diff --git a/packages/materials/src/libs/page-tab/index.vue b/packages/materials/src/libs/page-tab/index.vue new file mode 100644 index 0000000..0c0c699 --- /dev/null +++ b/packages/materials/src/libs/page-tab/index.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/packages/materials/src/libs/page-tab/shared.ts b/packages/materials/src/libs/page-tab/shared.ts new file mode 100644 index 0000000..db71f5d --- /dev/null +++ b/packages/materials/src/libs/page-tab/shared.ts @@ -0,0 +1,31 @@ +import { addColorAlpha, transformColorWithOpacity } from '@sa/color'; +import type { PageTabCssVars, PageTabCssVarsProps } from '../../types'; + +/** The active color of the tab */ +export const ACTIVE_COLOR = '#1890ff'; + +function createCssVars(props: PageTabCssVarsProps) { + const cssVars: PageTabCssVars = { + '--soy-primary-color': props.primaryColor, + '--soy-primary-color1': props.primaryColor1, + '--soy-primary-color2': props.primaryColor2, + '--soy-primary-color-opacity1': props.primaryColorOpacity1, + '--soy-primary-color-opacity2': props.primaryColorOpacity2, + '--soy-primary-color-opacity3': props.primaryColorOpacity3 + }; + + return cssVars; +} + +export function createTabCssVars(primaryColor: string) { + const cssProps: PageTabCssVarsProps = { + primaryColor, + primaryColor1: transformColorWithOpacity(primaryColor, 0.1, '#ffffff'), + primaryColor2: transformColorWithOpacity(primaryColor, 0.3, '#000000'), + primaryColorOpacity1: addColorAlpha(primaryColor, 0.1), + primaryColorOpacity2: addColorAlpha(primaryColor, 0.15), + primaryColorOpacity3: addColorAlpha(primaryColor, 0.3) + }; + + return createCssVars(cssProps); +} diff --git a/packages/materials/src/libs/page-tab/svg-close.vue b/packages/materials/src/libs/page-tab/svg-close.vue new file mode 100644 index 0000000..26ba07a --- /dev/null +++ b/packages/materials/src/libs/page-tab/svg-close.vue @@ -0,0 +1,18 @@ + + + + + diff --git a/packages/materials/src/libs/simple-scrollbar/index.ts b/packages/materials/src/libs/simple-scrollbar/index.ts new file mode 100644 index 0000000..1453a06 --- /dev/null +++ b/packages/materials/src/libs/simple-scrollbar/index.ts @@ -0,0 +1,3 @@ +import SimpleScrollbar from './index.vue'; + +export default SimpleScrollbar; diff --git a/packages/materials/src/libs/simple-scrollbar/index.vue b/packages/materials/src/libs/simple-scrollbar/index.vue new file mode 100644 index 0000000..1e8076b --- /dev/null +++ b/packages/materials/src/libs/simple-scrollbar/index.vue @@ -0,0 +1,18 @@ + + + + + diff --git a/packages/materials/src/types/index.ts b/packages/materials/src/types/index.ts new file mode 100644 index 0000000..bbcfb9d --- /dev/null +++ b/packages/materials/src/types/index.ts @@ -0,0 +1,294 @@ +/** Header config */ +interface AdminLayoutHeaderConfig { + /** + * Whether header is visible + * + * @default true + */ + headerVisible?: boolean; + /** + * Header class + * + * @default '' + */ + headerClass?: string; + /** + * Header height + * + * @default 56px + */ + headerHeight?: number; +} + +/** Tab config */ +interface AdminLayoutTabConfig { + /** + * Whether tab is visible + * + * @default true + */ + tabVisible?: boolean; + /** + * Tab class + * + * @default '' + */ + tabClass?: string; + /** + * Tab height + * + * @default 48px + */ + tabHeight?: number; +} + +/** Sider config */ +interface AdminLayoutSiderConfig { + /** + * Whether sider is visible + * + * @default true + */ + siderVisible?: boolean; + /** + * Sider class + * + * @default '' + */ + siderClass?: string; + /** + * Mobile sider class + * + * @default '' + */ + mobileSiderClass?: string; + /** + * Sider collapse status + * + * @default false + */ + siderCollapse?: boolean; + /** + * Sider width when collapse is false + * + * @default '220px' + */ + siderWidth?: number; + /** + * Sider width when collapse is true + * + * @default '64px' + */ + siderCollapsedWidth?: number; +} + +/** Content config */ +export interface AdminLayoutContentConfig { + /** + * Content class + * + * @default '' + */ + contentClass?: string; + /** + * Whether content is full the page + * + * If true, other elements will be hidden by `display: none` + */ + fullContent?: boolean; +} + +/** Footer config */ +export interface AdminLayoutFooterConfig { + /** + * Whether footer is visible + * + * @default true + */ + footerVisible?: boolean; + /** + * Whether footer is fixed + * + * @default true + */ + fixedFooter?: boolean; + /** + * Footer class + * + * @default '' + */ + footerClass?: string; + /** + * Footer height + * + * @default 48px + */ + footerHeight?: number; + /** + * Whether footer is on the right side + * + * When the layout is vertical, the footer is on the right side + */ + rightFooter?: boolean; +} + +/** + * Layout mode + * + * - Horizontal + * - Vertical + */ +export type LayoutMode = 'horizontal' | 'vertical'; + +/** + * The scroll mode when content overflow + * + * - Wrapper: the layout component's wrapper element has a scrollbar + * - Content: the layout component's content element has a scrollbar + * + * @default 'wrapper' + */ +export type LayoutScrollMode = 'wrapper' | 'content'; + +/** Admin layout props */ +export interface AdminLayoutProps + extends AdminLayoutHeaderConfig, + AdminLayoutTabConfig, + AdminLayoutSiderConfig, + AdminLayoutContentConfig, + AdminLayoutFooterConfig { + /** + * Layout mode + * + * - {@link LayoutMode} + */ + mode?: LayoutMode; + /** Is mobile layout */ + isMobile?: boolean; + /** + * Scroll mode + * + * - {@link ScrollMode} + */ + scrollMode?: LayoutScrollMode; + /** + * The id of the scroll element of the layout + * + * It can be used to get the corresponding Dom and scroll it + * + * @example + * use the default id by import + * ```ts + * import { adminLayoutScrollElId } from '@sa/vue-materials'; + * ``` + * + * @default + * ```ts + * const adminLayoutScrollElId = '__ADMIN_LAYOUT_SCROLL_EL_ID__' + * ``` + */ + scrollElId?: string; + /** The class of the scroll element */ + scrollElClass?: string; + /** The class of the scroll wrapper element */ + scrollWrapperClass?: string; + /** + * The common class of the layout + * + * Is can be used to configure the transition animation + * + * @default 'transition-all-300' + */ + commonClass?: string; + /** + * Whether fix the header and tab + * + * @default true + */ + fixedTop?: boolean; + /** + * The max z-index of the layout + * + * The z-index of Header,Tab,Sider and Footer will not exceed this value + */ + maxZIndex?: number; +} + +type Kebab = S extends Uncapitalize ? S : `-${Uncapitalize}`; + +type KebabCase = S extends `${infer Start}${infer End}` + ? `${Uncapitalize}${KebabCase>}` + : S; + +type Prefix = '--soy-'; + +export type LayoutCssVarsProps = Pick< + AdminLayoutProps, + 'headerHeight' | 'tabHeight' | 'siderWidth' | 'siderCollapsedWidth' | 'footerHeight' +> & { + headerZIndex?: number; + tabZIndex?: number; + siderZIndex?: number; + mobileSiderZIndex?: number; + footerZIndex?: number; +}; + +export type LayoutCssVars = { + [K in keyof LayoutCssVarsProps as `${Prefix}${KebabCase}`]: string | number; +}; + +/** + * The mode of the tab + * + * - Button: button style + * - Chrome: chrome style + * + * @default chrome + */ +export type PageTabMode = 'button' | 'chrome'; + +export interface PageTabProps { + /** Whether is dark mode */ + darkMode?: boolean; + /** + * The mode of the tab + * + * - {@link TabMode} + */ + mode?: PageTabMode; + /** + * The common class of the layout + * + * Is can be used to configure the transition animation + * + * @default 'transition-all-300' + */ + commonClass?: string; + /** The class of the button tab */ + buttonClass?: string; + /** The class of the chrome tab */ + chromeClass?: string; + /** Whether the tab is active */ + active?: boolean; + /** The color of the active tab */ + activeColor?: string; + /** + * Whether the tab is closable + * + * Show the close icon when true + */ + closable?: boolean; +} + +export type PageTabCssVarsProps = { + primaryColor: string; + primaryColor1: string; + primaryColor2: string; + primaryColorOpacity1: string; + primaryColorOpacity2: string; + primaryColorOpacity3: string; +}; + +export type PageTabCssVars = { + [K in keyof PageTabCssVarsProps as `${Prefix}${KebabCase}`]: string | number; +}; diff --git a/packages/materials/tsconfig.json b/packages/materials/tsconfig.json new file mode 100644 index 0000000..5823ed5 --- /dev/null +++ b/packages/materials/tsconfig.json @@ -0,0 +1,20 @@ +{ + "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"] +} diff --git a/packages/ofetch/package.json b/packages/ofetch/package.json new file mode 100644 index 0000000..7592932 --- /dev/null +++ b/packages/ofetch/package.json @@ -0,0 +1,15 @@ +{ + "name": "@sa/fetch", + "version": "1.3.15", + "exports": { + ".": "./src/index.ts" + }, + "typesVersions": { + "*": { + "*": ["./src/*"] + } + }, + "dependencies": { + "ofetch": "1.4.1" + } +} diff --git a/packages/ofetch/src/index.ts b/packages/ofetch/src/index.ts new file mode 100644 index 0000000..dce1ed4 --- /dev/null +++ b/packages/ofetch/src/index.ts @@ -0,0 +1,10 @@ +import { ofetch } from 'ofetch'; +import type { FetchOptions } from 'ofetch'; + +export function createRequest(options: FetchOptions) { + const request = ofetch.create(options); + + return request; +} + +export default createRequest; diff --git a/packages/ofetch/tsconfig.json b/packages/ofetch/tsconfig.json new file mode 100644 index 0000000..5823ed5 --- /dev/null +++ b/packages/ofetch/tsconfig.json @@ -0,0 +1,20 @@ +{ + "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"] +} diff --git a/packages/scripts/bin.ts b/packages/scripts/bin.ts new file mode 100644 index 0000000..1a1817b --- /dev/null +++ b/packages/scripts/bin.ts @@ -0,0 +1,3 @@ +#!/usr/bin/env tsx + +import './src/index.ts'; diff --git a/packages/scripts/package.json b/packages/scripts/package.json new file mode 100644 index 0000000..367448f --- /dev/null +++ b/packages/scripts/package.json @@ -0,0 +1,27 @@ +{ + "name": "@sa/scripts", + "version": "1.3.15", + "bin": { + "sa": "./bin.ts" + }, + "exports": { + ".": "./src/index.ts" + }, + "typesVersions": { + "*": { + "*": ["./src/*"] + } + }, + "devDependencies": { + "@soybeanjs/changelog": "0.3.25", + "bumpp": "10.2.3", + "c12": "3.2.0", + "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.3", + "rimraf": "6.0.1" + } +} diff --git a/packages/scripts/src/commands/changelog.ts b/packages/scripts/src/commands/changelog.ts new file mode 100644 index 0000000..e01ce40 --- /dev/null +++ b/packages/scripts/src/commands/changelog.ts @@ -0,0 +1,10 @@ +import { generateChangelog, generateTotalChangelog } from '@soybeanjs/changelog'; +import type { ChangelogOption } from '@soybeanjs/changelog'; + +export async function genChangelog(options?: Partial, total = false) { + if (total) { + await generateTotalChangelog(options); + } else { + await generateChangelog(options); + } +} diff --git a/packages/scripts/src/commands/cleanup.ts b/packages/scripts/src/commands/cleanup.ts new file mode 100644 index 0000000..a9d3990 --- /dev/null +++ b/packages/scripts/src/commands/cleanup.ts @@ -0,0 +1,5 @@ +import { rimraf } from 'rimraf'; + +export async function cleanup(paths: string[]) { + await rimraf(paths, { glob: true }); +} diff --git a/packages/scripts/src/commands/git-commit.ts b/packages/scripts/src/commands/git-commit.ts new file mode 100644 index 0000000..35c0df5 --- /dev/null +++ b/packages/scripts/src/commands/git-commit.ts @@ -0,0 +1,84 @@ +import path from 'node:path'; +import { readFileSync } from 'node:fs'; +import { prompt } from 'enquirer'; +import { execCommand } from '../shared'; +import { locales } from '../locales'; +import type { Lang } from '../locales'; + +interface PromptObject { + types: string; + scopes: string; + description: string; +} + +/** + * Git commit with Conventional Commits standard + * + * @param lang + */ +export async function gitCommit(lang: Lang = 'en-us') { + const { gitCommitMessages, gitCommitTypes, gitCommitScopes } = locales[lang]; + + const typesChoices = gitCommitTypes.map(([value, msg]) => { + const nameWithSuffix = `${value}:`; + + const message = `${nameWithSuffix.padEnd(12)}${msg}`; + + return { + name: value, + message + }; + }); + + const scopesChoices = gitCommitScopes.map(([value, msg]) => ({ + name: value, + message: `${value.padEnd(30)} (${msg})` + })); + + const result = await prompt([ + { + name: 'types', + type: 'select', + message: gitCommitMessages.types, + choices: typesChoices + }, + { + name: 'scopes', + type: 'select', + message: gitCommitMessages.scopes, + choices: scopesChoices + }, + { + name: 'description', + type: 'text', + message: gitCommitMessages.description + } + ]); + + const breaking = result.description.startsWith('!') ? '!' : ''; + + const description = result.description.replace(/^!/, '').trim(); + + const commitMsg = `${result.types}(${result.scopes})${breaking}: ${description}`; + + await execCommand('git', ['commit', '-m', commitMsg], { stdio: 'inherit' }); +} + +/** Git commit message verify */ +export async function gitCommitVerify(lang: Lang = 'en-us', ignores: RegExp[] = []) { + const gitPath = await execCommand('git', ['rev-parse', '--show-toplevel']); + + const gitMsgPath = path.join(gitPath, '.git', 'COMMIT_EDITMSG'); + + const commitMsg = readFileSync(gitMsgPath, 'utf8').trim(); + + if (ignores.some(regExp => regExp.test(commitMsg))) return; + + const REG_EXP = /(?[a-z]+)(?:\((?.+)\))?(?!)?: (?.+)/i; + + if (!REG_EXP.test(commitMsg)) { + const errorMsg = locales[lang].gitCommitVerify; + + throw new Error(errorMsg); + } +} diff --git a/packages/scripts/src/commands/index.ts b/packages/scripts/src/commands/index.ts new file mode 100644 index 0000000..c0ec9d6 --- /dev/null +++ b/packages/scripts/src/commands/index.ts @@ -0,0 +1,7 @@ +export * from './git-commit'; +export * from './cleanup'; +export * from './update-pkg'; +export * from './changelog'; +export * from './release'; +export * from './router'; +export * from './print-soybean'; diff --git a/packages/scripts/src/commands/print-soybean.ts b/packages/scripts/src/commands/print-soybean.ts new file mode 100644 index 0000000..1a09c42 --- /dev/null +++ b/packages/scripts/src/commands/print-soybean.ts @@ -0,0 +1,16 @@ +const SOYBEANJS = + '2020205f5f5f5f5f202020205f5f5f5f2020205f5f20202020205f5f20205f5f5f5f202020205f5f5f5f5f5f20202020202020202020202020205f2020205f20202020202020205f202020205f5f5f5f5f200a20202f205f5f5f5f7c20202f205f5f205c20205c205c2020202f202f207c20205f205c20207c20205f5f5f5f7c20202020202f5c20202020207c205c207c207c2020202020207c207c20202f205f5f5f5f7c0a207c20285f5f5f2020207c207c20207c207c20205c205c5f2f202f20207c207c5f29207c207c207c5f5f202020202020202f20205c202020207c20205c7c207c2020202020207c207c207c20285f5f5f20200a20205c5f5f5f205c20207c207c20207c207c2020205c2020202f2020207c20205f203c20207c20205f5f7c20202020202f202f5c205c2020207c202e2060207c20205f2020207c207c20205c5f5f5f205c200a20205f5f5f5f29207c207c207c5f5f7c207c202020207c207c202020207c207c5f29207c207c207c5f5f5f5f2020202f205f5f5f5f205c20207c207c5c20207c207c207c5f5f7c207c20205f5f5f5f29207c0a207c5f5f5f5f5f2f2020205c5f5f5f5f2f20202020207c5f7c202020207c5f5f5f5f2f20207c5f5f5f5f5f5f7c202f5f2f202020205c5f5c207c5f7c205c5f7c20205c5f5f5f5f2f20207c5f5f5f5f5f2f20'; + +function decode16(str: string): string { + const arr: string[] = []; + for (let i = 0; i < str.length; i += 2) { + const asciiCode = Number.parseInt(str.slice(i, i + 2), 16); + const charValue = String.fromCharCode(asciiCode); + arr.push(charValue); + } + return arr.join(''); +} + +export async function printSoybean() { + console.log(decode16(SOYBEANJS)); +} diff --git a/packages/scripts/src/commands/release.ts b/packages/scripts/src/commands/release.ts new file mode 100644 index 0000000..1cf3bc3 --- /dev/null +++ b/packages/scripts/src/commands/release.ts @@ -0,0 +1,12 @@ +import { versionBump } from 'bumpp'; + +export async function release(execute = 'pnpm sa changelog', push = true) { + await versionBump({ + files: ['**/package.json', '!**/node_modules'], + execute, + all: true, + tag: true, + commit: 'chore(projects): release v%s', + push + }); +} diff --git a/packages/scripts/src/commands/router.ts b/packages/scripts/src/commands/router.ts new file mode 100644 index 0000000..f407589 --- /dev/null +++ b/packages/scripts/src/commands/router.ts @@ -0,0 +1,90 @@ +import process from 'node:process'; +import path from 'node:path'; +import { writeFile } from 'node:fs/promises'; +import { existsSync, mkdirSync } from 'node:fs'; +import { prompt } from 'enquirer'; +import { green, red } from 'kolorist'; + +interface PromptObject { + routeName: string; + addRouteParams: boolean; + routeParams: string; +} + +/** generate route */ +export async function generateRoute() { + const result = await prompt([ + { + name: 'routeName', + type: 'text', + message: 'please enter route name', + initial: 'demo-route_child' + }, + { + name: 'addRouteParams', + type: 'confirm', + message: 'add route params?', + initial: false + } + ]); + + if (result.addRouteParams) { + const answers = await prompt({ + name: 'routeParams', + type: 'text', + message: 'please enter route params', + initial: 'id' + }); + + Object.assign(result, answers); + } + + const PAGE_DIR_NAME_PATTERN = /^[\w-]+[0-9a-zA-Z]+$/; + + if (!PAGE_DIR_NAME_PATTERN.test(result.routeName)) { + throw new Error(`${red('route name is invalid, it only allow letters, numbers, "-" or "_"')}. +For example: +(1) one level route: ${green('demo-route')} +(2) two level route: ${green('demo-route_child')} +(3) multi level route: ${green('demo-route_child_child')} +(4) group route: ${green('_ignore_demo-route')}' +`); + } + + const PARAM_REG = /^\w+$/g; + + if (result.routeParams && !PARAM_REG.test(result.routeParams)) { + throw new Error(red('route params is invalid, it only allow letters, numbers or "_".')); + } + + const cwd = process.cwd(); + + const [dir, ...rest] = result.routeName.split('_') as string[]; + + let routeDir = path.join(cwd, 'src', 'views', dir); + + if (rest.length) { + routeDir = path.join(routeDir, rest.join('_')); + } + + if (!existsSync(routeDir)) { + mkdirSync(routeDir, { recursive: true }); + } else { + throw new Error(red('route already exists')); + } + + const fileName = result.routeParams ? `[${result.routeParams}].vue` : 'index.vue'; + + const vueTemplate = ` + + + + +`; + + const filePath = path.join(routeDir, fileName); + + await writeFile(filePath, vueTemplate); +} diff --git a/packages/scripts/src/commands/update-pkg.ts b/packages/scripts/src/commands/update-pkg.ts new file mode 100644 index 0000000..54d1132 --- /dev/null +++ b/packages/scripts/src/commands/update-pkg.ts @@ -0,0 +1,5 @@ +import { execCommand } from '../shared'; + +export async function updatePkg(args: string[] = ['--deep', '-u']) { + execCommand('npx', ['ncu', ...args], { stdio: 'inherit' }); +} diff --git a/packages/scripts/src/config/index.ts b/packages/scripts/src/config/index.ts new file mode 100644 index 0000000..0fd9fbe --- /dev/null +++ b/packages/scripts/src/config/index.ts @@ -0,0 +1,39 @@ +import process from 'node:process'; +import { loadConfig } from 'c12'; +import type { CliOption } from '../types'; + +const defaultOptions: CliOption = { + cwd: process.cwd(), + cleanupDirs: [ + '**/dist', + '**/package-lock.json', + '**/yarn.lock', + '**/pnpm-lock.yaml', + '**/node_modules', + '!node_modules/**' + ], + ncuCommandArgs: ['--deep', '-u'], + changelogOptions: {}, + gitCommitVerifyIgnores: [ + /^((Merge pull request)|(Merge (.*?) into (.*?)|(Merge branch (.*?)))(?:\r?\n)*$)/m, + /^(Merge tag (.*?))(?:\r?\n)*$/m, + /^(R|r)evert (.*)/, + /^(amend|fixup|squash)!/, + /^(Merged (.*?)(in|into) (.*)|Merged PR (.*): (.*))/, + /^Merge remote-tracking branch(\s*)(.*)/, + /^Automatic merge(.*)/, + /^Auto-merged (.*?) into (.*)/ + ] +}; + +export async function loadCliOptions(overrides?: Partial, cwd = process.cwd()) { + const { config } = await loadConfig>({ + name: 'soybean', + defaults: defaultOptions, + overrides, + cwd, + packageJson: true + }); + + return config as CliOption; +} diff --git a/packages/scripts/src/index.ts b/packages/scripts/src/index.ts new file mode 100644 index 0000000..a537600 --- /dev/null +++ b/packages/scripts/src/index.ts @@ -0,0 +1,139 @@ +import cac from 'cac'; +import { blue, lightGreen } from 'kolorist'; +import { version } from '../package.json'; +import { + cleanup, + genChangelog, + generateRoute, + gitCommit, + gitCommitVerify, + printSoybean, + release, + updatePkg +} from './commands'; +import { loadCliOptions } from './config'; +import type { Lang } from './locales'; + +type Command = + | 'cleanup' + | 'update-pkg' + | 'git-commit' + | 'git-commit-verify' + | 'changelog' + | 'release' + | 'gen-route' + | 'create-route' + | 'print-soybean'; + +type CommandAction = (args?: A) => Promise | void; + +type CommandWithAction = Record }>; + +interface CommandArg { + /** Execute additional command after bumping and before git commit. Defaults to 'pnpm sa changelog' */ + execute?: string; + /** Indicates whether to push the git commit and tag. Defaults to true */ + push?: boolean; + /** Generate changelog by total tags */ + total?: boolean; + /** + * The glob pattern of dirs to clean up + * + * If not set, it will use the default value + * + * Multiple values use "," to separate them + */ + cleanupDir?: string; + /** + * display lang of cli + * + * @default 'en-us' + */ + lang?: Lang; +} + +export async function setupCli() { + const cliOptions = await loadCliOptions(); + + const cli = cac(blue('soybean-admin')); + + cli + .version(lightGreen(version)) + .option( + '-e, --execute [command]', + "Execute additional command after bumping and before git commit. Defaults to 'npx soy changelog'" + ) + .option('-p, --push', 'Indicates whether to push the git commit and tag') + .option('-t, --total', 'Generate changelog by total tags') + .option( + '-c, --cleanupDir ', + 'The glob pattern of dirs to cleanup, If not set, it will use the default value, Multiple values use "," to separate them' + ) + .option('-l, --lang ', 'display lang of cli', { default: 'en-us', type: [String] }) + .help(); + + const commands: CommandWithAction = { + cleanup: { + desc: 'delete dirs: node_modules, dist, etc.', + action: async () => { + await cleanup(cliOptions.cleanupDirs); + } + }, + 'update-pkg': { + desc: 'update package.json dependencies versions', + action: async () => { + await updatePkg(cliOptions.ncuCommandArgs); + } + }, + 'git-commit': { + desc: 'git commit, generate commit message which match Conventional Commits standard', + action: async args => { + await gitCommit(args?.lang); + } + }, + 'git-commit-verify': { + desc: 'verify git commit message, make sure it match Conventional Commits standard', + action: async args => { + await gitCommitVerify(args?.lang, cliOptions.gitCommitVerifyIgnores); + } + }, + changelog: { + desc: 'generate changelog', + action: async args => { + await genChangelog(cliOptions.changelogOptions, args?.total); + } + }, + release: { + desc: 'release: update version, generate changelog, commit code', + action: async args => { + await release(args?.execute, args?.push); + } + }, + 'gen-route': { + desc: 'create route page files', + action: async () => { + await generateRoute(); + } + }, + 'create-route': { + desc: 'create route page files', + action: async () => { + await generateRoute(); + } + }, + 'print-soybean': { + desc: 'print soybean', + action: async () => { + await printSoybean(); + } + } + }; + + for (const [command, { desc, action }] of Object.entries(commands)) { + cli.command(command, lightGreen(desc)).action(action); + } + + cli.parse(); +} + +setupCli(); diff --git a/packages/scripts/src/locales/index.ts b/packages/scripts/src/locales/index.ts new file mode 100644 index 0000000..74321d0 --- /dev/null +++ b/packages/scripts/src/locales/index.ts @@ -0,0 +1,82 @@ +import { bgRed, green, red, yellow } from 'kolorist'; + +export type Lang = 'zh-cn' | 'en-us'; + +export const locales = { + 'zh-cn': { + gitCommitMessages: { + types: '请选择提交类型', + scopes: '请选择提交范围', + description: `请输入描述信息(${yellow('!')}开头表示破坏性改动` + }, + gitCommitTypes: [ + ['feat', '新功能'], + ['feat-wip', '开发中的功能,比如某功能的部分代码'], + ['fix', '修复Bug'], + ['docs', '只涉及文档更新'], + ['typo', '代码或文档勘误,比如错误拼写'], + ['style', '修改代码风格,不影响代码含义的变更'], + ['refactor', '代码重构,既不修复 bug 也不添加功能的代码变更'], + ['perf', '可提高性能的代码更改'], + ['optimize', '优化代码质量的代码更改'], + ['test', '添加缺失的测试或更正现有测试'], + ['build', '影响构建系统或外部依赖项的更改'], + ['ci', '对 CI 配置文件和脚本的更改'], + ['chore', '没有修改src或测试文件的其他变更'], + ['revert', '还原先前的提交'] + ] as [string, string][], + gitCommitScopes: [ + ['projects', '项目'], + ['packages', '包'], + ['components', '组件'], + ['hooks', '钩子函数'], + ['utils', '工具函数'], + ['types', 'TS类型声明'], + ['styles', '代码风格'], + ['deps', '项目依赖'], + ['release', '发布项目新版本'], + ['other', '其他的变更'] + ] as [string, string][], + gitCommitVerify: `${bgRed(' 错误 ')} ${red('git 提交信息必须符合 Conventional Commits 标准!')}\n\n${green( + '推荐使用命令 `pnpm commit` 生成符合 Conventional Commits 标准的提交信息。\n获取有关 Conventional Commits 的更多信息,请访问此链接: https://conventionalcommits.org' + )}` + }, + 'en-us': { + gitCommitMessages: { + types: 'Please select a type', + scopes: 'Please select a scope', + description: `Please enter a description (add prefix ${yellow('!')} to indicate breaking change)` + }, + gitCommitTypes: [ + ['feat', 'A new feature'], + ['feat-wip', 'Features in development, such as partial code for a certain feature'], + ['fix', 'A bug fix'], + ['docs', 'Documentation only changes'], + ['typo', 'Code or document corrections, such as spelling errors'], + ['style', 'Changes that do not affect the meaning of the code'], + ['refactor', 'A code change that neither fixes a bug nor adds a feature'], + ['perf', 'A code change that improves performance'], + ['optimize', 'A code change that optimizes code quality'], + ['test', 'Adding missing tests or correcting existing tests'], + ['build', 'Changes that affect the build system or external dependencies'], + ['ci', 'Changes to our CI configuration files and scripts'], + ['chore', "Other changes that don't modify src or test files"], + ['revert', 'Reverts a previous commit'] + ] as [string, string][], + gitCommitScopes: [ + ['projects', 'project'], + ['packages', 'packages'], + ['components', 'components'], + ['hooks', 'hook functions'], + ['utils', 'utils functions'], + ['types', 'TS declaration'], + ['styles', 'style'], + ['deps', 'project dependencies'], + ['release', 'release project'], + ['other', 'other changes'] + ] as [string, string][], + gitCommitVerify: `${bgRed(' ERROR ')} ${red('git commit message must match the Conventional Commits standard!')}\n\n${green( + 'Recommended to use the command `pnpm commit` to generate Conventional Commits compliant commit information.\nGet more info about Conventional Commits, follow this link: https://conventionalcommits.org' + )}` + } +} satisfies Record>; diff --git a/packages/scripts/src/shared/index.ts b/packages/scripts/src/shared/index.ts new file mode 100644 index 0000000..9b1763f --- /dev/null +++ b/packages/scripts/src/shared/index.ts @@ -0,0 +1,7 @@ +import type { Options } from 'execa'; + +export async function execCommand(cmd: string, args: string[], options?: Options) { + const { execa } = await import('execa'); + const res = await execa(cmd, args, options); + return (res?.stdout as string)?.trim() || ''; +} diff --git a/packages/scripts/src/types/index.ts b/packages/scripts/src/types/index.ts new file mode 100644 index 0000000..32bc091 --- /dev/null +++ b/packages/scripts/src/types/index.ts @@ -0,0 +1,31 @@ +import type { ChangelogOption } from '@soybeanjs/changelog'; + +export interface CliOption { + /** The project root directory */ + cwd: string; + /** + * Cleanup dirs + * + * Glob pattern syntax {@link https://github.com/isaacs/minimatch} + * + * @default + * ```json + * ["** /dist", "** /pnpm-lock.yaml", "** /node_modules", "!node_modules/**"] + * ``` + */ + cleanupDirs: string[]; + /** + * Npm-check-updates command args + * + * @default ['--deep', '-u'] + */ + ncuCommandArgs: string[]; + /** + * Options of generate changelog + * + * @link https://github.com/soybeanjs/changelog + */ + changelogOptions: Partial; + /** The ignore pattern list of git commit verify */ + gitCommitVerifyIgnores: RegExp[]; +} diff --git a/packages/scripts/tsconfig.json b/packages/scripts/tsconfig.json new file mode 100644 index 0000000..67ab8a4 --- /dev/null +++ b/packages/scripts/tsconfig.json @@ -0,0 +1,20 @@ +{ + "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/**/*", "typings/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/uno-preset/package.json b/packages/uno-preset/package.json new file mode 100644 index 0000000..0f1af9d --- /dev/null +++ b/packages/uno-preset/package.json @@ -0,0 +1,12 @@ +{ + "name": "@sa/uno-preset", + "version": "1.3.15", + "exports": { + ".": "./src/index.ts" + }, + "typesVersions": { + "*": { + "*": ["./src/*"] + } + } +} diff --git a/packages/uno-preset/src/index.ts b/packages/uno-preset/src/index.ts new file mode 100644 index 0000000..6a1ae42 --- /dev/null +++ b/packages/uno-preset/src/index.ts @@ -0,0 +1,55 @@ +// @unocss-include + +import type { Preset } from '@unocss/core'; +import type { Theme } from '@unocss/preset-uno'; + +export function presetSoybeanAdmin(): Preset { + const preset: Preset = { + name: 'preset-soybean-admin', + shortcuts: [ + { + 'flex-center': 'flex justify-center items-center', + 'flex-x-center': 'flex justify-center', + 'flex-y-center': 'flex items-center', + 'flex-col': 'flex flex-col', + 'flex-col-center': 'flex-center flex-col', + 'flex-col-stretch': 'flex-col items-stretch', + 'i-flex-center': 'inline-flex justify-center items-center', + 'i-flex-x-center': 'inline-flex justify-center', + 'i-flex-y-center': 'inline-flex items-center', + 'i-flex-col': 'flex-col inline-flex', + 'i-flex-col-center': 'flex-col i-flex-center', + 'i-flex-col-stretch': 'i-flex-col items-stretch', + 'flex-1-hidden': 'flex-1 overflow-hidden' + }, + { + 'absolute-lt': 'absolute left-0 top-0', + 'absolute-lb': 'absolute left-0 bottom-0', + 'absolute-rt': 'absolute right-0 top-0', + 'absolute-rb': 'absolute right-0 bottom-0', + 'absolute-tl': 'absolute-lt', + 'absolute-tr': 'absolute-rt', + 'absolute-bl': 'absolute-lb', + 'absolute-br': 'absolute-rb', + 'absolute-center': 'absolute-lt flex-center size-full', + 'fixed-lt': 'fixed left-0 top-0', + 'fixed-lb': 'fixed left-0 bottom-0', + 'fixed-rt': 'fixed right-0 top-0', + 'fixed-rb': 'fixed right-0 bottom-0', + 'fixed-tl': 'fixed-lt', + 'fixed-tr': 'fixed-rt', + 'fixed-bl': 'fixed-lb', + 'fixed-br': 'fixed-rb', + 'fixed-center': 'fixed-lt flex-center size-full' + }, + { + 'nowrap-hidden': 'overflow-hidden whitespace-nowrap', + 'ellipsis-text': 'nowrap-hidden text-ellipsis' + } + ] + }; + + return preset; +} + +export default presetSoybeanAdmin; diff --git a/packages/uno-preset/tsconfig.json b/packages/uno-preset/tsconfig.json new file mode 100644 index 0000000..5823ed5 --- /dev/null +++ b/packages/uno-preset/tsconfig.json @@ -0,0 +1,20 @@ +{ + "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"] +} diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 0000000..bda6e8e --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,22 @@ +{ + "name": "@sa/utils", + "version": "1.3.15", + "exports": { + ".": "./src/index.ts" + }, + "typesVersions": { + "*": { + "*": ["./src/*"] + } + }, + "dependencies": { + "colord": "2.9.3", + "crypto-js": "4.2.0", + "klona": "2.0.6", + "localforage": "1.10.0", + "nanoid": "5.1.5" + }, + "devDependencies": { + "@types/crypto-js": "4.2.2" + } +} diff --git a/packages/utils/src/crypto.ts b/packages/utils/src/crypto.ts new file mode 100644 index 0000000..dc173c8 --- /dev/null +++ b/packages/utils/src/crypto.ts @@ -0,0 +1,27 @@ +import CryptoJS from 'crypto-js'; + +export class Crypto { + /** Secret */ + secret: string; + + constructor(secret: string) { + this.secret = secret; + } + + encrypt(data: T): string { + const dataString = JSON.stringify(data); + const encrypted = CryptoJS.AES.encrypt(dataString, this.secret); + return encrypted.toString(); + } + + decrypt(encrypted: string) { + const decrypted = CryptoJS.AES.decrypt(encrypted, this.secret); + const dataString = decrypted.toString(CryptoJS.enc.Utf8); + try { + return JSON.parse(dataString) as T; + } catch { + // avoid parse error + return null; + } + } +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts new file mode 100644 index 0000000..079fba6 --- /dev/null +++ b/packages/utils/src/index.ts @@ -0,0 +1,4 @@ +export * from './crypto'; +export * from './storage'; +export * from './nanoid'; +export * from './klona'; diff --git a/packages/utils/src/klona.ts b/packages/utils/src/klona.ts new file mode 100644 index 0000000..2fe5927 --- /dev/null +++ b/packages/utils/src/klona.ts @@ -0,0 +1,3 @@ +import { klona as jsonClone } from 'klona/json'; + +export { jsonClone }; diff --git a/packages/utils/src/nanoid.ts b/packages/utils/src/nanoid.ts new file mode 100644 index 0000000..5cc1d2e --- /dev/null +++ b/packages/utils/src/nanoid.ts @@ -0,0 +1,3 @@ +import { nanoid } from 'nanoid'; + +export { nanoid }; diff --git a/packages/utils/src/storage.ts b/packages/utils/src/storage.ts new file mode 100644 index 0000000..850562a --- /dev/null +++ b/packages/utils/src/storage.ts @@ -0,0 +1,76 @@ +import localforage from 'localforage'; + +/** The storage type */ +export type StorageType = 'local' | 'session'; + +export function createStorage(type: StorageType, storagePrefix: string) { + const stg = type === 'session' ? window.sessionStorage : window.localStorage; + + const storage = { + /** + * Set session + * + * @param key Session key + * @param value Session value + */ + set(key: K, value: T[K]) { + const json = JSON.stringify(value); + + stg.setItem(`${storagePrefix}${key as string}`, json); + }, + /** + * Get session + * + * @param key Session key + */ + get(key: K): T[K] | null { + const json = stg.getItem(`${storagePrefix}${key as string}`); + if (json) { + let storageData: T[K] | null = null; + + try { + storageData = JSON.parse(json); + } catch {} + + if (storageData) { + return storageData as T[K]; + } + } + + stg.removeItem(`${storagePrefix}${key as string}`); + + return null; + }, + remove(key: keyof T) { + stg.removeItem(`${storagePrefix}${key as string}`); + }, + clear() { + stg.clear(); + } + }; + return storage; +} + +type LocalForage = Omit & { + getItem(key: K, callback?: (err: any, value: T[K] | null) => void): Promise; + + setItem(key: K, value: T[K], callback?: (err: any, value: T[K]) => void): Promise; + + removeItem(key: keyof T, callback?: (err: any) => void): Promise; +}; + +type LocalforageDriver = 'local' | 'indexedDB' | 'webSQL'; + +export function createLocalforage(driver: LocalforageDriver) { + const driverMap: Record = { + local: localforage.LOCALSTORAGE, + indexedDB: localforage.INDEXEDDB, + webSQL: localforage.WEBSQL + }; + + localforage.config({ + driver: driverMap[driver] + }); + + return localforage as LocalForage; +} diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json new file mode 100644 index 0000000..5823ed5 --- /dev/null +++ b/packages/utils/tsconfig.json @@ -0,0 +1,20 @@ +{ + "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"] +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..dee51e9 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "packages/*" diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..fe16b5a Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..169b2ab --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ + diff --git a/scripts/generate-elegant-router.mjs b/scripts/generate-elegant-router.mjs new file mode 100644 index 0000000..78e1cf3 --- /dev/null +++ b/scripts/generate-elegant-router.mjs @@ -0,0 +1,60 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +import { setupElegantRouter } from '../build/plugins/router.ts'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const rootDir = path.resolve(__dirname, '..'); + +const generatedFiles = [ + path.resolve(rootDir, 'src/router/elegant/imports.ts'), + path.resolve(rootDir, 'src/router/elegant/routes.ts'), + path.resolve(rootDir, 'src/router/elegant/transform.ts'), + path.resolve(rootDir, 'src/typings/elegant-router.d.ts') +]; + +async function getMtimeMs(filePath) { + try { + const stat = await fs.stat(filePath); + return stat.mtimeMs; + } catch { + return 0; + } +} + +async function getGeneratedFileMtimes() { + return Promise.all(generatedFiles.map(getMtimeMs)); +} + +function isGenerated(before, after) { + return after.some((mtimeMs, index) => mtimeMs !== before[index]); +} + +async function waitForGeneration(beforeMtimes, timeoutMs = 10000) { + const startTime = Date.now(); + + while (Date.now() - startTime < timeoutMs) { + const afterMtimes = await getGeneratedFileMtimes(); + + if (isGenerated(beforeMtimes, afterMtimes)) { + return; + } + + await new Promise(resolve => setTimeout(resolve, 100)); + } + + throw new Error('Timed out while waiting for elegant-router generated files.'); +} + +process.chdir(rootDir); + +const beforeMtimes = await getGeneratedFileMtimes(); + +setupElegantRouter(); + +await waitForGeneration(beforeMtimes); + +console.log('Generated elegant-router files.'); diff --git a/scripts/generate-page-resource-manifest.mjs b/scripts/generate-page-resource-manifest.mjs new file mode 100644 index 0000000..17db812 --- /dev/null +++ b/scripts/generate-page-resource-manifest.mjs @@ -0,0 +1,206 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { customRoutes } from '../src/router/routes/custom-routes.ts'; +import { generatedRoutes } from '../src/router/elegant/routes.ts'; +import zhCn from '../src/locales/langs/zh-cn.ts'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const rootDir = path.resolve(__dirname, '..'); +const outputPath = path.resolve(rootDir, 'docs/frontend-page-resource-manifest.json'); +const excludedPageResourceRouteNames = new Set(['exception_403', 'exception_404', 'exception_500']); +const excludedPageResourcePathPrefixes = ['/function/', '/plugin/']; + +function isConstantRoute(route) { + return Boolean(route.meta?.constant); +} + +function getRouteOrder(route) { + const order = Number(route.meta?.order); + + if (Number.isFinite(order)) { + return order; + } + + return Number.MAX_SAFE_INTEGER; +} + +function sortRoutes(routes) { + return [...routes].sort((next, prev) => { + const orderDiff = getRouteOrder(next) - getRouteOrder(prev); + + if (orderDiff !== 0) { + return orderDiff; + } + + return String(next.path || '').localeCompare(String(prev.path || '')); + }); +} + +function getPageComponent(component) { + if (!component) { + return null; + } + + if (component.startsWith('view.')) { + return component; + } + + if (component.startsWith('layout.') && component.includes('$view.')) { + return component; + } + + return null; +} + +function getPageType(component) { + if (!component) { + return null; + } + + if (component.startsWith('view.')) { + return 'leaf'; + } + + if (component.startsWith('layout.') && component.includes('$view.')) { + return 'single'; + } + + return null; +} + +function shouldExcludePageResource(route) { + if (excludedPageResourceRouteNames.has(route.name)) { + return true; + } + + return excludedPageResourcePathPrefixes.some(prefix => String(route.path || '').startsWith(prefix)); +} + +function getNumberMetaValue(value) { + const parsed = Number(value); + + if (Number.isFinite(parsed)) { + return parsed; + } + + return null; +} + +function normalizeRouteProps(props) { + if (props === true) { + return true; + } + + if (props && typeof props === 'object') { + return props; + } + + return null; +} + +function getRouteLabel(route) { + const routeLocales = zhCn.route || {}; + const i18nKey = route.meta?.i18nKey; + + if (typeof i18nKey === 'string' && i18nKey.startsWith('route.')) { + const localeKey = i18nKey.slice('route.'.length); + + if (routeLocales[localeKey]) { + return routeLocales[localeKey]; + } + } + + if (routeLocales[route.name]) { + return routeLocales[route.name]; + } + + return route.meta?.title || route.name; +} + +function collectPageResources(routes, options, items) { + const { parentName = null, source } = options; + const orderedRoutes = sortRoutes(routes); + + for (const route of orderedRoutes) { + const pageComponent = getPageComponent(route.component); + const hideInMenu = Boolean(route.meta?.hideInMenu); + + // The backend page-node whitelist only contains menu-visible pages. + if (pageComponent && !hideInMenu && !shouldExcludePageResource(route)) { + const order = getNumberMetaValue(route.meta?.order); + const fixedIndexInTab = getNumberMetaValue(route.meta?.fixedIndexInTab); + const props = normalizeRouteProps(route.props); + const meta = { + title: getRouteLabel(route), + i18nKey: route.meta?.i18nKey || null, + icon: route.meta?.icon || null, + localIcon: route.meta?.localIcon || null, + order, + keepAlive: Boolean(route.meta?.keepAlive), + hideInMenu, + activeMenu: route.meta?.activeMenu || null, + multiTab: Boolean(route.meta?.multiTab), + fixedIndexInTab + }; + + items.push({ + name: route.name, + path: route.path, + component: pageComponent, + title: meta.title, + routeTitle: route.meta?.title || route.name, + i18nKey: meta.i18nKey, + icon: meta.icon, + localIcon: meta.localIcon, + order, + hideInMenu, + keepAlive: meta.keepAlive, + activeMenu: meta.activeMenu, + multiTab: meta.multiTab, + fixedIndexInTab, + redirect: route.redirect || null, + props, + meta, + parentName, + pageType: getPageType(pageComponent), + source + }); + } + + if (route.children?.length) { + collectPageResources(route.children, { parentName: route.name, source }, items); + } + } +} + +function getPageResources() { + const items = []; + + collectPageResources(customRoutes.filter(route => !isConstantRoute(route)), { source: 'custom' }, items); + collectPageResources(generatedRoutes.filter(route => !isConstantRoute(route)), { source: 'generated' }, items); + + const uniqueItems = Array.from(new Map(items.map(item => [item.name, item])).values()); + + return uniqueItems; +} + +const pageResources = getPageResources(); + +const result = { + generatedAt: new Date().toISOString(), + description: 'Frontend visible page resource whitelist for backend route/menu configuration.', + rules: { + directoryComponent: 'layout.base', + pageComponentPattern: 'view.', + singlePageComponentPattern: 'layout.$view.' + }, + total: pageResources.length, + items: pageResources +}; + +fs.writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`, 'utf8'); + +console.log(`Generated ${pageResources.length} page resources -> ${path.relative(rootDir, outputPath)}`); diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..b39f84d --- /dev/null +++ b/src/App.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/assets/imgs/soybean.jpg b/src/assets/imgs/soybean.jpg new file mode 100644 index 0000000..75457d5 Binary files /dev/null and b/src/assets/imgs/soybean.jpg differ diff --git a/src/assets/svg-icon/activity.svg b/src/assets/svg-icon/activity.svg new file mode 100644 index 0000000..abe892f --- /dev/null +++ b/src/assets/svg-icon/activity.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svg-icon/alova.svg b/src/assets/svg-icon/alova.svg new file mode 100644 index 0000000..a21d0e2 --- /dev/null +++ b/src/assets/svg-icon/alova.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/svg-icon/at-sign.svg b/src/assets/svg-icon/at-sign.svg new file mode 100644 index 0000000..625214d --- /dev/null +++ b/src/assets/svg-icon/at-sign.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svg-icon/avatar.svg b/src/assets/svg-icon/avatar.svg new file mode 100644 index 0000000..66fe6f2 --- /dev/null +++ b/src/assets/svg-icon/avatar.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svg-icon/banner.svg b/src/assets/svg-icon/banner.svg new file mode 100644 index 0000000..192b637 --- /dev/null +++ b/src/assets/svg-icon/banner.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svg-icon/cast.svg b/src/assets/svg-icon/cast.svg new file mode 100644 index 0000000..4f008d3 --- /dev/null +++ b/src/assets/svg-icon/cast.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svg-icon/chrome.svg b/src/assets/svg-icon/chrome.svg new file mode 100644 index 0000000..6314173 --- /dev/null +++ b/src/assets/svg-icon/chrome.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svg-icon/copy.svg b/src/assets/svg-icon/copy.svg new file mode 100644 index 0000000..ab25601 --- /dev/null +++ b/src/assets/svg-icon/copy.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svg-icon/custom-icon.svg b/src/assets/svg-icon/custom-icon.svg new file mode 100644 index 0000000..b33a43f --- /dev/null +++ b/src/assets/svg-icon/custom-icon.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svg-icon/empty-data.svg b/src/assets/svg-icon/empty-data.svg new file mode 100644 index 0000000..293486c --- /dev/null +++ b/src/assets/svg-icon/empty-data.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svg-icon/expectation.svg b/src/assets/svg-icon/expectation.svg new file mode 100644 index 0000000..1d87d5e --- /dev/null +++ b/src/assets/svg-icon/expectation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/svg-icon/heart.svg b/src/assets/svg-icon/heart.svg new file mode 100644 index 0000000..56e59b4 --- /dev/null +++ b/src/assets/svg-icon/heart.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svg-icon/logo.png b/src/assets/svg-icon/logo.png new file mode 100644 index 0000000..670ac77 Binary files /dev/null and b/src/assets/svg-icon/logo.png differ diff --git a/src/assets/svg-icon/logo.svg b/src/assets/svg-icon/logo.svg new file mode 100644 index 0000000..341675d --- /dev/null +++ b/src/assets/svg-icon/logo.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svg-icon/network-error.svg b/src/assets/svg-icon/network-error.svg new file mode 100644 index 0000000..52f97ab --- /dev/null +++ b/src/assets/svg-icon/network-error.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svg-icon/no-icon.svg b/src/assets/svg-icon/no-icon.svg new file mode 100644 index 0000000..f6dcdd0 --- /dev/null +++ b/src/assets/svg-icon/no-icon.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svg-icon/no-permission.svg b/src/assets/svg-icon/no-permission.svg new file mode 100644 index 0000000..4c408ca --- /dev/null +++ b/src/assets/svg-icon/no-permission.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svg-icon/not-found.svg b/src/assets/svg-icon/not-found.svg new file mode 100644 index 0000000..a513656 --- /dev/null +++ b/src/assets/svg-icon/not-found.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svg-icon/service-error.svg b/src/assets/svg-icon/service-error.svg new file mode 100644 index 0000000..0120f1e --- /dev/null +++ b/src/assets/svg-icon/service-error.svg @@ -0,0 +1 @@ + diff --git a/src/assets/svg-icon/visactor.svg b/src/assets/svg-icon/visactor.svg new file mode 100644 index 0000000..5c4e884 --- /dev/null +++ b/src/assets/svg-icon/visactor.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svg-icon/wind.svg b/src/assets/svg-icon/wind.svg new file mode 100644 index 0000000..7c90590 --- /dev/null +++ b/src/assets/svg-icon/wind.svg @@ -0,0 +1 @@ + diff --git a/src/components/advanced/table-column-setting.vue b/src/components/advanced/table-column-setting.vue new file mode 100644 index 0000000..741ecf9 --- /dev/null +++ b/src/components/advanced/table-column-setting.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/src/components/advanced/table-header-operation.vue b/src/components/advanced/table-header-operation.vue new file mode 100644 index 0000000..c8ec0ba --- /dev/null +++ b/src/components/advanced/table-header-operation.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/src/components/common/app-provider.vue b/src/components/common/app-provider.vue new file mode 100644 index 0000000..83e8f4f --- /dev/null +++ b/src/components/common/app-provider.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/src/components/common/dark-mode-container.vue b/src/components/common/dark-mode-container.vue new file mode 100644 index 0000000..71f2733 --- /dev/null +++ b/src/components/common/dark-mode-container.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/src/components/common/exception-base.vue b/src/components/common/exception-base.vue new file mode 100644 index 0000000..fa1fe93 --- /dev/null +++ b/src/components/common/exception-base.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/src/components/common/full-screen.vue b/src/components/common/full-screen.vue new file mode 100644 index 0000000..d7f88aa --- /dev/null +++ b/src/components/common/full-screen.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/src/components/common/lang-switch.vue b/src/components/common/lang-switch.vue new file mode 100644 index 0000000..85bd083 --- /dev/null +++ b/src/components/common/lang-switch.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/src/components/common/menu-toggler.vue b/src/components/common/menu-toggler.vue new file mode 100644 index 0000000..31cb1b9 --- /dev/null +++ b/src/components/common/menu-toggler.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/src/components/common/pin-toggler.vue b/src/components/common/pin-toggler.vue new file mode 100644 index 0000000..aecc92d --- /dev/null +++ b/src/components/common/pin-toggler.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/src/components/common/reload-button.vue b/src/components/common/reload-button.vue new file mode 100644 index 0000000..9af971f --- /dev/null +++ b/src/components/common/reload-button.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/src/components/common/system-logo.vue b/src/components/common/system-logo.vue new file mode 100644 index 0000000..f99c83c --- /dev/null +++ b/src/components/common/system-logo.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/src/components/common/theme-schema-switch.vue b/src/components/common/theme-schema-switch.vue new file mode 100644 index 0000000..aa07610 --- /dev/null +++ b/src/components/common/theme-schema-switch.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/src/components/custom/better-scroll.vue b/src/components/custom/better-scroll.vue new file mode 100644 index 0000000..7d2559c --- /dev/null +++ b/src/components/custom/better-scroll.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/src/components/custom/business-form-dialog.vue b/src/components/custom/business-form-dialog.vue new file mode 100644 index 0000000..02d5a9f --- /dev/null +++ b/src/components/custom/business-form-dialog.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/src/components/custom/business-form-drawer.vue b/src/components/custom/business-form-drawer.vue new file mode 100644 index 0000000..49e027d --- /dev/null +++ b/src/components/custom/business-form-drawer.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/src/components/custom/business-form-section.vue b/src/components/custom/business-form-section.vue new file mode 100644 index 0000000..c90fe2f --- /dev/null +++ b/src/components/custom/business-form-section.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/src/components/custom/business-table-action-cell.tsx b/src/components/custom/business-table-action-cell.tsx new file mode 100644 index 0000000..7fadb6d --- /dev/null +++ b/src/components/custom/business-table-action-cell.tsx @@ -0,0 +1,111 @@ +import { computed, defineComponent, ref } from 'vue'; +import type { PropType } from 'vue'; +import { ElButton, ElPopover } from 'element-plus'; +import { $t } from '@/locales'; + +export type BusinessTableAction = { + key: string; + label: string; + buttonType?: 'primary' | 'success' | 'warning' | 'danger' | 'info'; + disabled?: boolean; + onClick: () => void | Promise; +}; + +export default defineComponent({ + name: 'BusinessTableActionCell', + props: { + actions: { + type: Array as PropType, + required: true + } + }, + setup(props) { + const popoverVisible = ref(false); + + const directActions = computed(() => { + if (props.actions.length <= 2) { + return props.actions; + } + + return props.actions.slice(0, 1); + }); + + const moreActions = computed(() => { + if (props.actions.length <= 2) { + return []; + } + + return props.actions.slice(1); + }); + + async function handleAction(action: BusinessTableAction) { + if (action.disabled) { + return; + } + + popoverVisible.value = false; + await action.onClick(); + } + + return () => ( +
event.stopPropagation()}> + {directActions.value.map(action => ( + handleAction(action)} + > + {action.label} + + ))} + + {moreActions.value.length > 0 && ( + + {{ + reference: () => ( + event.stopPropagation()} + > + + {$t('common.more')} + + + + ), + default: () => ( +
+ {moreActions.value.map(action => ( + handleAction(action)} + > + {action.label} + + ))} +
+ ) + }} +
+ )} +
+ ); + } +}); diff --git a/src/components/custom/button-icon.vue b/src/components/custom/button-icon.vue new file mode 100644 index 0000000..037780e --- /dev/null +++ b/src/components/custom/button-icon.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/src/components/custom/count-to.vue b/src/components/custom/count-to.vue new file mode 100644 index 0000000..910b4cc --- /dev/null +++ b/src/components/custom/count-to.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/src/components/custom/custom-icon-select.vue b/src/components/custom/custom-icon-select.vue new file mode 100644 index 0000000..36012a1 --- /dev/null +++ b/src/components/custom/custom-icon-select.vue @@ -0,0 +1,239 @@ + + + + + diff --git a/src/components/custom/github-link.vue b/src/components/custom/github-link.vue new file mode 100644 index 0000000..3463800 --- /dev/null +++ b/src/components/custom/github-link.vue @@ -0,0 +1,18 @@ + + + + + diff --git a/src/components/custom/look-forward.vue b/src/components/custom/look-forward.vue new file mode 100644 index 0000000..0d1de23 --- /dev/null +++ b/src/components/custom/look-forward.vue @@ -0,0 +1,18 @@ + + + + + diff --git a/src/components/custom/soybean-avatar.vue b/src/components/custom/soybean-avatar.vue new file mode 100644 index 0000000..e7256c2 --- /dev/null +++ b/src/components/custom/soybean-avatar.vue @@ -0,0 +1,9 @@ + + + + + diff --git a/src/components/custom/svg-icon.vue b/src/components/custom/svg-icon.vue new file mode 100644 index 0000000..504763e --- /dev/null +++ b/src/components/custom/svg-icon.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/src/components/custom/table-search-panel.vue b/src/components/custom/table-search-panel.vue new file mode 100644 index 0000000..f7ff21e --- /dev/null +++ b/src/components/custom/table-search-panel.vue @@ -0,0 +1,163 @@ + + + + + diff --git a/src/components/custom/wave-bg.vue b/src/components/custom/wave-bg.vue new file mode 100644 index 0000000..b7d5653 --- /dev/null +++ b/src/components/custom/wave-bg.vue @@ -0,0 +1,648 @@ + + + + + diff --git a/src/components/custom/wave-bg.vue.bak b/src/components/custom/wave-bg.vue.bak new file mode 100644 index 0000000..1d63f2e --- /dev/null +++ b/src/components/custom/wave-bg.vue.bak @@ -0,0 +1,61 @@ + + + + + diff --git a/src/components/custom/web-site-link.vue b/src/components/custom/web-site-link.vue new file mode 100644 index 0000000..13b8c47 --- /dev/null +++ b/src/components/custom/web-site-link.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/src/constants/app.ts b/src/constants/app.ts new file mode 100644 index 0000000..f1177fa --- /dev/null +++ b/src/constants/app.ts @@ -0,0 +1,55 @@ +import { transformRecordToOption } from '@/utils/common'; + +export const GLOBAL_HEADER_MENU_ID = '__GLOBAL_HEADER_MENU__'; + +export const GLOBAL_SIDER_MENU_ID = '__GLOBAL_SIDER_MENU__'; + +export const themeSchemaRecord: Record = { + light: 'theme.themeSchema.light', + dark: 'theme.themeSchema.dark', + auto: 'theme.themeSchema.auto' +}; + +export const themeSchemaOptions = transformRecordToOption(themeSchemaRecord); + +export const loginModuleRecord: Record = { + 'pwd-login': 'page.login.pwdLogin.title', + 'reset-pwd': 'page.login.resetPwd.title' +}; + +export const themeLayoutModeRecord: Record = { + vertical: 'theme.layoutMode.vertical', + 'vertical-mix': 'theme.layoutMode.vertical-mix', + horizontal: 'theme.layoutMode.horizontal', + 'horizontal-mix': 'theme.layoutMode.horizontal-mix' +}; + +export const themeLayoutModeOptions = transformRecordToOption(themeLayoutModeRecord); + +export const themeScrollModeRecord: Record = { + wrapper: 'theme.scrollMode.wrapper', + content: 'theme.scrollMode.content' +}; + +export const themeScrollModeOptions = transformRecordToOption(themeScrollModeRecord); + +export const themeTabModeRecord: Record = { + chrome: 'theme.tab.mode.chrome', + button: 'theme.tab.mode.button' +}; + +export const themeTabModeOptions = transformRecordToOption(themeTabModeRecord); + +export const themePageAnimationModeRecord: Record = { + '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' +}; + +export const themePageAnimationModeOptions = transformRecordToOption(themePageAnimationModeRecord); + +export const DARK_CLASS = 'dark'; diff --git a/src/constants/business.ts b/src/constants/business.ts new file mode 100644 index 0000000..c0f2cd5 --- /dev/null +++ b/src/constants/business.ts @@ -0,0 +1,69 @@ +import { transformRecordToOption } from '@/utils/common'; + +export const enableStatusRecord: Record = { + '1': 'page.system.common.status.enable', + '2': 'page.system.common.status.disable' +}; + +export const enableStatusOptions = transformRecordToOption(enableStatusRecord); + +export const commonStatusRecord: Record = { + 0: 'page.system.common.status.enable', + 1: 'page.system.common.status.disable' +}; + +export const commonStatusOptions = [ + { value: 0, label: commonStatusRecord[0] }, + { value: 1, label: commonStatusRecord[1] } +] satisfies CommonType.Option[]; + +export const dictStatusRecord: Record<'0' | '1', App.I18n.I18nKey> = { + '0': 'page.system.common.status.enable', + '1': 'page.system.common.status.disable' +}; + +export const dictStatusOptions = [ + { value: 0, label: dictStatusRecord['0'] }, + { value: 1, label: dictStatusRecord['1'] } +] satisfies CommonType.Option[]; + +export const userGenderRecord: Record = { + 0: 'page.system.user.gender.unknown', + 1: 'page.system.user.gender.male', + 2: 'page.system.user.gender.female' +}; + +export const userGenderOptions = transformRecordToOption(userGenderRecord); + +export const menuTypeRecord: Record = { + 1: 'page.system.menu.type.directory', + 2: 'page.system.menu.type.menu', + 3: 'page.system.menu.type.button' +}; + +export const menuTypeOptions = [ + { value: 1, label: menuTypeRecord[1] }, + { value: 2, label: menuTypeRecord[2] }, + { value: 3, label: menuTypeRecord[3] } +] satisfies CommonType.Option[]; + +export const roleTypeRecord: Record = { + 1: 'page.system.role.type.system', + 2: 'page.system.role.type.custom' +}; + +export const roleTypeOptions = [ + { value: 1, label: roleTypeRecord[1] }, + { value: 2, label: roleTypeRecord[2] } +] satisfies CommonType.Option[]; + +export const menuRouteKindRecord: Record = { + dir: 'page.system.menu.routeKindEnum.directory', + view: 'page.system.menu.routeKindEnum.view', + single: 'page.system.menu.routeKindEnum.single', + iframe: 'page.system.menu.routeKindEnum.iframe', + external: 'page.system.menu.routeKindEnum.external', + redirect: 'page.system.menu.routeKindEnum.redirect' +}; + +export const menuRouteKindOptions = transformRecordToOption(menuRouteKindRecord); diff --git a/src/constants/common.ts b/src/constants/common.ts new file mode 100644 index 0000000..2d5e05e --- /dev/null +++ b/src/constants/common.ts @@ -0,0 +1,8 @@ +import { transformRecordToOption } from '@/utils/common'; + +export const yesOrNoRecord: Record = { + Y: 'common.yesOrNo.yes', + N: 'common.yesOrNo.no' +}; + +export const yesOrNoOptions = transformRecordToOption(yesOrNoRecord); diff --git a/src/constants/map-sdk.ts b/src/constants/map-sdk.ts new file mode 100644 index 0000000..eea25d2 --- /dev/null +++ b/src/constants/map-sdk.ts @@ -0,0 +1,8 @@ +/** baidu map sdk url */ +export const BAIDU_MAP_SDK_URL = `https://api.map.baidu.com/getscript?v=3.0&ak=KSezYymXPth1DIGILRX3oYN9PxbOQQmU&services=&t=20210201100830&s=1`; + +/** Amap sdk url */ +export const AMAP_SDK_URL = 'https://webapi.amap.com/maps?v=2.0&key=e7bd02bd504062087e6563daf4d6721d'; + +/** tencent sdk url */ +export const TENCENT_MAP_SDK_URL = 'https://map.qq.com/api/gljs?v=1.exp&key=A6DBZ-KXPLW-JKSRY-ONZF4-CPHY3-K6BL7'; diff --git a/src/constants/reg.ts b/src/constants/reg.ts new file mode 100644 index 0000000..8805ca8 --- /dev/null +++ b/src/constants/reg.ts @@ -0,0 +1,25 @@ +export const REG_USER_NAME = /^[\u4E00-\u9FA5a-zA-Z0-9_-]{4,16}$/; + +/** Phone reg */ +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}$/; + +/** + * Password reg + * + * 6-18 characters, including letters, numbers, and underscores + */ +export const REG_PWD = /^\w{6,18}$/; + +/** Email reg */ +export const REG_EMAIL = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/; + +/** Six digit code reg */ +export const REG_CODE_SIX = /^\d{6}$/; + +/** Four digit code reg */ +export const REG_CODE_FOUR = /^\d{4}$/; + +/** Url reg */ +export const REG_URL = + /(((^https?:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)$/; diff --git a/src/constants/service.ts b/src/constants/service.ts new file mode 100644 index 0000000..0822e36 --- /dev/null +++ b/src/constants/service.ts @@ -0,0 +1,5 @@ +/** Web 端网关前缀 */ +export const WEB_SERVICE_PREFIX = '/admin-api'; + +/** System 模块接口前缀 */ +export const SYSTEM_SERVICE_PREFIX = `${WEB_SERVICE_PREFIX}/system`; diff --git a/src/enum/index.ts b/src/enum/index.ts new file mode 100644 index 0000000..2739b3a --- /dev/null +++ b/src/enum/index.ts @@ -0,0 +1,7 @@ +export enum SetupStoreId { + App = 'app-store', + Theme = 'theme-store', + Auth = 'auth-store', + Route = 'route-store', + Tab = 'tab-store' +} diff --git a/src/hooks/business/auth.ts b/src/hooks/business/auth.ts new file mode 100644 index 0000000..f8fc749 --- /dev/null +++ b/src/hooks/business/auth.ts @@ -0,0 +1,21 @@ +import { useAuthStore } from '@/store/modules/auth'; + +export function useAuth() { + const authStore = useAuthStore(); + + function hasAuth(codes: string | string[]) { + if (!authStore.isLogin) { + return false; + } + + if (typeof codes === 'string') { + return authStore.userInfo.buttons.includes(codes); + } + + return codes.some(code => authStore.userInfo.buttons.includes(code)); + } + + return { + hasAuth + }; +} diff --git a/src/hooks/business/captcha.ts b/src/hooks/business/captcha.ts new file mode 100644 index 0000000..98124d9 --- /dev/null +++ b/src/hooks/business/captcha.ts @@ -0,0 +1,71 @@ +import { computed } from 'vue'; +import { useCountDown, useLoading } from '@sa/hooks'; +import { REG_PHONE } from '@/constants/reg'; +import { $t } from '@/locales'; + +export function useCaptcha() { + const { loading, startLoading, endLoading } = useLoading(); + const { count, start, stop, isCounting } = useCountDown(10); + + const label = computed(() => { + let text = $t('page.login.codeLogin.getCode'); + + const countingLabel = $t('page.login.codeLogin.reGetCode', { time: count.value }); + + if (loading.value) { + text = ''; + } + + if (isCounting.value) { + text = countingLabel; + } + + return text; + }); + + function isPhoneValid(phone: string) { + if (phone.trim() === '') { + window.$message?.error?.($t('form.phone.required')); + + return false; + } + + if (!REG_PHONE.test(phone)) { + window.$message?.error?.($t('form.phone.invalid')); + + return false; + } + + return true; + } + + async function getCaptcha(phone: string) { + const valid = isPhoneValid(phone); + + if (!valid || loading.value) { + return; + } + + startLoading(); + + // request + await new Promise(resolve => { + setTimeout(resolve, 500); + }); + + window.$message?.success?.($t('page.login.codeLogin.sendCodeSuccess')); + + start(); + + endLoading(); + } + + return { + label, + start, + stop, + isCounting, + loading, + getCaptcha + }; +} diff --git a/src/hooks/common/echarts.ts b/src/hooks/common/echarts.ts new file mode 100644 index 0000000..7330683 --- /dev/null +++ b/src/hooks/common/echarts.ts @@ -0,0 +1,239 @@ +import { computed, effectScope, nextTick, onScopeDispose, ref, 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'; +import type { + BarSeriesOption, + GaugeSeriesOption, + LineSeriesOption, + PictorialBarSeriesOption, + PieSeriesOption, + RadarSeriesOption, + ScatterSeriesOption +} from 'echarts/charts'; +import { + DatasetComponent, + GridComponent, + LegendComponent, + TitleComponent, + ToolboxComponent, + TooltipComponent, + TransformComponent +} from 'echarts/components'; +import type { + DatasetComponentOption, + GridComponentOption, + LegendComponentOption, + TitleComponentOption, + ToolboxComponentOption, + TooltipComponentOption +} from 'echarts/components'; +import { LabelLayout, UniversalTransition } from 'echarts/features'; +import { CanvasRenderer } from 'echarts/renderers'; +import { useThemeStore } from '@/store/modules/theme'; + +export type ECOption = echarts.ComposeOption< + | BarSeriesOption + | LineSeriesOption + | PieSeriesOption + | ScatterSeriesOption + | PictorialBarSeriesOption + | RadarSeriesOption + | GaugeSeriesOption + | TitleComponentOption + | LegendComponentOption + | TooltipComponentOption + | GridComponentOption + | ToolboxComponentOption + | DatasetComponentOption +>; + +echarts.use([ + TitleComponent, + LegendComponent, + TooltipComponent, + GridComponent, + DatasetComponent, + TransformComponent, + ToolboxComponent, + BarChart, + LineChart, + PieChart, + ScatterChart, + PictorialBarChart, + RadarChart, + GaugeChart, + LabelLayout, + UniversalTransition, + CanvasRenderer +]); + +interface ChartHooks { + onRender?: (chart: echarts.ECharts) => void | Promise; + onUpdated?: (chart: echarts.ECharts) => void | Promise; + onDestroy?: (chart: echarts.ECharts) => void | Promise; +} + +/** + * use echarts + * + * @param optionsFactory echarts options factory function + * @param darkMode dark mode + */ +export function useEcharts(optionsFactory: () => T, hooks: ChartHooks = {}) { + const scope = effectScope(); + + const themeStore = useThemeStore(); + const darkMode = computed(() => themeStore.darkMode); + + const domRef = ref(null); + const initialSize = { width: 0, height: 0 }; + const { width, height } = useElementSize(domRef, initialSize); + + let chart: echarts.ECharts | null = null; + const chartOptions: T = optionsFactory(); + + const { + onRender = instance => { + const textColor = darkMode.value ? 'rgb(224, 224, 224)' : 'rgb(31, 31, 31)'; + const maskColor = darkMode.value ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.8)'; + + instance.showLoading({ + color: themeStore.themeColor, + textColor, + fontSize: 14, + maskColor + }); + }, + onUpdated = instance => { + instance.hideLoading(); + }, + 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); + } + + /** + * update chart options + * + * @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); + + if (isRendered()) { + chart?.clear(); + } + + chart?.setOption({ ...updatedOpts, backgroundColor: 'transparent' }); + + await onUpdated?.(chart!); + } + + function setOptions(options: T) { + chart?.setOption(options); + } + + /** render chart */ + async function render() { + if (!isRendered()) { + const chartTheme = darkMode.value ? 'dark' : 'light'; + + await nextTick(); + + chart = echarts.init(domRef.value, chartTheme); + + chart.setOption({ ...chartOptions, backgroundColor: 'transparent' }); + + await onRender?.(chart); + } + } + + /** resize chart */ + function resize() { + chart?.resize(); + } + + /** destroy chart */ + async function destroy() { + if (!chart) return; + + await onDestroy?.(chart); + chart?.dispose(); + chart = null; + } + + /** change chart theme */ + async function changeTheme() { + await destroy(); + await render(); + await onUpdated?.(chart!); + } + + /** + * render chart by size + * + * @param w width + * @param h height + */ + async function renderChartBySize(w: number, h: number) { + initialSize.width = w; + initialSize.height = h; + + // size is abnormal, destroy chart + if (!canRender()) { + await destroy(); + + return; + } + + // resize chart + if (isRendered()) { + resize(); + } + + // render chart + await render(); + + if (chart) { + await onUpdated?.(chart); + } + } + + scope.run(() => { + watch([width, height], ([newWidth, newHeight]) => { + renderChartBySize(newWidth, newHeight); + }); + + watch(darkMode, () => { + changeTheme(); + }); + }); + + onScopeDispose(() => { + destroy(); + scope.stop(); + }); + + return { + domRef, + updateOptions, + setOptions + }; +} diff --git a/src/hooks/common/form.ts b/src/hooks/common/form.ts new file mode 100644 index 0000000..ad22509 --- /dev/null +++ b/src/hooks/common/form.ts @@ -0,0 +1,97 @@ +import { ref, toValue } from 'vue'; +import type { ComputedRef, Ref } from 'vue'; +import type { FormInstance } from 'element-plus'; +import { REG_CODE_SIX, REG_EMAIL, REG_PHONE, REG_PWD, REG_USER_NAME } from '@/constants/reg'; +import { $t } from '@/locales'; + +export function useFormRules() { + const patternRules = { + userName: { + pattern: REG_USER_NAME, + message: $t('form.userName.invalid'), + trigger: 'change' + }, + phone: { + pattern: REG_PHONE, + message: $t('form.phone.invalid'), + trigger: 'change' + }, + pwd: { + pattern: REG_PWD, + message: $t('form.pwd.invalid'), + trigger: 'change' + }, + code: { + pattern: REG_CODE_SIX, + message: $t('form.code.invalid'), + trigger: 'change' + }, + email: { + pattern: REG_EMAIL, + message: $t('form.email.invalid'), + trigger: 'change' + } + } satisfies Record; + + const formRules = { + userName: [createRequiredRule($t('form.userName.required')), patternRules.userName], + phone: [createRequiredRule($t('form.phone.required')), patternRules.phone], + pwd: [createRequiredRule($t('form.pwd.required')), patternRules.pwd], + code: [createRequiredRule($t('form.code.required')), patternRules.code], + email: [createRequiredRule($t('form.email.required')), patternRules.email] + } satisfies Record; + + /** the default required rule */ + const defaultRequiredRule = createRequiredRule($t('form.required')); + + function createRequiredRule(message: string): App.Global.FormRule { + return { + required: true, + message + }; + } + + /** create a rule for confirming the password */ + function createConfirmPwdRule(pwd: string | Ref | ComputedRef) { + const confirmPwdRule: App.Global.FormRule[] = [ + { required: true, message: $t('form.confirmPwd.required') }, + { + asyncValidator: (rule, value) => { + if (value.trim() !== '' && value !== toValue(pwd)) { + return Promise.reject(rule.message); + } + return Promise.resolve(); + }, + message: $t('form.confirmPwd.invalid'), + trigger: 'input' + } + ]; + return confirmPwdRule; + } + + return { + patternRules, + formRules, + defaultRequiredRule, + createRequiredRule, + createConfirmPwdRule + }; +} + +export function useForm() { + const formRef = ref(null); + + async function validate() { + await formRef.value?.validate(); + } + + async function restoreValidation() { + formRef.value?.resetFields(); + } + + return { + formRef, + validate, + restoreValidation + }; +} diff --git a/src/hooks/common/icon.ts b/src/hooks/common/icon.ts new file mode 100644 index 0000000..8998f60 --- /dev/null +++ b/src/hooks/common/icon.ts @@ -0,0 +1,10 @@ +import { useSvgIconRender } from '@sa/hooks'; +import SvgIcon from '@/components/custom/svg-icon.vue'; + +export function useSvgIcon() { + const { SvgIconVNode } = useSvgIconRender(SvgIcon); + + return { + SvgIconVNode + }; +} diff --git a/src/hooks/common/router.ts b/src/hooks/common/router.ts new file mode 100644 index 0000000..4ab21ea --- /dev/null +++ b/src/hooks/common/router.ts @@ -0,0 +1,120 @@ +import { useRouter } from 'vue-router'; +import type { RouteLocationRaw } from 'vue-router'; +import type { RouteKey } from '@elegant-router/types'; +import { router as globalRouter } from '@/router'; + +/** + * Router push + * + * Jump to the specified route, it can replace function router.push + * + * @param inSetup Whether is in vue script setup + */ +export function useRouterPush(inSetup = true) { + const router = inSetup ? useRouter() : globalRouter; + const route = globalRouter.currentRoute; + + const routerPush = router.push; + + const routerBack = router.back; + + interface RouterPushOptions { + query?: Record; + params?: Record; + } + + async function routerPushByKey(key: RouteKey, options?: RouterPushOptions) { + const { query, params } = options || {}; + + const routeLocation: RouteLocationRaw = { + name: key + }; + + if (Object.keys(query || {}).length) { + routeLocation.query = query; + } + + if (Object.keys(params || {}).length) { + routeLocation.params = params; + } + + return routerPush(routeLocation); + } + + function routerPushByKeyWithMetaQuery(key: RouteKey) { + const allRoutes = router.getRoutes(); + const meta = allRoutes.find(item => item.name === key)?.meta || null; + + const query: Record = {}; + + meta?.query?.forEach(item => { + query[item.key] = item.value; + }); + + return routerPushByKey(key, { query }); + } + + async function toHome() { + return routerPushByKey('root'); + } + + /** + * Navigate to login page + * + * @param loginModule The login module + * @param redirectUrl The redirect url, if not specified, it will be the current route fullPath + */ + async function toLogin(loginModule?: UnionKey.LoginModule, redirectUrl?: string) { + const module = loginModule || 'pwd-login'; + + const options: RouterPushOptions = { + params: { + module + } + }; + + const redirect = redirectUrl || route.value.fullPath; + + options.query = { + redirect + }; + + return routerPushByKey('login', options); + } + + /** + * Toggle login module + * + * @param module + */ + async function toggleLoginModule(module: UnionKey.LoginModule) { + const query = route.value.query as Record; + + return routerPushByKey('login', { query, params: { module } }); + } + + /** + * Redirect from login + * + * @param [needRedirect=true] Whether to redirect after login. Default is `true` + */ + async function redirectFromLogin(needRedirect = true) { + const redirect = route.value.query?.redirect as string; + + if (needRedirect && redirect) { + await routerPush(redirect); + } else { + await toHome(); + } + } + + return { + routerPush, + routerBack, + routerPushByKey, + routerPushByKeyWithMetaQuery, + toLogin, + toggleLoginModule, + redirectFromLogin + }; +} diff --git a/src/hooks/common/table.ts b/src/hooks/common/table.ts new file mode 100644 index 0000000..3c80e9c --- /dev/null +++ b/src/hooks/common/table.ts @@ -0,0 +1,318 @@ +import { computed, effectScope, onScopeDispose, reactive, shallowRef, watch } from 'vue'; +import type { Ref } from 'vue'; +import type { PaginationEmits, PaginationProps } from 'element-plus'; +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 { useAppStore } from '@/store/modules/app'; +import { $t } from '@/locales'; + +type RemoveReadonly = { + -readonly [key in keyof T]: T[key]; +}; + +export type UseUITableOptions = Omit< + UseTableOptions, Pagination>, + 'pagination' | 'getColumnChecks' | 'getColumns' +> & { + /** + * get column visible + * + * @param column + * + * @default true + * + * @returns true if the column is visible, false otherwise + */ + getColumnVisible?: (column: UI.TableColumn) => boolean; +}; + +const SELECTION_KEY = '__selection__'; + +const EXPAND_KEY = '__expand__'; + +const INDEX_KEY = '__index__'; + +export function useUITable(options: UseUITableOptions) { + const scope = effectScope(); + const appStore = useAppStore(); + + const result = useTable, false>({ + ...options, + getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible), + getColumns + }); + + // 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); + }); + + scope.run(() => { + watch( + () => appStore.locale, + () => { + result.reloadColumns(); + } + ); + }); + + onScopeDispose(() => { + scope.stop(); + }); + + return { + ...result, + scrollX + }; +} + +type PaginationParams = Pick; + +type UseUIPaginatedTableOptions = UseUITableOptions & { + paginationProps?: Partial>; + /** + * whether to show the total count of the table + * + * @default true + */ + showTotal?: boolean; + onPaginationParamsChange?: (params: PaginationParams) => void | Promise; +}; + +export function useUIPaginatedTable(options: UseUIPaginatedTableOptions) { + const scope = effectScope(); + const appStore = useAppStore(); + + const isMobile = computed(() => appStore.isMobile); + + const pagination: Partial> = reactive({ + currentPage: 1, + pageSize: 10, + total: 0, + pageSizes: [10, 15, 20, 25, 30], + 'current-change': (page: number) => { + pagination.currentPage = page; + + return true; + }, + 'size-change': (pageSize: number) => { + pagination.currentPage = 1; + pagination.pageSize = pageSize; + + return true; + }, + ...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: Partial> = { + ...pagination, + pagerCount: isMobile.value ? 3 : 9 + }; + + return p; + }); + + const paginationParams = computed(() => { + const { currentPage, pageSize } = pagination; + + return { + currentPage, + pageSize + }; + }); + + const result = useTable, true>({ + ...options, + pagination: true, + getColumnChecks: cols => getColumnChecks(cols, options.getColumnVisible), + getColumns, + onFetched: data => { + pagination.total = data.total; + } + }); + + async function getDataByPage(page: number = 1) { + if (page !== pagination.currentPage) { + pagination.currentPage = 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( + data: Ref, + idKey: keyof TableData, + getData: () => Promise +) { + const { bool: drawerVisible, setTrue: openDrawer, setFalse: closeDrawer } = useBoolean(); + + const operateType = shallowRef('add'); + + function handleAdd() { + operateType.value = 'add'; + openDrawer(); + } + + /** the editing row data */ + const editingData = shallowRef(null); + + function handleEdit(id: TableData[keyof TableData]) { + operateType.value = 'edit'; + const findItem = data.value.find(item => item[idKey] === id) || null; + editingData.value = jsonClone(findItem); + + openDrawer(); + } + + /** the checked row keys of table */ + const checkedRowKeys = shallowRef([]); + + /** the hook after the batch delete operation is completed */ + async function onBatchDeleted() { + window.$message?.success($t('common.deleteSuccess')); + + checkedRowKeys.value = []; + + await getData(); + } + + /** the hook after the delete operation is completed */ + async function onDeleted() { + window.$message?.success($t('common.deleteSuccess')); + + await getData(); + } + + return { + drawerVisible, + openDrawer, + closeDrawer, + operateType, + handleAdd, + editingData, + handleEdit, + checkedRowKeys, + onBatchDeleted, + onDeleted + }; +} + +export function defaultTransform( + response: FlatResponseData> +): PaginationData { + 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>( + cols: Column[], + getColumnVisible?: (column: Column) => boolean +) { + const checks: TableColumnCheck[] = []; + + cols.forEach(column => { + if (column.type === 'selection') { + checks.push({ + prop: SELECTION_KEY, + label: $t('common.check'), + checked: true, + visible: getColumnVisible?.(column) ?? false + }); + } else if (column.type === 'expand') { + checks.push({ + prop: EXPAND_KEY, + label: $t('common.expandColumn'), + checked: true, + visible: getColumnVisible?.(column) ?? false + }); + } else if (column.type === 'index') { + checks.push({ + prop: INDEX_KEY, + label: $t('common.index'), + checked: true, + visible: getColumnVisible?.(column) ?? false + }); + } else { + checks.push({ + prop: column.prop as string, + label: column.label as string, + checked: true, + visible: getColumnVisible?.(column) ?? true + }); + } + }); + + return checks; +} + +function getColumns>(cols: Column[], checks: TableColumnCheck[]) { + const columnMap = new Map(); + + cols.forEach(column => { + if (column.type === 'selection') { + columnMap.set(SELECTION_KEY, column); + } else if (column.type === 'expand') { + columnMap.set(EXPAND_KEY, column); + } else if (column.type === 'index') { + columnMap.set(INDEX_KEY, column); + } else { + columnMap.set(column.prop as string, column); + } + }); + + const filteredColumns = checks.filter(item => item.checked).map(check => columnMap.get(check.prop) as Column); + + return filteredColumns; +} diff --git a/src/hooks/common/vchart.ts b/src/hooks/common/vchart.ts new file mode 100644 index 0000000..5f3ff46 --- /dev/null +++ b/src/hooks/common/vchart.ts @@ -0,0 +1,158 @@ +import { computed, effectScope, onScopeDispose, ref, watch } from 'vue'; +import { useElementSize } from '@vueuse/core'; +import VChart, { registerLiquidChart } from '@visactor/vchart'; +import type { ISpec, ITheme } from '@visactor/vchart'; +import light from '@visactor/vchart-theme/public/light.json'; +import dark from '@visactor/vchart-theme/public/dark.json'; +import { useThemeStore } from '@/store/modules/theme'; + +registerLiquidChart(); + +// register the theme +VChart.ThemeManager.registerTheme('light', light as ITheme); +VChart.ThemeManager.registerTheme('dark', dark as ITheme); + +interface ChartHooks { + onRender?: (chart: VChart) => void | Promise; + onUpdated?: (chart: VChart) => void | Promise; + onDestroy?: (chart: VChart) => void | Promise; +} + +export function useVChart(specFactory: () => T, hooks: ChartHooks = {}) { + const scope = effectScope(); + const themeStore = useThemeStore(); + const darkMode = computed(() => themeStore.darkMode); + + const domRef = ref(null); + const initialSize = { width: 0, height: 0 }; + const { width, height } = useElementSize(domRef, initialSize); + + let chart: VChart | null = null; + const spec: T = specFactory(); + + const { onRender, onUpdated, 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); + } + + /** + * update chart spec + * + * @param callback callback function + */ + async function updateSpec(callback: (opts: T, optsFactory: () => T) => ISpec = () => spec) { + if (!isRendered()) return; + + const updatedOpts = callback(spec, specFactory); + + Object.assign(spec, updatedOpts); + + // if (isRendered()) { + // chart?.release(); + // } + + chart?.updateSpec({ ...updatedOpts }, true); + + await onUpdated?.(chart!); + } + + function setSpec(newSpec: T) { + chart?.updateSpec(newSpec); + } + + /** render chart */ + async function render() { + if (!isRendered()) { + // apply the theme + if (darkMode.value) { + VChart.ThemeManager.setCurrentTheme('dark'); + } else { + VChart.ThemeManager.setCurrentTheme('light'); + } + + chart = new VChart(spec, { dom: domRef.value as HTMLElement }); + chart.renderSync(); + + await onRender?.(chart); + } + } + + /** resize chart */ + function resize() { + // chart?.resize(); + } + + /** destroy chart */ + async function destroy() { + if (!chart) return; + + await onDestroy?.(chart); + chart?.release(); + chart = null; + } + + /** change chart theme */ + async function changeTheme() { + await destroy(); + await render(); + await onUpdated?.(chart!); + } + + /** + * render chart by size + * + * @param w width + * @param h height + */ + async function renderChartBySize(w: number, h: number) { + initialSize.width = w; + initialSize.height = h; + + // size is abnormal, destroy chart + if (!canRender()) { + await destroy(); + + return; + } + + // resize chart + if (isRendered()) { + resize(); + } + + // render chart + await render(); + } + + scope.run(() => { + watch([width, height], ([newWidth, newHeight]) => { + renderChartBySize(newWidth, newHeight); + }); + + watch(darkMode, () => { + changeTheme(); + }); + }); + + onScopeDispose(() => { + destroy(); + scope.stop(); + }); + + return { + domRef, + updateSpec, + setSpec + }; +} diff --git a/src/layouts/base-layout/index.vue b/src/layouts/base-layout/index.vue new file mode 100644 index 0000000..14355e2 --- /dev/null +++ b/src/layouts/base-layout/index.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/src/layouts/blank-layout/index.vue b/src/layouts/blank-layout/index.vue new file mode 100644 index 0000000..3868caa --- /dev/null +++ b/src/layouts/blank-layout/index.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/src/layouts/context/index.ts b/src/layouts/context/index.ts new file mode 100644 index 0000000..481d4ee --- /dev/null +++ b/src/layouts/context/index.ts @@ -0,0 +1,109 @@ +import { computed, nextTick, 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 { 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(() => routeStore.menus); + + const firstLevelMenus = computed(() => + routeStore.menus.map(menu => { + const { children: _, ...rest } = menu; + + return rest; + }) + ); + + const childLevelMenus = computed( + () => 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 { routerPushByKeyWithMetaQuery } = useRouterPush(); + + const selectedKey = computed(() => { + const { hideInMenu, activeMenu } = route.meta; + const name = route.name as string; + + const routeName = (hideInMenu ? activeMenu : name) || name; + + return routeName; + }); + + const selectedKeyDummy = ref(selectedKey.value); + + watch( + () => selectedKey.value, + val => { + selectedKeyDummy.value = val; + } + ); + + function handleSelect(key: RouteKey) { + selectedKeyDummy.value = key; + + routerPushByKeyWithMetaQuery(key); + + if (key.endsWith('-link')) { + nextTick(() => { + selectedKeyDummy.value = selectedKey.value; + }); + } + } + + return { + selectedKey, + selectedKeyDummy, + handleSelect + }; +} diff --git a/src/layouts/modules/global-breadcrumb/index.vue b/src/layouts/modules/global-breadcrumb/index.vue new file mode 100644 index 0000000..2949d0b --- /dev/null +++ b/src/layouts/modules/global-breadcrumb/index.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/src/layouts/modules/global-content/index.vue b/src/layouts/modules/global-content/index.vue new file mode 100644 index 0000000..b3e2691 --- /dev/null +++ b/src/layouts/modules/global-content/index.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/src/layouts/modules/global-footer/index.vue b/src/layouts/modules/global-footer/index.vue new file mode 100644 index 0000000..478a74b --- /dev/null +++ b/src/layouts/modules/global-footer/index.vue @@ -0,0 +1,13 @@ + + + + + diff --git a/src/layouts/modules/global-header/components/theme-button.vue b/src/layouts/modules/global-header/components/theme-button.vue new file mode 100644 index 0000000..ee5881d --- /dev/null +++ b/src/layouts/modules/global-header/components/theme-button.vue @@ -0,0 +1,18 @@ + + + + + diff --git a/src/layouts/modules/global-header/components/user-avatar.vue b/src/layouts/modules/global-header/components/user-avatar.vue new file mode 100644 index 0000000..9700c11 --- /dev/null +++ b/src/layouts/modules/global-header/components/user-avatar.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/src/layouts/modules/global-header/index.vue b/src/layouts/modules/global-header/index.vue new file mode 100644 index 0000000..44e92b4 --- /dev/null +++ b/src/layouts/modules/global-header/index.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/src/layouts/modules/global-logo/index.vue b/src/layouts/modules/global-logo/index.vue new file mode 100644 index 0000000..70dfce7 --- /dev/null +++ b/src/layouts/modules/global-logo/index.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/src/layouts/modules/global-menu/components/first-level-menu.vue b/src/layouts/modules/global-menu/components/first-level-menu.vue new file mode 100644 index 0000000..2e38f4c --- /dev/null +++ b/src/layouts/modules/global-menu/components/first-level-menu.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/src/layouts/modules/global-menu/components/menu-item.vue b/src/layouts/modules/global-menu/components/menu-item.vue new file mode 100644 index 0000000..113c112 --- /dev/null +++ b/src/layouts/modules/global-menu/components/menu-item.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/src/layouts/modules/global-menu/index.vue b/src/layouts/modules/global-menu/index.vue new file mode 100644 index 0000000..45434d9 --- /dev/null +++ b/src/layouts/modules/global-menu/index.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/src/layouts/modules/global-menu/modules/horizontal-menu.vue b/src/layouts/modules/global-menu/modules/horizontal-menu.vue new file mode 100644 index 0000000..1cad844 --- /dev/null +++ b/src/layouts/modules/global-menu/modules/horizontal-menu.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/src/layouts/modules/global-menu/modules/horizontal-mix-menu.vue b/src/layouts/modules/global-menu/modules/horizontal-mix-menu.vue new file mode 100644 index 0000000..7c78f2a --- /dev/null +++ b/src/layouts/modules/global-menu/modules/horizontal-mix-menu.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/src/layouts/modules/global-menu/modules/reversed-horizontal-mix-menu.vue b/src/layouts/modules/global-menu/modules/reversed-horizontal-mix-menu.vue new file mode 100644 index 0000000..d394937 --- /dev/null +++ b/src/layouts/modules/global-menu/modules/reversed-horizontal-mix-menu.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/src/layouts/modules/global-menu/modules/vertical-menu.vue b/src/layouts/modules/global-menu/modules/vertical-menu.vue new file mode 100644 index 0000000..dfc8630 --- /dev/null +++ b/src/layouts/modules/global-menu/modules/vertical-menu.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/src/layouts/modules/global-menu/modules/vertical-mix-menu.vue b/src/layouts/modules/global-menu/modules/vertical-mix-menu.vue new file mode 100644 index 0000000..401e8c9 --- /dev/null +++ b/src/layouts/modules/global-menu/modules/vertical-mix-menu.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/src/layouts/modules/global-search/components/search-footer.vue b/src/layouts/modules/global-search/components/search-footer.vue new file mode 100644 index 0000000..0fa6c8f --- /dev/null +++ b/src/layouts/modules/global-search/components/search-footer.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/src/layouts/modules/global-search/components/search-modal.vue b/src/layouts/modules/global-search/components/search-modal.vue new file mode 100644 index 0000000..2989f26 --- /dev/null +++ b/src/layouts/modules/global-search/components/search-modal.vue @@ -0,0 +1,150 @@ + + + + + diff --git a/src/layouts/modules/global-search/components/search-result.vue b/src/layouts/modules/global-search/components/search-result.vue new file mode 100644 index 0000000..4f6567e --- /dev/null +++ b/src/layouts/modules/global-search/components/search-result.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/src/layouts/modules/global-search/index.vue b/src/layouts/modules/global-search/index.vue new file mode 100644 index 0000000..95c2a7b --- /dev/null +++ b/src/layouts/modules/global-search/index.vue @@ -0,0 +1,18 @@ + + + + + diff --git a/src/layouts/modules/global-sider/index.vue b/src/layouts/modules/global-sider/index.vue new file mode 100644 index 0000000..65f03c7 --- /dev/null +++ b/src/layouts/modules/global-sider/index.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/src/layouts/modules/global-tab/context-menu.vue b/src/layouts/modules/global-tab/context-menu.vue new file mode 100644 index 0000000..03e1fb4 --- /dev/null +++ b/src/layouts/modules/global-tab/context-menu.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/src/layouts/modules/global-tab/index.vue b/src/layouts/modules/global-tab/index.vue new file mode 100644 index 0000000..b4ebb0c --- /dev/null +++ b/src/layouts/modules/global-tab/index.vue @@ -0,0 +1,208 @@ + + + + + diff --git a/src/layouts/modules/theme-drawer/components/layout-mode-card.vue b/src/layouts/modules/theme-drawer/components/layout-mode-card.vue new file mode 100644 index 0000000..4cc1695 --- /dev/null +++ b/src/layouts/modules/theme-drawer/components/layout-mode-card.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/src/layouts/modules/theme-drawer/components/setting-item.vue b/src/layouts/modules/theme-drawer/components/setting-item.vue new file mode 100644 index 0000000..a6ddaf4 --- /dev/null +++ b/src/layouts/modules/theme-drawer/components/setting-item.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/src/layouts/modules/theme-drawer/index.vue b/src/layouts/modules/theme-drawer/index.vue new file mode 100644 index 0000000..c66fe29 --- /dev/null +++ b/src/layouts/modules/theme-drawer/index.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/src/layouts/modules/theme-drawer/modules/config-operation.vue b/src/layouts/modules/theme-drawer/modules/config-operation.vue new file mode 100644 index 0000000..58b6f9d --- /dev/null +++ b/src/layouts/modules/theme-drawer/modules/config-operation.vue @@ -0,0 +1,56 @@ + + +