diff --git a/packages/client/components.d.ts b/packages/client/components.d.ts index dd1270d..613e14f 100644 --- a/packages/client/components.d.ts +++ b/packages/client/components.d.ts @@ -13,7 +13,6 @@ declare module 'vue' { CookieDemo: typeof import('./src/components/CookieDemo.vue')['default'] DataFetch: typeof import('./src/components/DataFetch.vue')['default'] HelloWorld: typeof import('./src/components/HelloWorld.vue')['default'] - MazBtn: typeof import('maz-ui/components/MazBtn')['default'] QuillEditor: typeof import('./src/components/QuillEditor/index.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] diff --git a/packages/client/package.json b/packages/client/package.json index ad13702..aae169d 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -8,6 +8,7 @@ "check": "vue-tsc" }, "devDependencies": { + "sass-embedded": "^1.93.2", "unplugin-vue-components": "^29.1.0", "vue-tsc": "^3.1.0" }, diff --git a/packages/client/src/App.vue b/packages/client/src/App.vue index 121ff3d..f32228a 100644 --- a/packages/client/src/App.vue +++ b/packages/client/src/App.vue @@ -10,5 +10,3 @@ onServerPrefetch(() => { - - diff --git a/packages/client/src/assets/styles/scss/_global.scss b/packages/client/src/assets/styles/scss/_global.scss new file mode 100644 index 0000000..5b4dac0 --- /dev/null +++ b/packages/client/src/assets/styles/scss/_global.scss @@ -0,0 +1,206 @@ +/* + theme-helpers.scss + 为主题开发提供的 SCSS 帮助函数 / mixin / 示例 + 说明(中文注释): + - 提供把 SCSS map 转换为 CSS 自定义属性(variables)的 mixin + - 提供生成主题(light/dark)选择器的 mixin 示例 + - 提供读取 CSS 变量的辅助函数 `css-var()` + - 提供颜色可读性判断函数 `readable-text()`(基于简单亮度公式) + - 提供颜色微调辅助 `tone()` + + 使用方式:在你的主样式中导入此文件并调用 mixin/map。示例在文件底部有注释。 +*/ + +// ----------------------------- +// CSS 变量相关帮助 +// ----------------------------- + +// 返回一个 var(...) 字符串,方便在 SCSS 中使用 CSS 变量 +@function css-var($name, $fallback: null) { + @if $fallback == null { + @return unquote("var(--#{$name})"); + } @else { + @return unquote("var(--#{$name}, #{$fallback})"); + } +} + +// 将一个 map 转换为 CSS 变量声明,需在选择器块内使用 +// 用法: +// :root { @include declare-theme-variables($my-theme-map); } +@mixin declare-theme-variables($map) { + @each $token, $val in $map { + // 允许传入颜色、字符串或数字 + --#{$token}: #{$val}; + } +} + +// 生成主题选择器(selector 可以是 ":root"、":root[data-theme=\"dark\"]" 或 ".theme-dark") +// 用法:@include generate-theme(':root', $theme-light); +@mixin generate-theme($selector, $map) { + #{$selector} { + @include declare-theme-variables($map); + } +} + + +// ----------------------------- +// 颜色工具函数 +// ----------------------------- + +// 计算近似亮度(0-255)用于对比判定(基于 ITU BT.601 近似) +@function _luma($color) { + // 期望 $color 为 color 类型 + $r: red($color); + $g: green($color); + $b: blue($color); + @return ($r * 0.299) + ($g * 0.587) + ($b * 0.114); +} + +// 根据背景色返回可读的文字颜色(#000 或 #fff) +// 示例: color: readable-text(#0d1117); +@function readable-text($bg, $light: #ffffff, $dark: #000000) { + // 如果传入的不是 color 类型,尝试转换(如果是变量字符串则无法计算) + @if type-of($bg) != 'color' { + // 无法在构建时计算 CSS 变量的对比,默认返回白色以便在暗色环境下可读 + @return $light; + } + @if _luma($bg) > 186 { + @return $dark; + } + @return $light; +} + +// 基于 lighten/darken 的简单色调微调函数(正值变亮,负值变暗) +@function tone($color, $percent) { + @if type-of($color) != 'color' { + @warn "tone(): first argument is not a color; returned value will be unchanged."; + @return $color; + } + @if $percent == 0 { + @return $color; + } + @if $percent > 0 { + @return lighten($color, $percent); + } @else { + @return darken($color, abs($percent)); + } +} + +// 使颜色变浅的辅助函数 +// 用法: +// lighten-by(#0d1117, 20) -> 以 20% 变亮 +// lighten-by(#0d1117, 20%) -> 以 20% 变亮 +// lighten-by(#0d1117, 0.2) -> 以 20% 变亮(小数形式) +@function lighten-by($color, $amount) { + @if type-of($color) != 'color' { + @warn "lighten-by(): first argument is not a color; returned value will be unchanged."; + @return $color; + } + @if type-of($amount) != 'number' { + @warn "lighten-by(): amount must be a number (e.g. 20, 20% or 0.2). Returning original color."; + @return $color; + } + + // 规范化为百分比单位(Sass 的 percent 类型) + $pct: $amount; + @if unit($amount) != '%' { + // 无单位数字:如果在 (0,1] 范围内,视为小数比例;否则当作百分比数值 + @if $amount > 0 and $amount <= 1 { + $pct: $amount * 100%; + } @else { + $pct: $amount * 1%; + } + } + + @return lighten($color, $pct); +} + +// ----------------------------- +// 常用组件/场景 mixin +// ----------------------------- + +// 简单的背景/文字组合,接收背景颜色或变量名 +// 用法:@include bg-fg('color-canvas-default'); // 传入变量名 +// @include bg-fg(#0d1117); // 传入 color 类型 +@mixin bg-fg($bg, $fg: null) { + @if type-of($bg) == 'string' { + // 假定传入的是变量名,使用 css-var + background: css-var($bg); + @if $fg == null { + // 无法静态计算对比,留空或用户自行指定 + color: inherit; + } else { + color: css-var($fg); + } + } @else if type-of($bg) == 'color' { + background: $bg; + @if $fg == null { + color: readable-text($bg); + } @else if type-of($fg) == 'color' { + color: $fg; + } @else { + color: css-var($fg); + } + } @else { + @warn "bg-fg(): unsupported bg type"; + } +} + +// 一个可重用的按钮样式 mixin,支持传入变量名或颜色 +// 用法: +// .btn { @include theme-button('color-accent-emphasis'); } +@mixin theme-button($bg, $fg: null, $radius: 6px, $pad-y: 8px, $pad-x: 12px) { + display: inline-flex; + align-items: center; + justify-content: center; + padding: $pad-y $pad-x; + border-radius: $radius; + border: none; + cursor: pointer; + @if type-of($bg) == 'string' { + background: css-var($bg); + @if $fg == null { color: css-var('color-fg-default'); } @else { color: css-var($fg); } + } @else if type-of($bg) == 'color' { + background: $bg; + @if $fg == null { color: readable-text($bg); } @else if type-of($fg) == 'color' { color: $fg; } @else { color: css-var($fg); } + } + // 微交互 + &:hover { filter: brightness(0.95); } + &:active { transform: translateY(1px); } +} + + +// ----------------------------- +// 示例(注释掉,直接拷贝到你的样式里使用) +// ----------------------------- + +// 示例主题 maps:键名与全局 CSS 变量中的命名保持一致(但不包含前缀 --) +// $theme-light: ( +// 'color-fg-default': #24292f, +// 'color-fg-muted': #57606a, +// 'color-canvas-default': #ffffff, +// 'color-border-default': #d0d7de, +// 'color-accent-fg': #0969da, +// ); +// +// $theme-dark: ( +// 'color-fg-default': #c9d1d9, +// 'color-fg-muted': #8b949e, +// 'color-canvas-default': #0d1117, +// 'color-border-default': #30363d, +// 'color-accent-fg': #58a6ff, +// ); +// +// 生成到 :root 和手动切换器: +// @include generate-theme(':root', $theme-light); +// @include generate-theme(':root[data-theme="dark"]', $theme-dark); +// +// 使用 CSS 变量: +// .markdown-body { color: css-var('color-fg-default'); background: css-var('color-canvas-default'); } +// +// 使用 mixin 快速为按钮应用主题颜色: +// .btn { @include theme-button('color-accent-emphasis'); } + +// ----------------------------- +// 结束 +// ----------------------------- diff --git a/packages/client/src/assets/styles/scss/_theme-helpers.scss b/packages/client/src/assets/styles/scss/_theme-helpers.scss new file mode 100644 index 0000000..19e3dcf --- /dev/null +++ b/packages/client/src/assets/styles/scss/_theme-helpers.scss @@ -0,0 +1,190 @@ + +// 返回一个 var(...) 字符串,方便在 SCSS 中使用 CSS 变量 +@function css-var($name, $fallback: null) { + @if $fallback == null { + @return unquote("var(--#{$name})"); + } @else { + @return unquote("var(--#{$name}, #{$fallback})"); + } +} + +// 将一个 map 转换为 CSS 变量声明,需在选择器块内使用 +// 用法: +// :root { @include declare-theme-variables($my-theme-map); } +@mixin declare-theme-variables($map) { + @each $token, $val in $map { + // 允许传入颜色、字符串或数字 + --#{$token}: #{$val}; + } +} + +// 生成主题选择器(selector 可以是 ":root"、":root[data-theme=\"dark\"]" 或 ".theme-dark") +// 用法:@include generate-theme(':root', $theme-light); +@mixin generate-theme($selector, $map) { + #{$selector} { + @include declare-theme-variables($map); + } +} + + +// ----------------------------- +// 颜色工具函数 +// ----------------------------- + +// 计算近似亮度(0-255)用于对比判定(基于 ITU BT.601 近似) +@function _luma($color) { + // 期望 $color 为 color 类型 + $r: red($color); + $g: green($color); + $b: blue($color); + @return ($r * 0.299) + ($g * 0.587) + ($b * 0.114); +} + +// 根据背景色返回可读的文字颜色(#000 或 #fff) +// 示例: color: readable-text(#0d1117); +@function readable-text($bg, $light: #ffffff, $dark: #000000) { + // 如果传入的不是 color 类型,尝试转换(如果是变量字符串则无法计算) + @if type-of($bg) != 'color' { + // 无法在构建时计算 CSS 变量的对比,默认返回白色以便在暗色环境下可读 + @return $light; + } + @if _luma($bg) > 186 { + @return $dark; + } + @return $light; +} + +// 基于 lighten/darken 的简单色调微调函数(正值变亮,负值变暗) +@function tone($color, $percent) { + @if type-of($color) != 'color' { + @warn "tone(): first argument is not a color; returned value will be unchanged."; + @return $color; + } + @if $percent == 0 { + @return $color; + } + @if $percent > 0 { + @return lighten($color, $percent); + } @else { + @return darken($color, abs($percent)); + } +} + +// 使颜色变浅的辅助函数 +// 用法: +// lighten-by(#0d1117, 20) -> 以 20% 变亮 +// lighten-by(#0d1117, 20%) -> 以 20% 变亮 +// lighten-by(#0d1117, 0.2) -> 以 20% 变亮(小数形式) +@function lighten-by($color, $amount) { + @if type-of($color) != 'color' { + @warn "lighten-by(): first argument is not a color; returned value will be unchanged."; + @return $color; + } + @if type-of($amount) != 'number' { + @warn "lighten-by(): amount must be a number (e.g. 20, 20% or 0.2). Returning original color."; + @return $color; + } + + // 规范化为百分比单位(Sass 的 percent 类型) + $pct: $amount; + @if unit($amount) != '%' { + // 无单位数字:如果在 (0,1] 范围内,视为小数比例;否则当作百分比数值 + @if $amount > 0 and $amount <= 1 { + $pct: $amount * 100%; + } @else { + $pct: $amount * 1%; + } + } + + @return lighten($color, $pct); +} + +// ----------------------------- +// 常用组件/场景 mixin +// ----------------------------- + +// 简单的背景/文字组合,接收背景颜色或变量名 +// 用法:@include bg-fg('color-canvas-default'); // 传入变量名 +// @include bg-fg(#0d1117); // 传入 color 类型 +@mixin bg-fg($bg, $fg: null) { + @if type-of($bg) == 'string' { + // 假定传入的是变量名,使用 css-var + background: css-var($bg); + @if $fg == null { + // 无法静态计算对比,留空或用户自行指定 + color: inherit; + } else { + color: css-var($fg); + } + } @else if type-of($bg) == 'color' { + background: $bg; + @if $fg == null { + color: readable-text($bg); + } @else if type-of($fg) == 'color' { + color: $fg; + } @else { + color: css-var($fg); + } + } @else { + @warn "bg-fg(): unsupported bg type"; + } +} + +// 一个可重用的按钮样式 mixin,支持传入变量名或颜色 +// 用法: +// .btn { @include theme-button('color-accent-emphasis'); } +@mixin theme-button($bg, $fg: null, $radius: 6px, $pad-y: 8px, $pad-x: 12px) { + display: inline-flex; + align-items: center; + justify-content: center; + padding: $pad-y $pad-x; + border-radius: $radius; + border: none; + cursor: pointer; + @if type-of($bg) == 'string' { + background: css-var($bg); + @if $fg == null { color: css-var('color-fg-default'); } @else { color: css-var($fg); } + } @else if type-of($bg) == 'color' { + background: $bg; + @if $fg == null { color: readable-text($bg); } @else if type-of($fg) == 'color' { color: $fg; } @else { color: css-var($fg); } + } + // 微交互 + &:hover { filter: brightness(0.95); } + &:active { transform: translateY(1px); } +} + + +// ----------------------------- +// 示例(注释掉,直接拷贝到你的样式里使用) +// ----------------------------- + +// 示例主题 maps:键名与全局 CSS 变量中的命名保持一致(但不包含前缀 --) +// $theme-light: ( +// 'color-fg-default': #24292f, +// 'color-fg-muted': #57606a, +// 'color-canvas-default': #ffffff, +// 'color-border-default': #d0d7de, +// 'color-accent-fg': #0969da, +// ); +// +// $theme-dark: ( +// 'color-fg-default': #c9d1d9, +// 'color-fg-muted': #8b949e, +// 'color-canvas-default': #0d1117, +// 'color-border-default': #30363d, +// 'color-accent-fg': #58a6ff, +// ); +// +// 生成到 :root 和手动切换器: +// @include generate-theme(':root', $theme-light); +// @include generate-theme(':root[data-theme="dark"]', $theme-dark); +// +// 使用 CSS 变量: +// .markdown-body { color: css-var('color-fg-default'); background: css-var('color-canvas-default'); } +// +// 使用 mixin 快速为按钮应用主题颜色: +// .btn { @include theme-button('color-accent-emphasis'); } + +// ----------------------------- +// 结束 +// ----------------------------- diff --git a/packages/client/src/assets/styles/scss/common.scss b/packages/client/src/assets/styles/scss/common.scss new file mode 100644 index 0000000..c09dff8 --- /dev/null +++ b/packages/client/src/assets/styles/scss/common.scss @@ -0,0 +1,60 @@ +html, +body { + height: 100%; +} + +/* 全局主题变量(使用 _theme-helpers.scss 中的 mixin/map) + - 在 :root 中生成默认亮色主题变量 + - 支持手动切换(data-theme="dark" / .theme-dark) + - 保留 prefers-color-scheme 媒体查询用于自动切换 +*/ + +// 亮色主题变量 map(键不带 -- 前缀) +$theme-light: ( + "color-fg-default": #24292f, + "color-fg-muted": #57606a, + "color-fg-subtle": #6e7781, + "color-canvas-default": #ffffff, + "color-canvas-subtle": #f6f8fa, + "color-border-default": #d0d7de, + "color-border-muted": hsla(210, 18%, 87%, 1), + "color-neutral-muted": rgba(175, 184, 193, 0.2), + "color-accent-fg": #0969da, + "color-accent-emphasis": #0969da, + "color-attention-subtle": #fff8c5, + "color-danger-fg": #cf222e, + "color-mark-default": rgb(255, 255, 0), + "color-mark-fg": rgb(255, 187, 0), +); + +// 暗色主题变量 map(对应亮色变量的语义) +$theme-dark: ( + "color-fg-default": #c9d1d9, + "color-fg-muted": #8b949e, + "color-fg-subtle": #6e7681, + "color-canvas-default": #0d1117, + "color-canvas-subtle": #010409, + "color-border-default": #30363d, + "color-border-muted": hsla(210, 18%, 20%, 1), + "color-neutral-muted": rgba(175, 184, 193, 0.12), + "color-accent-fg": #58a6ff, + "color-accent-emphasis": #2389ff, + "color-attention-subtle": rgba(255, 214, 10, 0.07), + "color-danger-fg": #ff7b72, + "color-mark-default": rgb(255, 214, 10), + "color-mark-fg": rgb(255, 165, 0), +); + +// 在 :root 中生成默认(亮色)变量,便于组件直接使用 css var +@include generate-theme(":root", $theme-light); + +// 手动主题切换支持:data-theme 或 class +@include generate-theme(':root[data-theme="dark"]', $theme-dark); +@include generate-theme(".theme-dark", $theme-dark); + +#app { + height: 100%; + background-color: css-var(color-canvas-default); + color: css-var(color-fg-default); + line-height: 1.2; +} diff --git a/packages/client/src/entry-client.ts b/packages/client/src/entry-client.ts index 10f7d3d..5984435 100644 --- a/packages/client/src/entry-client.ts +++ b/packages/client/src/entry-client.ts @@ -5,6 +5,8 @@ import { createHead } from '@unhead/vue/client' import "@/assets/styles/css/reset.css" import 'vue-final-modal/style.css' +import "@/assets/styles/scss/common.scss" + import { MazUi } from 'maz-ui/plugins/maz-ui' import { mazUi, ocean, pristine, obsidian } from '@maz-ui/themes' import { zhCN } from '@maz-ui/translations' diff --git a/packages/client/src/pages/index.vue b/packages/client/src/pages/index.vue index 42643a0..f7ce484 100644 --- a/packages/client/src/pages/index.vue +++ b/packages/client/src/pages/index.vue @@ -1,7 +1,3 @@ - - + + + + diff --git a/packages/client/vite.config.ts b/packages/client/vite.config.ts index e285888..f6862c2 100644 --- a/packages/client/vite.config.ts +++ b/packages/client/vite.config.ts @@ -31,6 +31,13 @@ export default defineConfig({ ssr: { noExternal: process.env.NODE_ENV === 'development' ? ['vue-router'] : [] }, + css: { + preprocessorOptions: { + "scss": { + additionalData: `@use "@/assets/styles/scss/_global.scss" as *;\n` + } + } + }, plugins: [ devtoolsJson(), VueRouter({