From b94114513b6ed068029b0cee3eb4d067f08550b2 Mon Sep 17 00:00:00 2001 From: dash <1549469775@qq.com> Date: Sat, 11 Oct 2025 01:29:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=B8=BB=E9=A2=98?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=92=8C=E5=85=A8=E5=B1=80=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=EF=BC=8C=E5=8C=85=E5=90=AB=E4=BA=AE=E8=89=B2=E5=92=8C=E6=9A=97?= =?UTF-8?q?=E8=89=B2=E4=B8=BB=E9=A2=98=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.lockb | Bin 167363 -> 185946 bytes packages/client/components.d.ts | 2 - packages/client/package.json | 1 + packages/client/src/App.vue | 2 - .../client/src/assets/styles/scss/_global.scss | 206 +++++++++++++++++++++ .../src/assets/styles/scss/_theme-helpers.scss | 190 +++++++++++++++++++ packages/client/src/assets/styles/scss/common.scss | 60 ++++++ packages/client/src/entry-client.ts | 2 + packages/client/src/pages/index.vue | 53 ++++-- packages/client/vite.config.ts | 7 + 10 files changed, 501 insertions(+), 22 deletions(-) create mode 100644 packages/client/src/assets/styles/scss/_global.scss create mode 100644 packages/client/src/assets/styles/scss/_theme-helpers.scss create mode 100644 packages/client/src/assets/styles/scss/common.scss diff --git a/bun.lockb b/bun.lockb index b602fbb90e2b8f6bb582916b1202501518ac3315..b7188fe5f95347c37f5c42a061ae4d25d3de0581 100644 GIT binary patch delta 15660 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 FKL8EK13mx% delta 3727 zcmeH~YfzL`9LCRiQ-KAQK;@>ai&jI-sAUlfp{SIGl~M*yUQn^79Pm=(peD-%rc61= zyy8j0TVCVlA~;KGGpLn*sL4VyHB3<_O>KyozSyWe&+gwDobf|_Z9KETea?B#|D5-- z?|ILE&(;PXt8}az7N2)~Xkuj6@Y>lw**8{yl(aYIP-?-^cSbwIN|X1Oe~~kM8_ z4jS#7piMeXO9xfko3}cBr*~akDi3#4+7D88{UQBS#&?Q#K?BjgCH>u%*5Be&d!el@ z73aHt3$t*b1KI}~31w^)XSZi=i*WPmW7!gY1nQFZztK>{KZSS>87bqsE9XlTpUP?y zzIYa+EamnY$uJn0Dgk39APvewhqm(?p0Gb?h;%c0gADmE%2V7ZlZPS*eGjzRiW$P@x&$}#Jx4MkyO+Cdsu~1iKIy^O86G|1; zo116!==3{x{e_;5F81#7hxpojr;xqkRY(lqO`nUm!7M(j!`()@xUHN5ET4+UE!X%Q zye`EHKzk|YM%g4Dx3Hn&Rf@;A{Tb9*s>DMsq<)e5X1~r-+^K$-z%9}l0*@&V=iCbA zdG`i&68D+(aqCYKZzq&hrbB7s?J{%D>G0=wOV^|5x(!(Nh{x^aF5oMlw~L1i2c3ZT zq}nuK3irLU=@085V5yfW2B3b(=P0#LJZ^HefVc2o(iVyOy$zI!8$K!uEC($6Ves(L z;3PXyn*4EY6@vx?cpDB#;6T);q0Zaj^#`g!s82zix1m|$Vo?u3o!5U*JSXZr1781O zC_ZX1=mPz~moRuwT%gPvi*FRBm-whSHc;WjN5vb0`Wrlb6sDhy^Y9j-v>Y(QuIM;3 zxm~B|ji$6+k2fdV^+a7~qR#7#DDIPc!6?8zbSKyb7MhFawbRXPf*D9Im=E3p@qo`< z0vG~Z;7Je%9s`3wG>8EMK~KQv@f>Ie7eNQO46Xt`f+xTnobFtZ3vvJ+IZM@>Rln-v zSeY+h&@tN0?Ysumg1<0^DM1ye1WX^8|3c<_RyGSxU#CV;UZ7C6CR5DY>gIQ}f$O7*n^nK8V@Sn}KOL}-v4(u7=v~gY5&!zE2@f+aE8_`bVe_4({=rNk7z#6H_^vsJ{5Yk#Xqd0m+9)%k8_we;!nLe z-sC1&p(dwGJG_UZt-yg+bFt}@Vm)oC5;G3!7!iP@qId#LVTv`udm_au(vAy(em7yQ ZH#61RS!|N#TK&w3GAqm*(`Jp){{ZBz!>j-R diff --git a/packages/client/components.d.ts b/packages/client/components.d.ts index cb7b8d4..613e14f 100644 --- a/packages/client/components.d.ts +++ b/packages/client/components.d.ts @@ -9,12 +9,10 @@ export {} declare module 'vue' { export interface GlobalComponents { AiDemo: typeof import('./src/components/AiDemo/index.vue')['default'] - AXBubble: typeof import('ant-design-x-vue')['Bubble'] ClientOnly: typeof import('./../../internal/x/components/ClientOnly.vue')['default'] 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 ca92079..35ef609 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 1949a3d..d3fb4e7 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({