mirror of
https://github.com/m-xlsea/ruoyi-plus-soybean.git
synced 2025-09-23 23:39:47 +08:00
feat(projects): 1.0 beta
This commit is contained in:
17
packages/color-palette/package.json
Normal file
17
packages/color-palette/package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@sa/color-palette",
|
||||
"version": "1.0.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"colord": "2.9.3"
|
||||
}
|
||||
}
|
29
packages/color-palette/src/color.ts
Normal file
29
packages/color-palette/src/color.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { colord, extend } from 'colord';
|
||||
import type { HslColor } from 'colord';
|
||||
import labPlugin from 'colord/plugins/lab';
|
||||
|
||||
extend([labPlugin]);
|
||||
|
||||
export function isValidColor(color: string) {
|
||||
return colord(color).isValid();
|
||||
}
|
||||
|
||||
export function getHex(color: string) {
|
||||
return colord(color).toHex();
|
||||
}
|
||||
|
||||
export function getRgb(color: string) {
|
||||
return colord(color).toRgb();
|
||||
}
|
||||
|
||||
export function getHsl(color: string) {
|
||||
return colord(color).toHsl();
|
||||
}
|
||||
|
||||
export function getDeltaE(color1: string, color2: string) {
|
||||
return colord(color1).delta(color2);
|
||||
}
|
||||
|
||||
export function transformHslToHex(color: HslColor) {
|
||||
return colord(color).toHex();
|
||||
}
|
56
packages/color-palette/src/index.ts
Normal file
56
packages/color-palette/src/index.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { getColorPaletteFamily } from './palette';
|
||||
import { getColorName } from './name';
|
||||
import type { ColorPalette, ColorPaletteNumber, ColorPaletteItem, ColorPaletteFamily } from './type';
|
||||
import defaultPalettes from './json/palette.json';
|
||||
|
||||
/**
|
||||
* get color palette by provided color and color name
|
||||
* @param color the provided color
|
||||
* @param colorName color name
|
||||
*/
|
||||
export function getColorPalette(color: string, colorName: string) {
|
||||
const colorPaletteFamily = getColorPaletteFamily(color, colorName);
|
||||
|
||||
const colorMap = new Map<ColorPaletteNumber, ColorPaletteItem>();
|
||||
|
||||
colorPaletteFamily.palettes.forEach(palette => {
|
||||
colorMap.set(palette.number, palette);
|
||||
});
|
||||
|
||||
const mainColor = colorMap.get(500) as ColorPaletteItem;
|
||||
const matchColor = colorPaletteFamily.palettes.find(palette => palette.hexcode === color) as ColorPaletteItem;
|
||||
|
||||
const colorPalette: ColorPalette = {
|
||||
...colorPaletteFamily,
|
||||
colorMap,
|
||||
main: mainColor,
|
||||
match: matchColor
|
||||
};
|
||||
|
||||
return colorPalette;
|
||||
}
|
||||
|
||||
/**
|
||||
* get color by color palette number
|
||||
* @param color color
|
||||
* @param num color palette number
|
||||
* @return color hexcode
|
||||
*/
|
||||
export function getColorByColorPaletteNumber(color: string, num: ColorPaletteNumber) {
|
||||
const colorPalette = getColorPalette(color, color);
|
||||
|
||||
const colorItem = colorPalette.colorMap.get(num) as ColorPaletteItem;
|
||||
|
||||
return colorItem.hexcode;
|
||||
}
|
||||
|
||||
export default getColorPalette;
|
||||
|
||||
/**
|
||||
* the builtin color palettes
|
||||
*/
|
||||
const colorPalettes = defaultPalettes as ColorPaletteFamily[];
|
||||
|
||||
export { getColorName, colorPalettes };
|
||||
|
||||
export type { ColorPalette, ColorPaletteNumber, ColorPaletteItem, ColorPaletteFamily };
|
1568
packages/color-palette/src/json/color-name.json
Normal file
1568
packages/color-palette/src/json/color-name.json
Normal file
File diff suppressed because it is too large
Load Diff
274
packages/color-palette/src/json/palette.json
Normal file
274
packages/color-palette/src/json/palette.json
Normal file
@ -0,0 +1,274 @@
|
||||
[
|
||||
{
|
||||
"key": "red",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fef2f2", "number": 50, "name": "Bridesmaid" },
|
||||
{ "hexcode": "#fee2e2", "number": 100, "name": "Pippin" },
|
||||
{ "hexcode": "#fecaca", "number": 200, "name": "Your Pink" },
|
||||
{ "hexcode": "#fca5a5", "number": 300, "name": "Cornflower Lilac" },
|
||||
{ "hexcode": "#f87171", "number": 400, "name": "Bittersweet" },
|
||||
{ "hexcode": "#ef4444", "number": 500, "name": "Cinnabar" },
|
||||
{ "hexcode": "#dc2626", "number": 600, "name": "Persian Red" },
|
||||
{ "hexcode": "#b91c1c", "number": 700, "name": "Thunderbird" },
|
||||
{ "hexcode": "#991b1b", "number": 800, "name": "Old Brick" },
|
||||
{ "hexcode": "#7f1d1d", "number": 900, "name": "Falu Red" },
|
||||
{ "hexcode": "#450a0a", "number": 950, "name": "Mahogany" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "orange",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fff7ed", "number": 50, "name": "Serenade" },
|
||||
{ "hexcode": "#ffedd5", "number": 100, "name": "Derby" },
|
||||
{ "hexcode": "#fed7aa", "number": 200, "name": "Caramel" },
|
||||
{ "hexcode": "#fdba74", "number": 300, "name": "Macaroni and Cheese" },
|
||||
{ "hexcode": "#fb923c", "number": 400, "name": "Neon Carrot" },
|
||||
{ "hexcode": "#f97316", "number": 500, "name": "Ecstasy" },
|
||||
{ "hexcode": "#ea580c", "number": 600, "name": "Trinidad" },
|
||||
{ "hexcode": "#c2410c", "number": 700, "name": "Tia Maria" },
|
||||
{ "hexcode": "#9a3412", "number": 800, "name": "Tabasco" },
|
||||
{ "hexcode": "#7c2d12", "number": 900, "name": "Pueblo" },
|
||||
{ "hexcode": "#431407", "number": 950, "name": "Rebel" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "amber",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fffbeb", "number": 50, "name": "Island Spice" },
|
||||
{ "hexcode": "#fef3c7", "number": 100, "name": "Beeswax" },
|
||||
{ "hexcode": "#fde68a", "number": 200, "name": "Sweet Corn" },
|
||||
{ "hexcode": "#fcd34d", "number": 300, "name": "Mustard" },
|
||||
{ "hexcode": "#fbbf24", "number": 400, "name": "Lightning Yellow" },
|
||||
{ "hexcode": "#f59e0b", "number": 500, "name": "California" },
|
||||
{ "hexcode": "#d97706", "number": 600, "name": "Christine" },
|
||||
{ "hexcode": "#b45309", "number": 700, "name": "Vesuvius" },
|
||||
{ "hexcode": "#92400e", "number": 800, "name": "Korma" },
|
||||
{ "hexcode": "#78350f", "number": 900, "name": "Copper Canyon" },
|
||||
{ "hexcode": "#451a03", "number": 950, "name": "Brown Pod" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "yellow",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fefce8", "number": 50, "name": "Orange White" },
|
||||
{ "hexcode": "#fef9c3", "number": 100, "name": "Lemon Chiffon" },
|
||||
{ "hexcode": "#fef08a", "number": 200, "name": "Sweet Corn" },
|
||||
{ "hexcode": "#fde047", "number": 300, "name": "Bright Sun" },
|
||||
{ "hexcode": "#facc15", "number": 400, "name": "Candlelight" },
|
||||
{ "hexcode": "#eab308", "number": 500, "name": "Corn" },
|
||||
{ "hexcode": "#ca8a04", "number": 600, "name": "Pirate Gold" },
|
||||
{ "hexcode": "#a16207", "number": 700, "name": "Mai Tai" },
|
||||
{ "hexcode": "#854d0e", "number": 800, "name": "Korma" },
|
||||
{ "hexcode": "#713f12", "number": 900, "name": "Sepia" },
|
||||
{ "hexcode": "#422006", "number": 950, "name": "Dark Ebony" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "lime",
|
||||
"palettes": [
|
||||
{ "hexcode": "#f7fee7", "number": 50, "name": "Spring Sun" },
|
||||
{ "hexcode": "#ecfccb", "number": 100, "name": "Chiffon" },
|
||||
{ "hexcode": "#d9f99d", "number": 200, "name": "Gossip" },
|
||||
{ "hexcode": "#bef264", "number": 300, "name": "Sulu" },
|
||||
{ "hexcode": "#a3e635", "number": 400, "name": "Conifer" },
|
||||
{ "hexcode": "#84cc16", "number": 500, "name": "Lima" },
|
||||
{ "hexcode": "#65a30d", "number": 600, "name": "Christi" },
|
||||
{ "hexcode": "#4d7c0f", "number": 700, "name": "Green Leaf" },
|
||||
{ "hexcode": "#3f6212", "number": 800, "name": "Dell" },
|
||||
{ "hexcode": "#365314", "number": 900, "name": "Clover" },
|
||||
{ "hexcode": "#1a2e05", "number": 950, "name": "Deep Forest Green" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "green",
|
||||
"palettes": [
|
||||
{ "hexcode": "#f0fdf4", "number": 50, "name": "Ottoman" },
|
||||
{ "hexcode": "#dcfce7", "number": 100, "name": "Blue Romance" },
|
||||
{ "hexcode": "#bbf7d0", "number": 200, "name": "Magic Mint" },
|
||||
{ "hexcode": "#86efac", "number": 300, "name": "Algae Green" },
|
||||
{ "hexcode": "#4ade80", "number": 400, "name": "Emerald" },
|
||||
{ "hexcode": "#22c55e", "number": 500, "name": "Malachite" },
|
||||
{ "hexcode": "#16a34a", "number": 600, "name": "Salem" },
|
||||
{ "hexcode": "#15803d", "number": 700, "name": "Jewel" },
|
||||
{ "hexcode": "#166534", "number": 800, "name": "Jewel" },
|
||||
{ "hexcode": "#14532d", "number": 900, "name": "Green Pea" },
|
||||
{ "hexcode": "#052e16", "number": 950, "name": "English Holly" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "emerald",
|
||||
"palettes": [
|
||||
{ "hexcode": "#ecfdf5", "number": 50, "name": "White Ice" },
|
||||
{ "hexcode": "#d1fae5", "number": 100, "name": "Granny Apple" },
|
||||
{ "hexcode": "#a7f3d0", "number": 200, "name": "Magic Mint" },
|
||||
{ "hexcode": "#6ee7b7", "number": 300, "name": "Bermuda" },
|
||||
{ "hexcode": "#34d399", "number": 400, "name": "Shamrock" },
|
||||
{ "hexcode": "#10b981", "number": 500, "name": "Mountain Meadow" },
|
||||
{ "hexcode": "#059669", "number": 600, "name": "Green Haze" },
|
||||
{ "hexcode": "#047857", "number": 700, "name": "Watercourse" },
|
||||
{ "hexcode": "#065f46", "number": 800, "name": "Watercourse" },
|
||||
{ "hexcode": "#064e3b", "number": 900, "name": "Evening Sea" },
|
||||
{ "hexcode": "#022c22", "number": 950, "name": "Burnham" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "teal",
|
||||
"palettes": [
|
||||
{ "hexcode": "#f0fdfa", "number": 50, "name": "White Ice" },
|
||||
{ "hexcode": "#ccfbf1", "number": 100, "name": "Scandal" },
|
||||
{ "hexcode": "#99f6e4", "number": 200, "name": "Ice Cold" },
|
||||
{ "hexcode": "#5eead4", "number": 300, "name": "Turquoise Blue" },
|
||||
{ "hexcode": "#2dd4bf", "number": 400, "name": "Turquoise" },
|
||||
{ "hexcode": "#14b8a6", "number": 500, "name": "Java" },
|
||||
{ "hexcode": "#0d9488", "number": 600, "name": "Blue Chill" },
|
||||
{ "hexcode": "#0f766e", "number": 700, "name": "Genoa" },
|
||||
{ "hexcode": "#115e59", "number": 800, "name": "Eden" },
|
||||
{ "hexcode": "#134e4a", "number": 900, "name": "Eden" },
|
||||
{ "hexcode": "#042f2e", "number": 950, "name": "Tiber" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "cyan",
|
||||
"palettes": [
|
||||
{ "hexcode": "#ecfeff", "number": 50, "name": "Bubbles" },
|
||||
{ "hexcode": "#cffafe", "number": 100, "name": "Oyster Bay" },
|
||||
{ "hexcode": "#a5f3fc", "number": 200, "name": "Anakiwa" },
|
||||
{ "hexcode": "#67e8f9", "number": 300, "name": "Spray" },
|
||||
{ "hexcode": "#22d3ee", "number": 400, "name": "Bright Turquoise" },
|
||||
{ "hexcode": "#06b6d4", "number": 500, "name": "Cerulean" },
|
||||
{ "hexcode": "#0891b2", "number": 600, "name": "Bondi Blue" },
|
||||
{ "hexcode": "#0e7490", "number": 700, "name": "Blue Chill" },
|
||||
{ "hexcode": "#155e75", "number": 800, "name": "Blumine" },
|
||||
{ "hexcode": "#164e63", "number": 900, "name": "Chathams Blue" },
|
||||
{ "hexcode": "#083344", "number": 950, "name": "Tarawera" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "sky",
|
||||
"palettes": [
|
||||
{ "hexcode": "#f0f9ff", "number": 50, "name": "Alice Blue" },
|
||||
{ "hexcode": "#e0f2fe", "number": 100, "name": "Pattens Blue" },
|
||||
{ "hexcode": "#bae6fd", "number": 200, "name": "French Pass" },
|
||||
{ "hexcode": "#7dd3fc", "number": 300, "name": "Malibu" },
|
||||
{ "hexcode": "#38bdf8", "number": 400, "name": "Picton Blue" },
|
||||
{ "hexcode": "#0ea5e9", "number": 500, "name": "Cerulean" },
|
||||
{ "hexcode": "#0284c7", "number": 600, "name": "Lochmara" },
|
||||
{ "hexcode": "#0369a1", "number": 700, "name": "Bahama Blue" },
|
||||
{ "hexcode": "#075985", "number": 800, "name": "Venice Blue" },
|
||||
{ "hexcode": "#0c4a6e", "number": 900, "name": "Chathams Blue" },
|
||||
{ "hexcode": "#082f49", "number": 950, "name": "Blue Whale" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "blue",
|
||||
"palettes": [
|
||||
{ "hexcode": "#eff6ff", "number": 50, "name": "Zumthor" },
|
||||
{ "hexcode": "#dbeafe", "number": 100, "name": "Hawkes Blue" },
|
||||
{ "hexcode": "#bfdbfe", "number": 200, "name": "Tropical Blue" },
|
||||
{ "hexcode": "#93c5fd", "number": 300, "name": "Malibu" },
|
||||
{ "hexcode": "#60a5fa", "number": 400, "name": "Cornflower Blue" },
|
||||
{ "hexcode": "#3b82f6", "number": 500, "name": "Dodger Blue" },
|
||||
{ "hexcode": "#2563eb", "number": 600, "name": "Royal Blue" },
|
||||
{ "hexcode": "#1d4ed8", "number": 700, "name": "Cerulean Blue" },
|
||||
{ "hexcode": "#1e40af", "number": 800, "name": "Persian Blue" },
|
||||
{ "hexcode": "#1e3a8a", "number": 900, "name": "Bay of Many" },
|
||||
{ "hexcode": "#172554", "number": 950, "name": "Bunting" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "indigo",
|
||||
"palettes": [
|
||||
{ "hexcode": "#eef2ff", "number": 50, "name": "Zircon" },
|
||||
{ "hexcode": "#e0e7ff", "number": 100, "name": "Hawkes Blue" },
|
||||
{ "hexcode": "#c7d2fe", "number": 200, "name": "Periwinkle" },
|
||||
{ "hexcode": "#a5b4fc", "number": 300, "name": "Perano" },
|
||||
{ "hexcode": "#818cf8", "number": 400, "name": "Portage" },
|
||||
{ "hexcode": "#6366f1", "number": 500, "name": "Royal Blue" },
|
||||
{ "hexcode": "#4f46e5", "number": 600, "name": "Royal Blue" },
|
||||
{ "hexcode": "#4338ca", "number": 700, "name": "Governor Bay" },
|
||||
{ "hexcode": "#3730a3", "number": 800, "name": "Governor Bay" },
|
||||
{ "hexcode": "#312e81", "number": 900, "name": "Minsk" },
|
||||
{ "hexcode": "#1e1b4b", "number": 950, "name": "Port Gore" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "violet",
|
||||
"palettes": [
|
||||
{ "hexcode": "#f5f3ff", "number": 50, "name": "Titan White" },
|
||||
{ "hexcode": "#ede9fe", "number": 100, "name": "Titan White" },
|
||||
{ "hexcode": "#ddd6fe", "number": 200, "name": "Fog" },
|
||||
{ "hexcode": "#c4b5fd", "number": 300, "name": "Melrose" },
|
||||
{ "hexcode": "#a78bfa", "number": 400, "name": "Dull Lavender" },
|
||||
{ "hexcode": "#8b5cf6", "number": 500, "name": "Medium Purple" },
|
||||
{ "hexcode": "#7c3aed", "number": 600, "name": "Purple Heart" },
|
||||
{ "hexcode": "#6d28d9", "number": 700, "name": "Purple Heart" },
|
||||
{ "hexcode": "#5b21b6", "number": 800, "name": "Purple Heart" },
|
||||
{ "hexcode": "#4c1d95", "number": 900, "name": "Daisy Bush" },
|
||||
{ "hexcode": "#2e1065", "number": 950, "name": "Violent Violet" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "purple",
|
||||
"palettes": [
|
||||
{ "hexcode": "#faf5ff", "number": 50, "name": "Magnolia" },
|
||||
{ "hexcode": "#f3e8ff", "number": 100, "name": "Blue Chalk" },
|
||||
{ "hexcode": "#e9d5ff", "number": 200, "name": "Blue Chalk" },
|
||||
{ "hexcode": "#d8b4fe", "number": 300, "name": "Mauve" },
|
||||
{ "hexcode": "#c084fc", "number": 400, "name": "Heliotrope" },
|
||||
{ "hexcode": "#a855f7", "number": 500, "name": "Medium Purple" },
|
||||
{ "hexcode": "#9333ea", "number": 600, "name": "Electric Violet" },
|
||||
{ "hexcode": "#7e22ce", "number": 700, "name": "Purple Heart" },
|
||||
{ "hexcode": "#6b21a8", "number": 800, "name": "Seance" },
|
||||
{ "hexcode": "#581c87", "number": 900, "name": "Daisy Bush" },
|
||||
{ "hexcode": "#3b0764", "number": 950, "name": "Christalle" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "fuchsia",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fdf4ff", "number": 50, "name": "White Pointer" },
|
||||
{ "hexcode": "#fae8ff", "number": 100, "name": "White Pointer" },
|
||||
{ "hexcode": "#f5d0fe", "number": 200, "name": "Mauve" },
|
||||
{ "hexcode": "#f0abfc", "number": 300, "name": "Mauve" },
|
||||
{ "hexcode": "#e879f9", "number": 400, "name": "Heliotrope" },
|
||||
{ "hexcode": "#d946ef", "number": 500, "name": "Heliotrope" },
|
||||
{ "hexcode": "#c026d3", "number": 600, "name": "Fuchsia Pink" },
|
||||
{ "hexcode": "#a21caf", "number": 700, "name": "Violet Eggplant" },
|
||||
{ "hexcode": "#86198f", "number": 800, "name": "Seance" },
|
||||
{ "hexcode": "#701a75", "number": 900, "name": "Seance" },
|
||||
{ "hexcode": "#4a044e", "number": 950, "name": "Clairvoyant" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pink",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fdf2f8", "number": 50, "name": "Wisp Pink" },
|
||||
{ "hexcode": "#fce7f3", "number": 100, "name": "Carousel Pink" },
|
||||
{ "hexcode": "#fbcfe8", "number": 200, "name": "Classic Rose" },
|
||||
{ "hexcode": "#f9a8d4", "number": 300, "name": "Lavender Pink" },
|
||||
{ "hexcode": "#f472b6", "number": 400, "name": "Persian Pink" },
|
||||
{ "hexcode": "#ec4899", "number": 500, "name": "Brilliant Rose" },
|
||||
{ "hexcode": "#db2777", "number": 600, "name": "Cerise" },
|
||||
{ "hexcode": "#be185d", "number": 700, "name": "Maroon Flush" },
|
||||
{ "hexcode": "#9d174d", "number": 800, "name": "Disco" },
|
||||
{ "hexcode": "#831843", "number": 900, "name": "Disco" },
|
||||
{ "hexcode": "#500724", "number": 950, "name": "Cab Sav" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "rose",
|
||||
"palettes": [
|
||||
{ "hexcode": "#fff1f2", "number": 50, "name": "Lavender blush" },
|
||||
{ "hexcode": "#ffe4e6", "number": 100, "name": "Cosmos" },
|
||||
{ "hexcode": "#fecdd3", "number": 200, "name": "Pastel Pink" },
|
||||
{ "hexcode": "#fda4af", "number": 300, "name": "Sweet Pink" },
|
||||
{ "hexcode": "#fb7185", "number": 400, "name": "Froly" },
|
||||
{ "hexcode": "#f43f5e", "number": 500, "name": "Radical Red" },
|
||||
{ "hexcode": "#e11d48", "number": 600, "name": "Amaranth" },
|
||||
{ "hexcode": "#be123c", "number": 700, "name": "Cardinal" },
|
||||
{ "hexcode": "#9f1239", "number": 800, "name": "Shiraz" },
|
||||
{ "hexcode": "#881337", "number": 900, "name": "Claret" },
|
||||
{ "hexcode": "#4c0519", "number": 950, "name": "Cab Sav" }
|
||||
]
|
||||
}
|
||||
]
|
46
packages/color-palette/src/name.ts
Normal file
46
packages/color-palette/src/name.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { getHex, getRgb, getHsl } from './color';
|
||||
import colorNames from './json/color-name.json';
|
||||
|
||||
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 hexcode = `#${hexValue}`;
|
||||
|
||||
const match = hex === hexcode;
|
||||
|
||||
if (match) {
|
||||
name = colorName;
|
||||
} else {
|
||||
const { r, g, b } = getRgb(hexcode);
|
||||
const { h, s, l } = getHsl(hexcode);
|
||||
|
||||
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 = cl < 0 ? 'Invalid Color' : colorNames[cl][1];
|
||||
|
||||
return name;
|
||||
}
|
95
packages/color-palette/src/palette.ts
Normal file
95
packages/color-palette/src/palette.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { isValidColor, getHsl, getDeltaE, transformHslToHex } from './color';
|
||||
import { getColorName } from './name';
|
||||
import type { ColorPaletteFamily, ColorPaletteFamilyWithNearestPalette } from './type';
|
||||
import defaultPalettes from './json/palette.json';
|
||||
|
||||
export function getNearestColorPaletteFamily(color: string, families: ColorPaletteFamily[]) {
|
||||
const familyWithConfig = families.map(family => {
|
||||
const palettes = family.palettes.map(palette => {
|
||||
return {
|
||||
...palette,
|
||||
delta: getDeltaE(color, palette.hexcode)
|
||||
};
|
||||
});
|
||||
|
||||
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.hexcode);
|
||||
const { l: currLightness } = getHsl(curr.hexcode);
|
||||
|
||||
const deltaPrev = Math.abs(prevLightness - l);
|
||||
const deltaCurr = Math.abs(currLightness - l);
|
||||
|
||||
return deltaPrev < deltaCurr ? prev : curr;
|
||||
})
|
||||
};
|
||||
|
||||
return paletteFamily;
|
||||
}
|
||||
|
||||
export function getColorPaletteFamily(color: string, colorName: string) {
|
||||
if (!isValidColor(color)) {
|
||||
throw new Error('Invalid color, please check color value!');
|
||||
}
|
||||
|
||||
const { h: h1, s: s1 } = getHsl(color);
|
||||
|
||||
const { nearestLightnessPalette, palettes } = getNearestColorPaletteFamily(
|
||||
color,
|
||||
defaultPalettes as ColorPaletteFamily[]
|
||||
);
|
||||
|
||||
const { number, hexcode } = nearestLightnessPalette;
|
||||
|
||||
const { h: h2, s: s2 } = getHsl(hexcode);
|
||||
|
||||
const deltaH = h1 - h2 || h2;
|
||||
|
||||
const sRatio = s1 / s2;
|
||||
|
||||
const colorPaletteFamily: ColorPaletteFamily = {
|
||||
key: colorName,
|
||||
palettes: palettes.map(palette => {
|
||||
let hexValue = color;
|
||||
|
||||
const isSame = number === palette.number;
|
||||
|
||||
if (!isSame) {
|
||||
const { h: h3, s: s3, l } = getHsl(palette.hexcode);
|
||||
|
||||
const newH = deltaH < 0 ? h3 + deltaH : deltaH;
|
||||
const newS = s3 * sRatio;
|
||||
|
||||
hexValue = transformHslToHex({
|
||||
h: newH,
|
||||
s: newS,
|
||||
l
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
hexcode: hexValue,
|
||||
number: palette.number,
|
||||
name: getColorName(hexValue)
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
return colorPaletteFamily;
|
||||
}
|
63
packages/color-palette/src/type.ts
Normal file
63
packages/color-palette/src/type.ts
Normal file
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* the color palette number
|
||||
*/
|
||||
export type ColorPaletteNumber = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950;
|
||||
|
||||
/**
|
||||
* the color palette item
|
||||
*/
|
||||
export type ColorPaletteItem = {
|
||||
/**
|
||||
* the color hexcode
|
||||
*/
|
||||
hexcode: string;
|
||||
/**
|
||||
* the color number
|
||||
* @link {@link ColorPaletteNumber}
|
||||
*/
|
||||
number: ColorPaletteNumber;
|
||||
/**
|
||||
* the color name
|
||||
*/
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type ColorPaletteFamily = {
|
||||
/**
|
||||
* the color palette family key
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* the color palette family's palettes
|
||||
*/
|
||||
palettes: ColorPaletteItem[];
|
||||
};
|
||||
|
||||
export type ColorPaletteWithDelta = ColorPaletteItem & {
|
||||
delta: number;
|
||||
};
|
||||
|
||||
export type ColorPaletteItemWithName = ColorPaletteItem & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type ColorPaletteFamilyWithNearestPalette = ColorPaletteFamily & {
|
||||
nearestPalette: ColorPaletteWithDelta;
|
||||
nearestLightnessPalette: ColorPaletteWithDelta;
|
||||
};
|
||||
|
||||
export type ColorPalette = ColorPaletteFamily & {
|
||||
/**
|
||||
* the color map of the palette
|
||||
*/
|
||||
colorMap: Map<ColorPaletteNumber, ColorPaletteItem>;
|
||||
/**
|
||||
* the main color of the palette
|
||||
* @description which number is 500
|
||||
*/
|
||||
main: ColorPaletteItemWithName;
|
||||
/**
|
||||
* the match color of the palette
|
||||
*/
|
||||
match: ColorPaletteItemWithName;
|
||||
};
|
38
packages/docs/.vitepress/config.ts
Normal file
38
packages/docs/.vitepress/config.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import path from 'node:path';
|
||||
import { defineConfig } from 'vitepress';
|
||||
|
||||
export default defineConfig({
|
||||
title: 'Soybean Admin',
|
||||
description: '一个优雅、清新、漂亮的中后台模版',
|
||||
head: [
|
||||
['meta', { name: 'author', content: 'Soybean' }],
|
||||
[
|
||||
'meta',
|
||||
{
|
||||
name: 'keywords',
|
||||
content: 'soybean, soybean-admin, vite, vue, vue3, soybean-admin docs'
|
||||
}
|
||||
],
|
||||
['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }],
|
||||
[
|
||||
'meta',
|
||||
{
|
||||
name: 'viewport',
|
||||
content: 'width=device-width,initial-scale=1,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no'
|
||||
}
|
||||
],
|
||||
['link', { rel: 'icon', href: '/favicon.ico' }]
|
||||
],
|
||||
srcDir: path.join(process.cwd(), 'packages/docs/src'),
|
||||
themeConfig: {
|
||||
logo: '/logo.svg',
|
||||
socialLinks: [{ icon: 'github', link: 'https://github.com/honghuangdc/soybean-admin' }],
|
||||
algolia: {
|
||||
appId: '98WN1RY04S',
|
||||
apiKey: '13e9f5767b774422a5880723d9c23265',
|
||||
indexName: 'soybean'
|
||||
},
|
||||
nav: [],
|
||||
sidebar: {}
|
||||
}
|
||||
});
|
32
packages/docs/.vitepress/icon.ts
Normal file
32
packages/docs/.vitepress/icon.ts
Normal file
@ -0,0 +1,32 @@
|
||||
export const qqSvg = `
|
||||
<svg height="2500" viewBox="-1.94 0 124.879 145.085" width="2101" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="m60.503 142.237c-12.533 0-24.038-4.195-31.445-10.46-3.762 1.124-8.574 2.932-11.61 5.175-2.6 1.918-2.275 3.874-1.807 4.663 2.056 3.47 35.273 2.216 44.862 1.136zm0 0c12.535 0 24.039-4.195 31.447-10.46 3.76 1.124 8.573 2.932 11.61 5.175 2.598 1.918 2.274 3.874 1.805 4.663-2.056 3.47-35.272 2.216-44.862 1.136zm0 0"
|
||||
fill="#faab07"
|
||||
/>
|
||||
<path
|
||||
d="m60.576 67.119c20.698-.14 37.286-4.147 42.907-5.683 1.34-.367 2.056-1.024 2.056-1.024.005-.189.085-3.37.085-5.01 0-27.634-13.044-55.401-45.124-55.402-32.08.001-45.125 27.769-45.125 55.401 0 1.642.08 4.822.086 5.01 0 0 .583.615 1.65.913 5.19 1.444 22.09 5.65 43.312 5.795zm56.245 23.02c-1.283-4.129-3.034-8.944-4.808-13.568 0 0-1.02-.126-1.537.023-15.913 4.623-35.202 7.57-49.9 7.392h-.153c-14.616.175-33.774-2.737-49.634-7.315-.606-.175-1.802-.1-1.802-.1-1.774 4.624-3.525 9.44-4.808 13.568-6.119 19.69-4.136 27.838-2.627 28.02 3.239.392 12.606-14.821 12.606-14.821 0 15.459 13.957 39.195 45.918 39.413h.848c31.96-.218 45.917-23.954 45.917-39.413 0 0 9.368 15.213 12.607 14.822 1.508-.183 3.491-8.332-2.627-28.021"
|
||||
/>
|
||||
<path
|
||||
d="m49.085 40.824c-4.352.197-8.07-4.76-8.304-11.063-.236-6.305 3.098-11.576 7.45-11.773 4.347-.195 8.064 4.76 8.3 11.065.238 6.306-3.097 11.577-7.446 11.771m31.133-11.063c-.233 6.302-3.951 11.26-8.303 11.063-4.35-.195-7.684-5.465-7.446-11.77.236-6.305 3.952-11.26 8.3-11.066 4.352.197 7.686 5.468 7.449 11.773"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="m87.952 49.725c-1.162-2.575-12.875-5.445-27.374-5.445h-.156c-14.5 0-26.212 2.87-27.375 5.446a.863.863 0 0 0 -.085.367c0 .186.063.352.16.496.98 1.427 13.985 8.487 27.3 8.487h.156c13.314 0 26.319-7.058 27.299-8.487a.873.873 0 0 0 .16-.498.856.856 0 0 0 -.085-.365"
|
||||
fill="#faab07"
|
||||
/>
|
||||
<path
|
||||
d="m54.434 29.854c.199 2.49-1.167 4.702-3.046 4.943-1.883.242-3.568-1.58-3.768-4.07-.197-2.492 1.167-4.704 3.043-4.944 1.886-.244 3.574 1.58 3.771 4.07m11.956.833c.385-.689 3.004-4.312 8.427-2.993 1.425.347 2.084.857 2.223 1.057.205.296.262.718.053 1.286-.412 1.126-1.263 1.095-1.734.875-.305-.142-4.082-2.66-7.562 1.097-.24.257-.668.346-1.073.04-.407-.308-.574-.93-.334-1.362"
|
||||
/>
|
||||
<path
|
||||
d="m60.576 83.08h-.153c-9.996.12-22.116-1.204-33.854-3.518-1.004 5.818-1.61 13.132-1.09 21.853 1.316 22.043 14.407 35.9 34.614 36.1h.82c20.208-.2 33.298-14.057 34.616-36.1.52-8.723-.087-16.035-1.092-21.854-11.739 2.315-23.862 3.64-33.86 3.518"
|
||||
fill="#fff"
|
||||
/>
|
||||
<g fill="#eb1923">
|
||||
<path d="m32.102 81.235v21.693s9.937 2.004 19.893.616v-20.009c-6.307-.357-13.109-1.152-19.893-2.3" />
|
||||
<path
|
||||
d="m105.539 60.412s-19.33 6.102-44.963 6.275h-.153c-25.591-.172-44.896-6.255-44.962-6.275l-6.474 16.158c16.193 4.882 36.261 8.028 51.436 7.845h.153c15.175.183 35.242-2.963 51.437-7.845zm0 0"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
4
packages/docs/.vitepress/theme/index.ts
Normal file
4
packages/docs/.vitepress/theme/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import Theme from 'vitepress/theme';
|
||||
import './style.css';
|
||||
|
||||
export default Theme;
|
90
packages/docs/.vitepress/theme/style.css
Normal file
90
packages/docs/.vitepress/theme/style.css
Normal file
@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Customize default theme styling by overriding CSS variables:
|
||||
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
|
||||
*/
|
||||
|
||||
/**
|
||||
* Colors
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-c-brand: #646cff;
|
||||
--vp-c-brand-light: #747bff;
|
||||
--vp-c-brand-lighter: #9499ff;
|
||||
--vp-c-brand-lightest: #bcc0ff;
|
||||
--vp-c-brand-dark: #535bf2;
|
||||
--vp-c-brand-darker: #454ce1;
|
||||
--vp-c-brand-dimm: rgba(100, 108, 255, 0.08);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Button
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-button-brand-border: var(--vp-c-brand-light);
|
||||
--vp-button-brand-text: var(--vp-c-white);
|
||||
--vp-button-brand-bg: var(--vp-c-brand);
|
||||
--vp-button-brand-hover-border: var(--vp-c-brand-light);
|
||||
--vp-button-brand-hover-text: var(--vp-c-white);
|
||||
--vp-button-brand-hover-bg: var(--vp-c-brand-light);
|
||||
--vp-button-brand-active-border: var(--vp-c-brand-light);
|
||||
--vp-button-brand-active-text: var(--vp-c-white);
|
||||
--vp-button-brand-active-bg: var(--vp-button-brand-bg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Home
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: -webkit-linear-gradient(
|
||||
120deg,
|
||||
var(--vp-c-brand-lightest) 30%,
|
||||
var(--vp-c-brand-darker)
|
||||
);
|
||||
|
||||
--vp-home-hero-image-background-image: linear-gradient(
|
||||
-45deg,
|
||||
var(--vp-c-brand-lightest) 30%,
|
||||
var(--vp-c-brand) 50%
|
||||
);
|
||||
--vp-home-hero-image-filter: blur(40px);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(56px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(72px);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Custom Block
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
--vp-custom-block-tip-border: var(--vp-c-brand);
|
||||
--vp-custom-block-tip-text: var(--vp-c-brand-darker);
|
||||
--vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--vp-custom-block-tip-border: var(--vp-c-brand);
|
||||
--vp-custom-block-tip-text: var(--vp-c-brand-lightest);
|
||||
--vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Algolia
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
.DocSearch {
|
||||
--docsearch-primary-color: var(--vp-c-brand) !important;
|
||||
}
|
12
packages/docs/package.json
Normal file
12
packages/docs/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@sa/docs",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "vitepress dev",
|
||||
"build": "vitepress build",
|
||||
"serve": "vitepress serve"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitepress": "1.0.0-rc.25"
|
||||
}
|
||||
}
|
44
packages/docs/src/index.md
Normal file
44
packages/docs/src/index.md
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
layout: home
|
||||
|
||||
title: Soybean Admin
|
||||
titleTemplate: 一个清新优雅的中后台模版
|
||||
|
||||
hero:
|
||||
name: Soybean Admin
|
||||
text: 清新优雅的中后台模版
|
||||
tagline: 基于 Vue3 + Vite3 + TS + NaiveUI + UnoCSS
|
||||
image:
|
||||
src: /logo.svg
|
||||
alt: Soybean Admin
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 开始
|
||||
link: /guide/
|
||||
- theme: alt
|
||||
text: 介绍
|
||||
link: /guide/introduction
|
||||
- theme: alt
|
||||
text: 在 GitHub 上查看
|
||||
link: https://github.com/honghuangdc/soybean-admin
|
||||
|
||||
features:
|
||||
- icon: 🆕
|
||||
title: 最新流行技术栈
|
||||
details: 基于Vue3、Vite3、TS、NaiveUI和UnoCSS等最新技术栈开发
|
||||
- icon: 🦋
|
||||
title: 极高水准的代码规范
|
||||
details: 代码规范完善,代码结构清晰
|
||||
- icon: 🛠️
|
||||
title: 丰富的插件
|
||||
details: 常见的Web端插件示例实现
|
||||
- icon: 🔩
|
||||
title: 主题配置
|
||||
details: 丰富的主题配置及暗黑主题适配
|
||||
- icon: 🔗
|
||||
title: 基于文件的路由系统
|
||||
details: 自动生成路由声明、路由导入和路由模块
|
||||
- icon: 🔑
|
||||
title: 权限管理
|
||||
details: 完善的前后端权限管理方案
|
||||
---
|
6
packages/eslint-config/configs/base.js
Normal file
6
packages/eslint-config/configs/base.js
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* @type {import('eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: [require.resolve('./ts.js'), require.resolve('./prettier.js')]
|
||||
};
|
44
packages/eslint-config/configs/js.js
Normal file
44
packages/eslint-config/configs/js.js
Normal file
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @type {import('eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
commonjs: true,
|
||||
es2024: true
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2024,
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
},
|
||||
sourceType: 'module'
|
||||
},
|
||||
ignorePatterns: [
|
||||
'node_modules',
|
||||
'*.min.*',
|
||||
'CHANGELOG.md',
|
||||
'dist',
|
||||
'LICENSE*',
|
||||
'output',
|
||||
'coverage',
|
||||
'public',
|
||||
'temp',
|
||||
'package-lock.json',
|
||||
'pnpm-lock.yaml',
|
||||
'yarn.lock',
|
||||
'__snapshots__',
|
||||
'!.github',
|
||||
'!.vitepress',
|
||||
'!.vscode'
|
||||
],
|
||||
plugins: ['n', 'promise'],
|
||||
extends: [require.resolve('../rules/all.js'), 'plugin:import/recommended'],
|
||||
rules: {
|
||||
// import
|
||||
'import/no-mutable-exports': 'error',
|
||||
'import/no-named-as-default': 'off'
|
||||
}
|
||||
};
|
11
packages/eslint-config/configs/prettier.js
Normal file
11
packages/eslint-config/configs/prettier.js
Normal file
@ -0,0 +1,11 @@
|
||||
const prettierRules = require('../rules/prettier');
|
||||
|
||||
/**
|
||||
* @type {import('eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['plugin:prettier/recommended'],
|
||||
rules: {
|
||||
'prettier/prettier': ['error', prettierRules]
|
||||
}
|
||||
};
|
61
packages/eslint-config/configs/ts.js
Normal file
61
packages/eslint-config/configs/ts.js
Normal file
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @type {import('eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: [require.resolve('./js.js'), 'plugin:import/typescript', 'plugin:@typescript-eslint/recommended'],
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project: ['tsconfig.json', 'packages/*/tsconfig.json', 'examples/*/tsconfig.json', 'docs/*/tsconfig.json']
|
||||
}
|
||||
}
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.tsx', '*.mts', '*.cts'],
|
||||
parser: '@typescript-eslint/parser'
|
||||
},
|
||||
{
|
||||
files: ['*.js', '*.mjs', '*.cjs', '*.cts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-var-requires': 'off'
|
||||
}
|
||||
}
|
||||
],
|
||||
rules: {
|
||||
// TS
|
||||
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports', disallowTypeAnnotations: false }],
|
||||
'@typescript-eslint/no-empty-interface': [
|
||||
'error',
|
||||
{
|
||||
allowSingleExtends: true
|
||||
}
|
||||
],
|
||||
|
||||
// Override JS
|
||||
'no-redeclare': 'off',
|
||||
'@typescript-eslint/no-redeclare': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
vars: 'all',
|
||||
args: 'all',
|
||||
ignoreRestSiblings: false,
|
||||
varsIgnorePattern: '^_',
|
||||
argsIgnorePattern: '^_'
|
||||
}
|
||||
],
|
||||
'no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-use-before-define': ['error', { functions: false, classes: false, variables: true }],
|
||||
'no-shadow': 'off',
|
||||
'@typescript-eslint/no-shadow': 'error',
|
||||
|
||||
// off
|
||||
'@typescript-eslint/consistent-type-definitions': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off'
|
||||
}
|
||||
};
|
30
packages/eslint-config/configs/vue.js
Normal file
30
packages/eslint-config/configs/vue.js
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @type {import('eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = {
|
||||
extends: ['plugin:vue/vue3-recommended', require.resolve('./base.js')],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.vue'],
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: {
|
||||
js: 'espree',
|
||||
jsx: 'espree',
|
||||
ts: '@typescript-eslint/parser',
|
||||
tsx: '@typescript-eslint/parser'
|
||||
},
|
||||
extraFileExtensions: ['.vue'],
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'off' // TS will check un declared variables, if the script code is is in a .vue file, this rule should not disabled
|
||||
}
|
||||
}
|
||||
],
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off'
|
||||
}
|
||||
};
|
6
packages/eslint-config/index.js
Normal file
6
packages/eslint-config/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
const baseConfig = require('./configs/base');
|
||||
|
||||
/**
|
||||
* @type {import('eslint').ESLint.ConfigData}
|
||||
*/
|
||||
module.exports = baseConfig;
|
23
packages/eslint-config/package.json
Normal file
23
packages/eslint-config/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "eslint-config-sa",
|
||||
"version": "1.0.0",
|
||||
"description": "SoybeanAdmin's eslint config resets",
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./vue": "./configs/vue.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/eslint": "8.44.7",
|
||||
"@typescript-eslint/eslint-plugin": "6.11.0",
|
||||
"@typescript-eslint/parser": "6.11.0",
|
||||
"eslint": "8.53.0",
|
||||
"eslint-config-prettier": "9.0.0",
|
||||
"eslint-import-resolver-typescript": "3.6.1",
|
||||
"eslint-plugin-import": "2.29.0",
|
||||
"eslint-plugin-n": "16.3.1",
|
||||
"eslint-plugin-prettier": "5.0.1",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-vue": "9.18.1",
|
||||
"prettier": "3.1.0"
|
||||
}
|
||||
}
|
1681
packages/eslint-config/rules/all.js
Normal file
1681
packages/eslint-config/rules/all.js
Normal file
File diff suppressed because it is too large
Load Diff
26
packages/eslint-config/rules/prettier.js
Normal file
26
packages/eslint-config/rules/prettier.js
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @type {import('prettier').Options}
|
||||
*/
|
||||
module.exports = {
|
||||
printWidth: 120,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
quoteProps: 'as-needed',
|
||||
jsxSingleQuote: false,
|
||||
trailingComma: 'none',
|
||||
bracketSpacing: true,
|
||||
bracketSameLine: false,
|
||||
arrowParens: 'avoid',
|
||||
rangeStart: 0,
|
||||
rangeEnd: Number.POSITIVE_INFINITY,
|
||||
requirePragma: false,
|
||||
insertPragma: false,
|
||||
proseWrap: 'preserve',
|
||||
htmlWhitespaceSensitivity: 'ignore',
|
||||
vueIndentScriptAndStyle: false,
|
||||
endOfLine: 'lf',
|
||||
embeddedLanguageFormatting: 'auto',
|
||||
singleAttributePerLine: false
|
||||
};
|
14
packages/hooks/package.json
Normal file
14
packages/hooks/package.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@sa/hooks",
|
||||
"version": "1.0.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
6
packages/hooks/src/index.ts
Normal file
6
packages/hooks/src/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import useBoolean from './use-boolean';
|
||||
import useLoading from './use-loading';
|
||||
import useContext from './use-context';
|
||||
import useSvgIconRender from './use-svg-icon-render';
|
||||
|
||||
export { useBoolean, useLoading, useContext, useSvgIconRender };
|
30
packages/hooks/src/use-boolean.ts
Normal file
30
packages/hooks/src/use-boolean.ts
Normal file
@ -0,0 +1,30 @@
|
||||
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
|
||||
};
|
||||
}
|
103
packages/hooks/src/use-context.ts
Normal file
103
packages/hooks/src/use-context.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { inject, provide } from 'vue';
|
||||
import type { InjectionKey } from 'vue';
|
||||
|
||||
/**
|
||||
* use context
|
||||
* @param contextName context name
|
||||
* @param fn context function
|
||||
* @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
|
||||
* <template>
|
||||
* <div>A</div>
|
||||
* </template>
|
||||
* <script setup lang="ts">
|
||||
* import { setupStore } from './context';
|
||||
*
|
||||
* setupStore();
|
||||
* // const { increment } = setupStore(); // also can control the store in the parent component
|
||||
* </script>
|
||||
* ```
|
||||
* // B.vue
|
||||
* ```vue
|
||||
* <template>
|
||||
* <div>B</div>
|
||||
* </template>
|
||||
* <script setup lang="ts">
|
||||
* import { useStore } from './context';
|
||||
*
|
||||
* const { count, increment } = useStore();
|
||||
* </script>
|
||||
* ```
|
||||
*
|
||||
* // C.vue is same as B.vue
|
||||
*/
|
||||
export default function useContext<T extends (...args: any[]) => any>(contextName: string, fn: T) {
|
||||
type Context = ReturnType<T>;
|
||||
|
||||
const { useProvide, useInject: useStore } = createContext<Context>(contextName);
|
||||
|
||||
function setupStore(...args: Parameters<T>) {
|
||||
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<T>(contextName: string) {
|
||||
const injectKey: InjectionKey<T> = Symbol(contextName);
|
||||
|
||||
function useProvide(context: T) {
|
||||
provide(injectKey, context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function useInject() {
|
||||
return inject(injectKey) as T;
|
||||
}
|
||||
|
||||
return {
|
||||
useProvide,
|
||||
useInject
|
||||
};
|
||||
}
|
15
packages/hooks/src/use-loading.ts
Normal file
15
packages/hooks/src/use-loading.ts
Normal file
@ -0,0 +1,15 @@
|
||||
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
|
||||
};
|
||||
}
|
56
packages/hooks/src/use-svg-icon-render.ts
Normal file
56
packages/hooks/src/use-svg-icon-render.ts
Normal file
@ -0,0 +1,56 @@
|
||||
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<Pick<CSSStyleDeclaration, 'color' | 'fontSize'>>;
|
||||
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
20
packages/hooks/tsconfig.json
Normal file
20
packages/hooks/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"noUnusedLocals": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
6
packages/materials/.eslintrc
Normal file
6
packages/materials/.eslintrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "sa/vue",
|
||||
"rules": {
|
||||
"vue/multi-word-component-names": "off"
|
||||
}
|
||||
}
|
22
packages/materials/package.json
Normal file
22
packages/materials/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@sa/materials",
|
||||
"version": "1.0.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@sa/utils": "workspace:*",
|
||||
"@simonwep/pickr": "1.9.0",
|
||||
"simplebar-vue": "2.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typed-css-modules": "0.8.1"
|
||||
}
|
||||
}
|
7
packages/materials/src/index.ts
Normal file
7
packages/materials/src/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import AdminLayout, { LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX } from './libs/admin-layout';
|
||||
import PageTab from './libs/page-tab';
|
||||
import SimpleScrollbar from './libs/simple-scrollbar';
|
||||
import ColorPicker from './libs/color-picker';
|
||||
|
||||
export { AdminLayout, LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX, PageTab, SimpleScrollbar, ColorPicker };
|
||||
export * from './types';
|
63
packages/materials/src/libs/admin-layout/index.module.css
Normal file
63
packages/materials/src/libs/admin-layout/index.module.css
Normal file
@ -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);
|
||||
}
|
17
packages/materials/src/libs/admin-layout/index.module.css.d.ts
vendored
Normal file
17
packages/materials/src/libs/admin-layout/index.module.css.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
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 = styles;
|
5
packages/materials/src/libs/admin-layout/index.ts
Normal file
5
packages/materials/src/libs/admin-layout/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import AdminLayout from './index.vue';
|
||||
import { LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX } from './shared';
|
||||
|
||||
export default AdminLayout;
|
||||
export { LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX };
|
238
packages/materials/src/libs/admin-layout/index.vue
Normal file
238
packages/materials/src/libs/admin-layout/index.vue
Normal file
@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div :class="['relative h-full', commonClass]" :style="cssVars">
|
||||
<div
|
||||
:id="isWrapperScroll ? scrollElId : undefined"
|
||||
:class="['flex flex-col h-full', commonClass, scrollWrapperClass, { 'overflow-y-auto': isWrapperScroll }]"
|
||||
>
|
||||
<!-- Header -->
|
||||
<template v-if="showHeader">
|
||||
<header
|
||||
v-show="!fullContent"
|
||||
:class="[
|
||||
style['layout-header'],
|
||||
'flex-shrink-0',
|
||||
commonClass,
|
||||
headerClass,
|
||||
headerLeftGapClass,
|
||||
{ 'absolute top-0 left-0 w-full': fixedHeaderAndTab }
|
||||
]"
|
||||
>
|
||||
<slot name="header"></slot>
|
||||
</header>
|
||||
<div
|
||||
v-show="!fullContent && fixedHeaderAndTab"
|
||||
:class="[style['layout-header-placement'], 'flex-shrink-0 overflow-hidden']"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<!-- Tab -->
|
||||
<template v-if="showTab">
|
||||
<div
|
||||
:class="[
|
||||
style['layout-tab'],
|
||||
'flex-shrink-0',
|
||||
commonClass,
|
||||
tabClass,
|
||||
{ 'top-0!': fullContent || !showHeader },
|
||||
leftGapClass,
|
||||
{ 'absolute left-0 w-full': fixedHeaderAndTab }
|
||||
]"
|
||||
>
|
||||
<slot name="tab"></slot>
|
||||
</div>
|
||||
<div
|
||||
v-show="fullContent || fixedHeaderAndTab"
|
||||
:class="[style['layout-tab-placement'], 'flex-shrink-0 overflow-hidden']"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<!-- Sider -->
|
||||
<template v-if="showSider">
|
||||
<aside
|
||||
v-show="!fullContent"
|
||||
:class="[
|
||||
'absolute left-0 top-0 h-full',
|
||||
commonClass,
|
||||
siderClass,
|
||||
siderPaddingClass,
|
||||
siderCollapse ? style['layout-sider_collapsed'] : style['layout-sider']
|
||||
]"
|
||||
>
|
||||
<slot name="sider"></slot>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<!-- Mobile Sider -->
|
||||
<template v-if="showMobileSider">
|
||||
<aside
|
||||
:class="[
|
||||
'absolute left-0 top-0 w-0 h-full bg-white',
|
||||
commonClass,
|
||||
mobileSiderClass,
|
||||
style['layout-mobile-sider'],
|
||||
siderCollapse ? 'overflow-hidden' : style['layout-sider']
|
||||
]"
|
||||
>
|
||||
<slot name="sider"></slot>
|
||||
</aside>
|
||||
<div
|
||||
v-show="!siderCollapse"
|
||||
:class="['absolute left-0 top-0 w-full h-full bg-[rgba(0,0,0,0.2)]', style['layout-mobile-sider-mask']]"
|
||||
@click="handleClickMask"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main
|
||||
:id="isContentScroll ? scrollElId : undefined"
|
||||
:class="[
|
||||
'flex flex-col flex-grow',
|
||||
commonClass,
|
||||
contentClass,
|
||||
leftGapClass,
|
||||
{ 'overflow-y-auto': isContentScroll }
|
||||
]"
|
||||
>
|
||||
<slot></slot>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<template v-if="showFooter">
|
||||
<footer
|
||||
v-show="!fullContent"
|
||||
:class="[
|
||||
style['layout-footer'],
|
||||
'flex-shrink-0',
|
||||
commonClass,
|
||||
footerClass,
|
||||
footerLeftGapClass,
|
||||
{ 'absolute left-0 bottom-0 w-full': fixedFooter }
|
||||
]"
|
||||
>
|
||||
<slot name="footer"></slot>
|
||||
</footer>
|
||||
<div
|
||||
v-show="!fullContent && fixedFooter"
|
||||
:class="[style['layout-footer-placement'], 'flex-shrink-0 overflow-hidden']"
|
||||
></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { AdminLayoutProps } from '../../types';
|
||||
import { LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX, createLayoutCssVars } from './shared';
|
||||
import style from './index.module.css';
|
||||
|
||||
defineOptions({
|
||||
name: 'AdminLayout'
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<AdminLayoutProps>(), {
|
||||
mode: 'vertical',
|
||||
scrollMode: 'content',
|
||||
scrollElId: LAYOUT_SCROLL_EL_ID,
|
||||
commonClass: 'transition-all-300',
|
||||
fixedTop: true,
|
||||
maxZIndex: LAYOUT_MAX_Z_INDEX,
|
||||
headerVisible: true,
|
||||
headerHeight: 56,
|
||||
tabVisible: true,
|
||||
tabHeight: 48,
|
||||
siderVisible: true,
|
||||
siderCollapse: false,
|
||||
siderWidth: 220,
|
||||
siderCollapsedWidth: 64,
|
||||
footerVisible: true,
|
||||
footerHeight: 48,
|
||||
rightFooter: false
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
/**
|
||||
* update siderCollapse
|
||||
*/
|
||||
(e: 'update:siderCollapse', collapse: boolean): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
type SlotFn = (props?: Record<string, unknown>) => any;
|
||||
|
||||
type Slots = {
|
||||
/** main */
|
||||
default?: SlotFn;
|
||||
/** header */
|
||||
header?: SlotFn;
|
||||
/** tab */
|
||||
tab?: SlotFn;
|
||||
/** sider */
|
||||
sider?: SlotFn;
|
||||
/** footer */
|
||||
footer?: SlotFn;
|
||||
};
|
||||
const slots = defineSlots<Slots>();
|
||||
|
||||
const cssVars = computed(() => createLayoutCssVars(props));
|
||||
|
||||
// config visible
|
||||
const showHeader = computed(() => Boolean(slots.header) && props.headerVisible);
|
||||
const showTab = computed(() => Boolean(slots.tab) && props.tabVisible);
|
||||
const showSider = computed(() => !props.isMobile && Boolean(slots.sider) && props.siderVisible);
|
||||
const showMobileSider = computed(() => props.isMobile && Boolean(slots.sider) && props.siderVisible);
|
||||
const showFooter = computed(() => Boolean(slots.footer) && props.footerVisible);
|
||||
|
||||
// scroll mode
|
||||
const isWrapperScroll = computed(() => props.scrollMode === 'wrapper');
|
||||
const isContentScroll = computed(() => props.scrollMode === 'content');
|
||||
|
||||
// layout direction
|
||||
const isVertical = computed(() => props.mode === 'vertical');
|
||||
const isHorizontal = computed(() => props.mode === 'horizontal');
|
||||
|
||||
const fixedHeaderAndTab = computed(() => props.fixedTop || (isHorizontal.value && isWrapperScroll.value));
|
||||
|
||||
// css
|
||||
const leftGapClass = computed(() => {
|
||||
if (!props.fullContent && showSider.value) {
|
||||
return props.siderCollapse ? style['left-gap_collapsed'] : style['left-gap'];
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
const headerLeftGapClass = computed(() => (isVertical.value ? leftGapClass.value : ''));
|
||||
|
||||
const footerLeftGapClass = computed(() => {
|
||||
const condition1 = isVertical.value;
|
||||
const condition2 = isHorizontal.value && isWrapperScroll.value && !props.fixedFooter;
|
||||
const condition3 = Boolean(isHorizontal.value && props.rightFooter);
|
||||
|
||||
if (condition1 || condition2 || condition3) {
|
||||
return leftGapClass.value;
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
const siderPaddingClass = computed(() => {
|
||||
let cls = '';
|
||||
|
||||
if (showHeader.value && !headerLeftGapClass.value) {
|
||||
cls += style['sider-padding-top'];
|
||||
}
|
||||
if (showFooter.value && !footerLeftGapClass.value) {
|
||||
cls += ` ${style['sider-padding-bottom']}`;
|
||||
}
|
||||
|
||||
return cls;
|
||||
});
|
||||
|
||||
function handleClickMask() {
|
||||
emit('update:siderCollapse', true);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
70
packages/materials/src/libs/admin-layout/shared.ts
Normal file
70
packages/materials/src/libs/admin-layout/shared.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import type { AdminLayoutProps, LayoutCssVarsProps, LayoutCssVars } 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);
|
||||
}
|
3
packages/materials/src/libs/color-picker/index.ts
Normal file
3
packages/materials/src/libs/color-picker/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import ColorPicker from './index.vue';
|
||||
|
||||
export default ColorPicker;
|
116
packages/materials/src/libs/color-picker/index.vue
Normal file
116
packages/materials/src/libs/color-picker/index.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import ColorPicker from '@simonwep/pickr';
|
||||
import '@simonwep/pickr/dist/themes/nano.min.css';
|
||||
|
||||
defineOptions({
|
||||
name: 'ColorPicker'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
color: string;
|
||||
palettes?: string[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
palettes: () => [
|
||||
'#3b82f6',
|
||||
'#6366f1',
|
||||
'#8b5cf6',
|
||||
'#a855f7',
|
||||
'#0ea5e9',
|
||||
'#06b6d4',
|
||||
'#f43f5e',
|
||||
'#ef4444',
|
||||
'#ec4899',
|
||||
'#d946ef',
|
||||
'#f97316',
|
||||
'#f59e0b',
|
||||
'#eab308',
|
||||
'#84cc16',
|
||||
'#22c55e',
|
||||
'#10b981',
|
||||
'#14b8a6'
|
||||
]
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:color', value: string): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const domRef = ref<HTMLElement | null>(null);
|
||||
const instance = ref<ColorPicker | null>(null);
|
||||
|
||||
function handleColorChange(hsva: ColorPicker.HSVaColor) {
|
||||
const color = hsva.toHEXA().toString();
|
||||
emit('update:color', color);
|
||||
}
|
||||
|
||||
function initColorPicker() {
|
||||
if (!domRef.value) return;
|
||||
|
||||
instance.value = ColorPicker.create({
|
||||
el: domRef.value,
|
||||
theme: 'nano',
|
||||
swatches: props.palettes,
|
||||
lockOpacity: true,
|
||||
default: props.color,
|
||||
disabled: props.disabled,
|
||||
components: {
|
||||
preview: true,
|
||||
opacity: false,
|
||||
hue: true,
|
||||
interaction: {
|
||||
hex: true,
|
||||
rgba: true,
|
||||
input: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
instance.value.on('change', handleColorChange);
|
||||
}
|
||||
|
||||
function updateColor(color: string) {
|
||||
if (!instance.value) return;
|
||||
|
||||
instance.value.setColor(color);
|
||||
}
|
||||
|
||||
function updateDisabled(disabled: boolean) {
|
||||
if (!instance.value) return;
|
||||
|
||||
if (disabled) {
|
||||
instance.value.disable();
|
||||
} else {
|
||||
instance.value.enable();
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.color,
|
||||
value => {
|
||||
updateColor(value);
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.disabled,
|
||||
value => {
|
||||
updateDisabled(value);
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
initColorPicker();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="domRef"></div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
49
packages/materials/src/libs/page-tab/button-tab.vue
Normal file
49
packages/materials/src/libs/page-tab/button-tab.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
':soy: relative inline-flex justify-center items-center gap-12px px-12px py-4px border-1px border-solid rounded-4px cursor-pointer whitespace-nowrap',
|
||||
style['button-tab'],
|
||||
{ [style['button-tab_dark']]: darkMode },
|
||||
{ [style['button-tab_active']]: active },
|
||||
{ [style['button-tab_active_dark']]: active && darkMode }
|
||||
]"
|
||||
>
|
||||
<slot name="prefix"></slot>
|
||||
<slot></slot>
|
||||
<slot name="suffix"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import style from './index.module.css';
|
||||
import type { PageTabProps } from '../../types';
|
||||
|
||||
defineOptions({
|
||||
name: 'ButtonTab'
|
||||
});
|
||||
|
||||
defineProps<PageTabProps>();
|
||||
|
||||
type SlotFn = (props?: Record<string, unknown>) => any;
|
||||
|
||||
type Slots = {
|
||||
/**
|
||||
* slot
|
||||
* @description the center content of the tab
|
||||
*/
|
||||
default?: SlotFn;
|
||||
/**
|
||||
* slot
|
||||
* @description the left content of the tab
|
||||
*/
|
||||
prefix?: SlotFn;
|
||||
/**
|
||||
* slot
|
||||
* @description the right content of the tab
|
||||
*/
|
||||
suffix?: SlotFn;
|
||||
};
|
||||
defineSlots<Slots>();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
31
packages/materials/src/libs/page-tab/chrome-tab-bg.vue
Normal file
31
packages/materials/src/libs/page-tab/chrome-tab-bg.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<svg style="width: 100%; height: 100%">
|
||||
<defs>
|
||||
<symbol id="geometry-left" viewBox="0 0 214 36">
|
||||
<path d="M17 0h197v36H0v-2c4.5 0 9-3.5 9-8V8c0-4.5 3.5-8 8-8z"></path>
|
||||
</symbol>
|
||||
<symbol id="geometry-right" viewBox="0 0 214 36">
|
||||
<use xlink:href="#geometry-left"></use>
|
||||
</symbol>
|
||||
<clipPath>
|
||||
<rect width="100%" height="100%" x="0"></rect>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<svg width="51%" height="100%">
|
||||
<use xlink:href="#geometry-left" width="214" height="36" fill="currentColor"></use>
|
||||
</svg>
|
||||
<g transform="scale(-1, 1)">
|
||||
<svg width="51%" height="100%" x="-100%" y="0">
|
||||
<use xlink:href="#geometry-right" width="214" height="36" fill="currentColor"></use>
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'ChromeTabBg'
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
55
packages/materials/src/libs/page-tab/chrome-tab.vue
Normal file
55
packages/materials/src/libs/page-tab/chrome-tab.vue
Normal file
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
':soy: relative inline-flex justify-center items-center gap-16px -mr-18px px-24px py-6px cursor-pointer whitespace-nowrap',
|
||||
style['chrome-tab'],
|
||||
{ [style['chrome-tab_dark']]: darkMode },
|
||||
{ [style['chrome-tab_active']]: active },
|
||||
{ [style['chrome-tab_active_dark']]: active && darkMode }
|
||||
]"
|
||||
>
|
||||
<div :class="[':soy: absolute left-0 top-0 -z-1 w-full h-full pointer-events-none', style['chrome-tab__bg']]">
|
||||
<ChromeTabBg />
|
||||
</div>
|
||||
<slot name="prefix"></slot>
|
||||
<slot></slot>
|
||||
<slot name="suffix"></slot>
|
||||
<div :class="[':soy: absolute right-7px w-1px h-16px bg-#1f2225', style['chrome-tab-divider']]"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ChromeTabBg from './chrome-tab-bg.vue';
|
||||
import style from './index.module.css';
|
||||
import type { PageTabProps } from '../../types';
|
||||
|
||||
defineOptions({
|
||||
name: 'ChromeTab'
|
||||
});
|
||||
|
||||
defineProps<PageTabProps>();
|
||||
|
||||
type SlotFn = (props?: Record<string, unknown>) => any;
|
||||
|
||||
type Slots = {
|
||||
/**
|
||||
* slot
|
||||
* @description the center content of the tab
|
||||
*/
|
||||
default?: SlotFn;
|
||||
/**
|
||||
* slot
|
||||
* @description the left content of the tab
|
||||
*/
|
||||
prefix?: SlotFn;
|
||||
/**
|
||||
* slot
|
||||
* @description the right content of the tab
|
||||
*/
|
||||
suffix?: SlotFn;
|
||||
};
|
||||
|
||||
defineSlots<Slots>();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
31
packages/materials/src/libs/page-tab/icon-close.vue
Normal file
31
packages/materials/src/libs/page-tab/icon-close.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div
|
||||
class=":soy: relative inline-flex justify-center items-center w-16px h-16px text-14px rd-50%"
|
||||
@click.stop="handleClick"
|
||||
>
|
||||
<svg width="1em" height="1em" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8L295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512L196.9 824.9A7.95 7.95 0 0 0 203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1l216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'IconClose'
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'click'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
function handleClick() {
|
||||
emit('click');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
97
packages/materials/src/libs/page-tab/index.module.css
Normal file
97
packages/materials/src/libs/page-tab/index.module.css
Normal file
@ -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 .icon_close:hover {
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
background-color: var(--soy-primary-color);
|
||||
}
|
||||
|
||||
.button-tab_dark .icon_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 .icon_close:hover {
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
background-color: #9ca3af;
|
||||
}
|
||||
|
||||
.chrome-tab_active .icon_close:hover {
|
||||
background-color: var(--soy-primary-color);
|
||||
}
|
||||
|
||||
.chrome-tab_dark .icon_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);
|
||||
}
|
14
packages/materials/src/libs/page-tab/index.module.css.d.ts
vendored
Normal file
14
packages/materials/src/libs/page-tab/index.module.css.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
declare const styles: {
|
||||
readonly 'button-tab': string;
|
||||
readonly 'button-tab_dark': string;
|
||||
readonly 'button-tab_active': string;
|
||||
readonly 'button-tab_active_dark': string;
|
||||
readonly icon_close: 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;
|
||||
};
|
||||
export = styles;
|
3
packages/materials/src/libs/page-tab/index.ts
Normal file
3
packages/materials/src/libs/page-tab/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import PageTab from './index.vue';
|
||||
|
||||
export default PageTab;
|
94
packages/materials/src/libs/page-tab/index.vue
Normal file
94
packages/materials/src/libs/page-tab/index.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<component :is="activeTabComponent.component" :class="activeTabComponent.class" :style="cssVars" v-bind="bindProps">
|
||||
<template #prefix>
|
||||
<slot name="prefix"></slot>
|
||||
</template>
|
||||
<slot></slot>
|
||||
<template #suffix>
|
||||
<slot name="suffix">
|
||||
<SvgIconClose v-if="closable" :class="[style['icon_close']]" @click="handleClose" />
|
||||
</slot>
|
||||
</template>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
import { createTabCssVars, ACTIVE_COLOR } from './shared';
|
||||
import ChromeTab from './chrome-tab.vue';
|
||||
import ButtonTab from './button-tab.vue';
|
||||
import SvgIconClose from './icon-close.vue';
|
||||
import style from './index.module.css';
|
||||
import type { PageTabProps, PageTabMode } from '../../types';
|
||||
|
||||
defineOptions({
|
||||
name: 'PageTab'
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<PageTabProps>(), {
|
||||
mode: 'chrome',
|
||||
commonClass: 'transition-all-300',
|
||||
activeColor: ACTIVE_COLOR,
|
||||
closable: true
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'close'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
type SlotFn = (props?: Record<string, unknown>) => any;
|
||||
|
||||
type Slots = {
|
||||
/**
|
||||
* slot
|
||||
* @description the center content of the tab
|
||||
*/
|
||||
default?: SlotFn;
|
||||
/**
|
||||
* slot
|
||||
* @description the left content of the tab
|
||||
*/
|
||||
prefix?: SlotFn;
|
||||
/**
|
||||
* slot
|
||||
* @description the right content of the tab
|
||||
*/
|
||||
suffix?: SlotFn;
|
||||
};
|
||||
|
||||
defineSlots<Slots>();
|
||||
|
||||
const activeTabComponent = computed(() => {
|
||||
const { mode, chromeClass, buttonClass } = props;
|
||||
|
||||
const tabComponentMap = {
|
||||
chrome: {
|
||||
component: ChromeTab,
|
||||
class: chromeClass
|
||||
},
|
||||
button: {
|
||||
component: ButtonTab,
|
||||
class: buttonClass
|
||||
}
|
||||
} satisfies Record<PageTabMode, { component: Component; class?: string }>;
|
||||
|
||||
return tabComponentMap[mode];
|
||||
});
|
||||
|
||||
const cssVars = computed(() => createTabCssVars(props.activeColor));
|
||||
|
||||
const bindProps = computed(() => {
|
||||
const { chromeClass: _chromeCls, buttonClass: _btnCls, ...rest } = props;
|
||||
|
||||
return rest;
|
||||
});
|
||||
|
||||
function handleClose() {
|
||||
emit('close');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
33
packages/materials/src/libs/page-tab/shared.ts
Normal file
33
packages/materials/src/libs/page-tab/shared.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { addColorAlpha, transformColorWithOpacity } from '@sa/utils';
|
||||
import type { PageTabCssVarsProps, PageTabCssVars } 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);
|
||||
}
|
3
packages/materials/src/libs/simple-scrollbar/index.ts
Normal file
3
packages/materials/src/libs/simple-scrollbar/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import SimpleScrollbar from './index.vue';
|
||||
|
||||
export default SimpleScrollbar;
|
18
packages/materials/src/libs/simple-scrollbar/index.vue
Normal file
18
packages/materials/src/libs/simple-scrollbar/index.vue
Normal file
@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import Simplebar from 'simplebar-vue';
|
||||
import 'simplebar-vue/dist/simplebar.min.css';
|
||||
|
||||
defineOptions({
|
||||
name: 'SimpleScrollbar'
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1-hidden h-full">
|
||||
<Simplebar class="h-full">
|
||||
<slot />
|
||||
</Simplebar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
282
packages/materials/src/types/index.ts
Normal file
282
packages/materials/src/types/index.ts
Normal file
@ -0,0 +1,282 @@
|
||||
/**
|
||||
* 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
|
||||
* @description 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
|
||||
* @description 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
|
||||
* @description it can be used to get the corresponding Dom and scroll it
|
||||
* @default
|
||||
* ```ts
|
||||
* const adminLayoutScrollElId = '__ADMIN_LAYOUT_SCROLL_EL_ID__'
|
||||
* ```
|
||||
* @example use the default id by import
|
||||
* ```ts
|
||||
* import { adminLayoutScrollElId } from '@sa/vue-materials';
|
||||
* ```
|
||||
*/
|
||||
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
|
||||
* @description 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
|
||||
* @description the z-index of Header,Tab,Sider and Footer will not exceed this value
|
||||
*/
|
||||
maxZIndex?: number;
|
||||
}
|
||||
|
||||
type Kebab<S extends string> = S extends Uncapitalize<S> ? S : `-${Uncapitalize<S>}`;
|
||||
|
||||
type KebabCase<S extends string> = S extends `${infer Start}${infer End}`
|
||||
? `${Uncapitalize<Start>}${KebabCase<Kebab<End>>}`
|
||||
: 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<K>}`]: 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
|
||||
* @description 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
|
||||
* @description 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<K>}`]: string | number;
|
||||
};
|
20
packages/materials/tsconfig.json
Normal file
20
packages/materials/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"noUnusedLocals": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
18
packages/request/package.json
Normal file
18
packages/request/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@sa/request",
|
||||
"version": "1.0.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "1.6.2",
|
||||
"ofetch": "1.3.3"
|
||||
}
|
||||
}
|
10
packages/request/src/axios/index.ts
Normal file
10
packages/request/src/axios/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import axios from 'axios';
|
||||
import type { CreateAxiosDefaults } from 'axios';
|
||||
|
||||
export function createAxios(config?: CreateAxiosDefaults) {
|
||||
const instance = axios.create(config);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
export default createAxios;
|
4
packages/request/src/index.ts
Normal file
4
packages/request/src/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { createAxios } from './axios';
|
||||
import { createOfetch } from './ofetch';
|
||||
|
||||
export { createAxios, createOfetch };
|
10
packages/request/src/ofetch/index.ts
Normal file
10
packages/request/src/ofetch/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { ofetch } from 'ofetch';
|
||||
import type { FetchOptions } from 'ofetch';
|
||||
|
||||
export function createOfetch(options: FetchOptions) {
|
||||
const request = ofetch.create(options);
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
export default createOfetch;
|
20
packages/request/tsconfig.json
Normal file
20
packages/request/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"noUnusedLocals": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
6
packages/scripts/bin.cjs
Executable file
6
packages/scripts/bin.cjs
Executable file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const path = require('path');
|
||||
const jiti = require('jiti')(__filename);
|
||||
|
||||
jiti(path.resolve(__dirname, './src/index.ts'));
|
28
packages/scripts/package.json
Normal file
28
packages/scripts/package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@sa/scripts",
|
||||
"version": "1.0.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"bin": {
|
||||
"sa": "./bin.cjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"c12": "1.5.1",
|
||||
"cac": "6.7.14",
|
||||
"consola": "3.2.3",
|
||||
"enquirer": "2.4.1",
|
||||
"execa": "8.0.1",
|
||||
"jiti": "1.21.0",
|
||||
"lint-staged": "15.1.0",
|
||||
"npm-check-updates": "16.14.6",
|
||||
"rimraf": "5.0.5"
|
||||
}
|
||||
}
|
5
packages/scripts/src/commands/cleanup.ts
Normal file
5
packages/scripts/src/commands/cleanup.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { rimraf } from 'rimraf';
|
||||
|
||||
export async function cleanup(paths: string[]) {
|
||||
await rimraf(paths, { glob: true });
|
||||
}
|
75
packages/scripts/src/commands/git-commit.ts
Normal file
75
packages/scripts/src/commands/git-commit.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import path from 'node:path';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import enquirer from 'enquirer';
|
||||
import { bgRed, red, green } from 'kolorist';
|
||||
import { execCommand } from '../shared';
|
||||
import type { CliOption } from '../types';
|
||||
|
||||
interface PromptObject {
|
||||
types: string;
|
||||
scopes: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export async function gitCommit(
|
||||
gitCommitTypes: CliOption['gitCommitTypes'],
|
||||
gitCommitScopes: CliOption['gitCommitScopes']
|
||||
) {
|
||||
const typesChoices = gitCommitTypes.map(([name, title]) => {
|
||||
const nameWithSuffix = `${name}:`;
|
||||
|
||||
const message = `${nameWithSuffix.padEnd(12)}${title}`;
|
||||
|
||||
return {
|
||||
name,
|
||||
message
|
||||
};
|
||||
});
|
||||
|
||||
const scopesChoices = gitCommitScopes.map(([name, title]) => ({
|
||||
name,
|
||||
message: `${name.padEnd(30)} (${title})`
|
||||
}));
|
||||
|
||||
const result = await enquirer.prompt<PromptObject>([
|
||||
{
|
||||
name: 'types',
|
||||
type: 'select',
|
||||
message: 'Please select a type',
|
||||
choices: typesChoices
|
||||
},
|
||||
{
|
||||
name: 'scopes',
|
||||
type: 'select',
|
||||
message: 'Please select a scope',
|
||||
choices: scopesChoices
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
message: 'Please enter a description'
|
||||
}
|
||||
]);
|
||||
|
||||
const commitMsg = `${result.types}(${result.scopes}): ${result.description}`;
|
||||
|
||||
await execCommand('git', ['commit', '-m', commitMsg], { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
export async function gitCommitVerify() {
|
||||
const gitPath = await execCommand('git', ['rev-parse', '--show-toplevel']);
|
||||
|
||||
const gitMsgPath = path.join(gitPath, '.git', 'COMMIT_EDITMSG');
|
||||
|
||||
const commitMsg = readFileSync(gitMsgPath, 'utf8').trim();
|
||||
|
||||
const REG_EXP = /(?<type>[a-z]+)(\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i;
|
||||
|
||||
if (!REG_EXP.test(commitMsg)) {
|
||||
throw new Error(
|
||||
`${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'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
5
packages/scripts/src/commands/index.ts
Normal file
5
packages/scripts/src/commands/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './git-commit';
|
||||
export * from './cleanup';
|
||||
export * from './update-pkg';
|
||||
export * from './prettier';
|
||||
export * from './lint-staged';
|
5
packages/scripts/src/commands/lint-staged.ts
Normal file
5
packages/scripts/src/commands/lint-staged.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export async function execLintStaged(config: Record<string, string | string[]>) {
|
||||
const lintStaged = (await import('lint-staged')).default;
|
||||
|
||||
return lintStaged({ config, allowEmpty: true });
|
||||
}
|
7
packages/scripts/src/commands/prettier.ts
Normal file
7
packages/scripts/src/commands/prettier.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { execCommand } from '../shared';
|
||||
|
||||
export async function prettierWrite(writeGlob: string[]) {
|
||||
await execCommand('npx', ['prettier', '--write', '.', ...writeGlob], {
|
||||
stdio: 'inherit'
|
||||
});
|
||||
}
|
5
packages/scripts/src/commands/update-pkg.ts
Normal file
5
packages/scripts/src/commands/update-pkg.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { execCommand } from '../shared';
|
||||
|
||||
export async function updatePkg(args: string[] = ['--deep', '-u']) {
|
||||
execCommand('npx', ['ncu', ...args], { stdio: 'inherit' });
|
||||
}
|
74
packages/scripts/src/config/index.ts
Normal file
74
packages/scripts/src/config/index.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { loadConfig } from 'c12';
|
||||
import type { CliOption } from '../types';
|
||||
|
||||
const eslintExt = '*.{js,jsx,mjs,cjs,ts,tsx,vue}';
|
||||
|
||||
const defaultOptions: CliOption = {
|
||||
cwd: process.cwd(),
|
||||
cleanupDirs: [
|
||||
'**/dist',
|
||||
'**/package-lock.json',
|
||||
'**/yarn.lock',
|
||||
'**/pnpm-lock.yaml',
|
||||
'**/node_modules',
|
||||
'!node_modules/**'
|
||||
],
|
||||
gitCommitTypes: [
|
||||
['feat', 'A new feature'],
|
||||
['fix', 'A bug fix'],
|
||||
['docs', 'Documentation only changes'],
|
||||
['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'],
|
||||
['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']
|
||||
],
|
||||
gitCommitScopes: [
|
||||
['projects', 'project'],
|
||||
['components', 'components'],
|
||||
['hooks', 'hook functions'],
|
||||
['utils', 'utils functions'],
|
||||
['types', 'TS declaration'],
|
||||
['styles', 'style'],
|
||||
['deps', 'project dependencies'],
|
||||
['release', 'release project'],
|
||||
['other', 'other changes']
|
||||
],
|
||||
ncuCommandArgs: ['--deep', '-u'],
|
||||
prettierWriteGlob: [
|
||||
`!**/${eslintExt}`,
|
||||
'!*.min.*',
|
||||
'!CHANGELOG.md',
|
||||
'!dist',
|
||||
'!LICENSE*',
|
||||
'!output',
|
||||
'!coverage',
|
||||
'!public',
|
||||
'!temp',
|
||||
'!package-lock.json',
|
||||
'!pnpm-lock.yaml',
|
||||
'!yarn.lock',
|
||||
'!.github',
|
||||
'!__snapshots__',
|
||||
'!node_modules'
|
||||
],
|
||||
lintStagedConfig: {
|
||||
[eslintExt]: 'eslint --fix',
|
||||
'*': 'sa prettier-write'
|
||||
}
|
||||
};
|
||||
|
||||
export async function loadCliOptions(overrides?: Partial<CliOption>, cwd = process.cwd()) {
|
||||
const { config } = await loadConfig<Partial<CliOption>>({
|
||||
name: 'soybean',
|
||||
defaults: defaultOptions,
|
||||
overrides,
|
||||
cwd,
|
||||
packageJson: true
|
||||
});
|
||||
|
||||
return config as CliOption;
|
||||
}
|
70
packages/scripts/src/index.ts
Executable file
70
packages/scripts/src/index.ts
Executable file
@ -0,0 +1,70 @@
|
||||
import cac from 'cac';
|
||||
import { blue, lightGreen } from 'kolorist';
|
||||
import { version } from '../package.json';
|
||||
import { cleanup, updatePkg, gitCommit, gitCommitVerify, prettierWrite, execLintStaged } from './commands';
|
||||
import { loadCliOptions } from './config';
|
||||
|
||||
type Command = 'cleanup' | 'update-pkg' | 'git-commit' | 'git-commit-verify' | 'prettier-write' | 'lint-staged';
|
||||
|
||||
type CommandAction<A extends object> = (args?: A) => Promise<void> | void;
|
||||
|
||||
type CommandWithAction<A extends object = object> = Record<Command, { desc: string; action: CommandAction<A> }>;
|
||||
|
||||
interface CommandArg {
|
||||
total?: boolean;
|
||||
}
|
||||
|
||||
export async function setupCli() {
|
||||
const cliOptions = await loadCliOptions();
|
||||
|
||||
const cli = cac(blue('soybean'));
|
||||
|
||||
cli.version(lightGreen(version)).help();
|
||||
|
||||
const commands: CommandWithAction<CommandArg> = {
|
||||
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 () => {
|
||||
await gitCommit(cliOptions.gitCommitTypes, cliOptions.gitCommitScopes);
|
||||
}
|
||||
},
|
||||
'git-commit-verify': {
|
||||
desc: 'verify git commit message, make sure it match Conventional Commits standard',
|
||||
action: async () => {
|
||||
await gitCommitVerify();
|
||||
}
|
||||
},
|
||||
'prettier-write': {
|
||||
desc: 'run prettier --write',
|
||||
action: async () => {
|
||||
await prettierWrite(cliOptions.prettierWriteGlob);
|
||||
}
|
||||
},
|
||||
'lint-staged': {
|
||||
desc: 'run lint-staged',
|
||||
action: async () => {
|
||||
await execLintStaged(cliOptions.lintStagedConfig);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const [command, { desc, action }] of Object.entries(commands)) {
|
||||
cli.command(command, lightGreen(desc)).action(action);
|
||||
}
|
||||
|
||||
cli.parse();
|
||||
}
|
||||
|
||||
setupCli();
|
7
packages/scripts/src/shared/index.ts
Normal file
7
packages/scripts/src/shared/index.ts
Normal file
@ -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?.trim() || '';
|
||||
}
|
37
packages/scripts/src/types/index.ts
Normal file
37
packages/scripts/src/types/index.ts
Normal file
@ -0,0 +1,37 @@
|
||||
export interface CliOption {
|
||||
/**
|
||||
* the project root directory
|
||||
*/
|
||||
cwd: string;
|
||||
/**
|
||||
* cleanup dirs
|
||||
* @default
|
||||
* ```json
|
||||
* ["** /dist", "** /pnpm-lock.yaml", "** /node_modules", "!node_modules/**"]
|
||||
* ```
|
||||
* @description glob pattern syntax {@link https://github.com/isaacs/minimatch}
|
||||
*/
|
||||
cleanupDirs: string[];
|
||||
/**
|
||||
* git commit types
|
||||
*/
|
||||
gitCommitTypes: [string, string][];
|
||||
/**
|
||||
* git commit scopes
|
||||
*/
|
||||
gitCommitScopes: [string, string][];
|
||||
/**
|
||||
* npm-check-updates command args
|
||||
* @default ["--deep","-u"]
|
||||
*/
|
||||
ncuCommandArgs: string[];
|
||||
/**
|
||||
* prettier write glob
|
||||
* @description glob pattern syntax {@link https://github.com/micromatch/micromatch}
|
||||
*/
|
||||
prettierWriteGlob: string[];
|
||||
/**
|
||||
* lint-staged config
|
||||
*/
|
||||
lintStagedConfig: Record<string, string | string[]>;
|
||||
}
|
20
packages/scripts/tsconfig.json
Normal file
20
packages/scripts/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"noUnusedLocals": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*", "typings/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
15
packages/scripts/typings/pkg.d.ts
vendored
Normal file
15
packages/scripts/typings/pkg.d.ts
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
declare module 'lint-staged' {
|
||||
interface LintStagedOptions {
|
||||
config?: Record<string, string | string[]>;
|
||||
allowEmpty?: boolean;
|
||||
}
|
||||
|
||||
type LintStagedFn = (options: LintStagedOptions) => Promise<boolean>;
|
||||
interface LintStaged extends LintStagedFn {
|
||||
default: LintStagedFn;
|
||||
}
|
||||
|
||||
const lintStaged: LintStaged;
|
||||
|
||||
export default lintStaged;
|
||||
}
|
14
packages/uno-preset/package.json
Normal file
14
packages/uno-preset/package.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@sa/uno-preset",
|
||||
"version": "1.0.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
57
packages/uno-preset/src/index.ts
Normal file
57
packages/uno-preset/src/index.ts
Normal file
@ -0,0 +1,57 @@
|
||||
// @unocss-include
|
||||
|
||||
import type { Preset } from '@unocss/core';
|
||||
import type { Theme } from '@unocss/preset-uno';
|
||||
|
||||
export function presetSoybeanAdmin(): Preset<Theme> {
|
||||
const preset: Preset<Theme> = {
|
||||
name: 'preset-soybean-admin',
|
||||
shortcuts: [
|
||||
{
|
||||
'wh-full': 'w-full h-full'
|
||||
},
|
||||
{
|
||||
'flex-center': 'flex justify-center items-center',
|
||||
'flex-x-center': 'flex justify-center',
|
||||
'flex-y-center': 'flex items-center',
|
||||
'flex-vertical': 'flex flex-col',
|
||||
'flex-vertical-center': 'flex-center flex-col',
|
||||
'flex-vertical-stretch': 'flex-vertical 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-vertical': 'inline-flex flex-col',
|
||||
'i-flex-vertical-stretch': 'i-flex-vertical 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 wh-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 wh-full'
|
||||
},
|
||||
{
|
||||
'nowrap-hidden': 'overflow-hidden whitespace-nowrap',
|
||||
'ellipsis-text': 'nowrap-hidden text-ellipsis'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return preset;
|
||||
}
|
||||
|
||||
export default presetSoybeanAdmin;
|
20
packages/uno-preset/tsconfig.json
Normal file
20
packages/uno-preset/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"noUnusedLocals": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
22
packages/utils/package.json
Normal file
22
packages/utils/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@sa/utils",
|
||||
"version": "1.0.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"colord": "2.9.3",
|
||||
"crypto-js": "4.2.0",
|
||||
"localforage": "1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/crypto-js": "4.2.1"
|
||||
}
|
||||
}
|
257
packages/utils/src/color.ts
Normal file
257
packages/utils/src/color.ts
Normal file
@ -0,0 +1,257 @@
|
||||
import { colord, extend } from 'colord';
|
||||
import namesPlugin from 'colord/plugins/names';
|
||||
import mixPlugin from 'colord/plugins/mix';
|
||||
import type { AnyColor, HsvColor, RgbColor } from 'colord';
|
||||
|
||||
extend([namesPlugin, mixPlugin]);
|
||||
|
||||
/**
|
||||
* add color alpha
|
||||
* @param color - color
|
||||
* @param alpha - alpha (0 - 1)
|
||||
*/
|
||||
export function addColorAlpha(color: string, 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: string, secondColor: string, 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: string, 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: string) {
|
||||
return colord(color).isEqual('#ffffff');
|
||||
}
|
||||
|
||||
/**
|
||||
* get rgb of color
|
||||
* @param color color
|
||||
*/
|
||||
export function getRgbOfColor(color: string) {
|
||||
return colord(color).toRgb();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* the color index of color palette
|
||||
* @description from left to right, the color is from light to dark, 6 is main color
|
||||
*/
|
||||
type ColorIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
|
||||
|
||||
/**
|
||||
* get color palette (from left to right, the color is from light to dark, 6 is main color)
|
||||
* @param color - color
|
||||
* @param index - the color index of color palette (the main color index is 6)
|
||||
* @returns hex color
|
||||
*/
|
||||
export function getColorPalette(color: AnyColor, index: ColorIndex): string {
|
||||
const transformColor = colord(color);
|
||||
|
||||
if (!transformColor.isValid()) {
|
||||
throw Error('invalid input color value');
|
||||
}
|
||||
|
||||
if (index === 6) {
|
||||
return colord(transformColor).toHex();
|
||||
}
|
||||
|
||||
const isLight = index < 6;
|
||||
const hsv = transformColor.toHsv();
|
||||
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 colord(newHsv).toHex();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: 4, opacity: 0.9 },
|
||||
{ index: 3, opacity: 0.95 },
|
||||
{ index: 2, opacity: 0.97 },
|
||||
{ index: 1, opacity: 0.98 }
|
||||
];
|
||||
|
||||
/**
|
||||
* get color palettes
|
||||
* @param color - color
|
||||
* @param darkTheme - dark theme
|
||||
* @param darkThemeMixColor - dark theme mix color (default: #141414)
|
||||
*/
|
||||
export function getColorPalettes(color: AnyColor, darkTheme = false, darkThemeMixColor = '#141414'): string[] {
|
||||
const indexes: ColorIndex[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
|
||||
const patterns = indexes.map(index => getColorPalette(color, index));
|
||||
|
||||
if (darkTheme) {
|
||||
const darkPatterns = darkColorMap.map(({ index, opacity }) => {
|
||||
const darkColor = colord(darkThemeMixColor).mix(patterns[index], opacity);
|
||||
|
||||
return darkColor;
|
||||
});
|
||||
|
||||
return darkPatterns.map(item => colord(item).toHex());
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
29
packages/utils/src/crypto.ts
Normal file
29
packages/utils/src/crypto.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
export class Crypto<T extends object> {
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
3
packages/utils/src/index.ts
Normal file
3
packages/utils/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './color';
|
||||
export * from './crypto';
|
||||
export * from './storage';
|
76
packages/utils/src/storage.ts
Normal file
76
packages/utils/src/storage.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import localforage from 'localforage';
|
||||
|
||||
/**
|
||||
* the storage type
|
||||
*/
|
||||
export type StorageType = 'local' | 'session';
|
||||
|
||||
export function createStorage<T extends object>(type: StorageType) {
|
||||
const stg = type === 'session' ? window.sessionStorage : window.localStorage;
|
||||
|
||||
const storage = {
|
||||
/**
|
||||
* set session
|
||||
* @param key session key
|
||||
* @param value session value
|
||||
*/
|
||||
set<K extends keyof T>(key: K, value: T[K]) {
|
||||
const json = JSON.stringify(value);
|
||||
|
||||
stg.setItem(key as string, json);
|
||||
},
|
||||
/**
|
||||
* get session
|
||||
* @param key session key
|
||||
*/
|
||||
get<K extends keyof T>(key: K): T[K] | null {
|
||||
const json = stg.getItem(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(key as string);
|
||||
|
||||
return null;
|
||||
},
|
||||
remove(key: keyof T) {
|
||||
stg.removeItem(key as string);
|
||||
},
|
||||
clear() {
|
||||
stg.clear();
|
||||
}
|
||||
};
|
||||
return storage;
|
||||
}
|
||||
|
||||
type LocalForage<T extends object> = Omit<typeof localforage, 'getItem' | 'setItem' | 'removeItem'> & {
|
||||
getItem<K extends keyof T>(key: K, callback?: (err: any, value: T[K] | null) => void): Promise<T[K] | null>;
|
||||
|
||||
setItem<K extends keyof T>(key: K, value: T[K], callback?: (err: any, value: T[K]) => void): Promise<T[K]>;
|
||||
|
||||
removeItem(key: keyof T, callback?: (err: any) => void): Promise<void>;
|
||||
};
|
||||
|
||||
type LocalforageDriver = 'local' | 'indexedDB' | 'webSQL';
|
||||
|
||||
export function createLocalforage<T extends object>(driver: LocalforageDriver) {
|
||||
const driverMap: Record<LocalforageDriver, string> = {
|
||||
local: localforage.LOCALSTORAGE,
|
||||
indexedDB: localforage.INDEXEDDB,
|
||||
webSQL: localforage.WEBSQL
|
||||
};
|
||||
|
||||
localforage.config({
|
||||
driver: driverMap[driver]
|
||||
});
|
||||
|
||||
return localforage as LocalForage<T>;
|
||||
}
|
Reference in New Issue
Block a user