Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions plugins/color-helper/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ npm-debug.log*

# TypeScript
*.tsbuildinfo

# 本地工作文档(不入库)
docs/
187 changes: 131 additions & 56 deletions plugins/color-helper/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React, { Component, lazy, Suspense } from 'react';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
Expand All @@ -16,16 +16,24 @@ import ViewCompactIcon from '@mui/icons-material/ViewCompact';
import BrushIcon from '@mui/icons-material/Brush';
import GradientIcon from '@mui/icons-material/Gradient';
import StarIcon from '@mui/icons-material/Star';
import StarBorderIcon from '@mui/icons-material/StarBorder';
import ColorizeIcon from '@mui/icons-material/Colorize';
import chroma from 'chroma-js';
import ColorPage from './pages/ColorPage';
import UIPalettesPage from './pages/UIPalettesPage';
import TraditionalColorsPage from './pages/TraditionalColorsPage';
import GradientsPage from './pages/GradientsPage';
import ImagePalettePage from './pages/ImagePalettePage';
import CollectColorsPage from './pages/CollectColorsPage';
import AIPalettePage from './pages/AIPalettePage';
import { copyText, dbStorage, screenColorPick, onPluginEnter, onPluginOut, isPlatform } from './utils/platform';
import { copyText, db, dbStorage, screenColorPick, onPluginEnter, onPluginOut, isPlatform } from './utils/platform';
import { collectDocId, saveCollectedColor } from './utils/collect';

// 代码分割试点: 只拆「单次使用 + 自带大块资源」的页面
// - GradientsPage: 渐变数据 4.05 kB gzip + 代码 17.71 kB
// - ImagePalettePage: 4 张封面图 107 kB + quantize.ts
// - AIPalettePage: 色轮 UI(iro 仅剩样式与 ui 常量, 逻辑已在首屏共用 chunk)
// 其余页面体积小且首屏常用, 保持静态导入
const GradientsPage = lazy(() => import('./pages/GradientsPage'));
const ImagePalettePage = lazy(() => import('./pages/ImagePalettePage'));
const AIPalettePage = lazy(() => import('./pages/AIPalettePage'));

/**
* App - 核心架构: 状态驱动导航(this.state.nav) + 平台生命周期
Expand All @@ -48,62 +56,104 @@ function colorFromBackgroundColor(bgColor: string, setting: boolean): string | n
}
}

// 导航项
const navItems = [
/**
* 侧边栏导航项
*/
export const navItems = [
{ key: "color", label: "颜色", icon: <PaletteIcon /> },
{ key: "ai", label: "AI 配色", icon: <AutoAwesomeIcon /> },
{ key: "ui", label: "UI 色卡", icon: <ViewCompactIcon /> },
{ key: "image", label: "图片色卡", icon: <ImageIcon /> },
{ key: "traditional", label: "传统色", icon: <BrushIcon /> },
{ key: "gradient", label: "渐变色", icon: <GradientIcon /> },
{ key: "image", label: "图片色卡", icon: <ImageIcon /> },
{ key: "collect", label: "收藏颜色", icon: <StarIcon /> },
{ key: "ai", label: "AI 配色", icon: <AutoAwesomeIcon /> },
];

interface AppState {
nav: string;
colorValue: (string | null)[];
openMessage: boolean;
messageData: { key: number; color: string; body?: string };
messageData: { key: number; color: string; text: string };
setting: boolean;
imagePayload: string | null;
/** 收藏页在 constructor 里一次性读库, 从提示条收藏成功后递增它重挂载刷新列表 */
collectVersion: number;
}

class App extends Component<{}, AppState> {
private imageEnterPayload: any = null;

state: AppState = {
nav: "",
colorValue: [],
openMessage: false,
messageData: { key: 0, color: "" },
messageData: { key: 0, color: "", text: "" },
setting: false,
imagePayload: null,
collectVersion: 0,
};

handleNavChange = (nav: string) => () => {
(document.activeElement as HTMLElement)?.blur();
this.setState({ nav });
};

handleColorClick = (eventOrColor: any) => {
/**
* 复制色值并弹出带操作的 snackbar
*
* `copied` 是真正写进剪贴板、也是提示条要原样回显的文本; `color` 是同一个颜色
* 解析出来的 hex, 只用于定位 收藏 / 查看 / 配色 三个操作指向哪个颜色。
*
* 颜色页各格式的复制按钮传 (原文, hex): rgb/hsl/hsv/hsi/cmyk/lab 串 chroma
* 解析不了, hex 必须由调用方一并给出。其余各页色块只传 DOM 事件或色值串。
*/
handleColorClick = (eventOrColor: any, knownHex?: string) => {
let color: string | null = null;
let copied: string | null = null;

if (eventOrColor?.currentTarget?.style) {
if (knownHex && typeof eventOrColor === 'string') {
// 颜色页各格式的复制: 原文 chroma 解析不了, hex 由调用方给出
color = knownHex;
copied = eventOrColor;
} else if (eventOrColor?.currentTarget?.style) {
color = colorFromBackgroundColor(eventOrColor.currentTarget.style.backgroundColor, this.state.setting);
copied = color;
} else if (typeof eventOrColor === 'string') {
try {
const c = chroma(eventOrColor);
color = this.state.setting ? c.hex().substring(1) : c.hex();
color = c.hex();
copied = eventOrColor;
} catch {}
}

if (color) {
copyText(color);
// 「色值去 #」只作用于复制出去的文本: 各格式串里没有 #, 去掉是空操作
if (copied && this.state.setting) copied = copied.replace(/#/g, '');

if (color && copied) {
copyText(copied);
this.setState({
messageData: { color, key: Date.now() },
messageData: { color, key: Date.now(), text: `已复制 "${copied}"` },
openMessage: true,
});
}
};

handleCollectColor = () => {
const { color } = this.state.messageData;
if (!color || !!db.get(collectDocId(color))) return;
if (!saveCollectedColor(color)) {
// 走到这里说明色值非法或 db 写入失败(已收藏的情况上面已拦下):
// 保留提示条上的操作按钮, 只换提示文本
this.setState({ messageData: { ...this.state.messageData, text: "收藏失败,请重试" } });
return;
}
this.setState({ collectVersion: this.state.collectVersion + 1 });
this.showMessage(`已收藏 "${color}",可在「收藏颜色」中查看`);
};

handleViewCollectedColors = () => {
this.setState({ nav: "collect", openMessage: false });
};

handleViewColorInfo = () => {
if (this.state.messageData.color) {
this.setState({ nav: "color", colorValue: [this.state.messageData.color], openMessage: false });
Expand All @@ -122,7 +172,7 @@ class App extends Component<{}, AppState> {
};

showMessage = (msg: string) => {
this.setState({ messageData: { key: Date.now(), color: "", body: msg }, openMessage: true });
this.setState({ messageData: { key: Date.now(), color: "", text: msg }, openMessage: true });
};

handleSettingCheckboxChange = (e: any) => {
Expand All @@ -140,28 +190,25 @@ class App extends Component<{}, AppState> {
const setting = !!dbStorage.getItem("setting");

if (code === "image") {
let imagePayload: string | null = null;
if (type === "img") {
this.imageEnterPayload = payload;
imagePayload = payload;
} else if (type === "files") {
this.imageEnterPayload = payload?.[0]?.path;
} else {
this.imageEnterPayload = null;
}
} else {
if (code === "pickercolor") {
screenColorPick(({ hex }) => {
this.setState({ nav: "color", colorValue: [hex], setting });
});
return;
}
if (code === "color" && type === "regex") {
this.setState({ nav: "color", colorValue: [payload], setting });
return;
}
if (code === "ai" && type === "regex") {
this.setState({ nav: "ai", colorValue: [payload], setting });
return;
imagePayload = payload?.[0]?.path || null;
}
this.setState({ nav: "image", imagePayload, setting });
return;
}

if (code === "pickercolor") {
screenColorPick(({ hex }) => {
this.setState({ nav: "color", colorValue: [hex], setting });
});
return;
}
if (code === "color" && type === "regex") {
this.setState({ nav: "color", colorValue: [payload], setting });
return;
}

this.setState({ nav: code, setting });
Expand All @@ -183,7 +230,9 @@ class App extends Component<{}, AppState> {
}

render() {
const { nav, colorValue, openMessage, messageData, setting } = this.state;
const { nav, colorValue, openMessage, messageData, setting, imagePayload, collectVersion } = this.state;
// 星标状态以 db 实时为准: 提示条跨页面存活, 期间收藏可能被增删
const favorited = !!messageData.color && !!db.get(collectDocId(messageData.color));

// 页面内容
let pageContent: React.ReactNode;
Expand All @@ -198,13 +247,13 @@ class App extends Component<{}, AppState> {
pageContent = <TraditionalColorsPage onColorClick={this.handleColorClick} />;
break;
case "gradient":
pageContent = <GradientsPage onColorClick={this.handleColorClick} />;
pageContent = <GradientsPage onColorClick={this.handleColorClick} showMessage={this.showMessage} />;
break;
case "image":
pageContent = <ImagePalettePage onColorClick={this.handleColorClick} />;
pageContent = <ImagePalettePage onColorClick={this.handleColorClick} initialImage={imagePayload} />;
break;
case "collect":
pageContent = <CollectColorsPage onColorClick={this.handleColorClick} />;
pageContent = <CollectColorsPage onColorClick={this.handleColorClick} key={collectVersion} />;
break;
case "ai":
pageContent = <AIPalettePage value={colorValue} onColorClick={this.handleColorClick} setting={setting} showMessage={this.showMessage} key={colorValue?.[0]} />;
Expand Down Expand Up @@ -246,16 +295,54 @@ class App extends Component<{}, AppState> {
</Tooltip>
</div>
<div className="app-content">
{pageContent}
<Suspense fallback={<div className="page-loading" />}>
{pageContent}
</Suspense>
</div>
<Snackbar
anchorOrigin={{ horizontal: 'right', vertical: 'top' }}
open={openMessage}
autoHideDuration={3000}
onClose={this.handleSnackbarClose}
message={messageData.color ? `已复制 "${messageData.color}"` : messageData.body}
// pre-line: 复制 rgb/hsl 时提示要给色值换行, 默认 white-space 会把换行折成空格
message={<span style={{ whiteSpace: 'pre-line' }}>{messageData.text}</span>}
action={messageData.color ? (
<>
{/* 「查看」的去处是颜色页, 已经在颜色页上时点下去只会把同一个色
再追加成色板里的重复色块, 所以不显示。按当前页面判断:
提示条跨页面存活, 弹出时所在的页面说明不了现在 */}
{nav !== "color" && (
<Button
disableFocusRipple
tabIndex={-1}
startIcon={<PaletteIcon />}
style={{ marginRight: '10px' }}
variant="contained"
color="primary"
size="small"
onClick={this.handleViewColorInfo}
>
查看
</Button>
)}
<Tooltip
disableFocusListener
placement="top"
title={favorited ? '已收藏,点此查看收藏列表' : '收藏这个颜色'}
>
<Button
disableFocusRipple
tabIndex={-1}
startIcon={favorited ? <StarIcon /> : <StarBorderIcon />}
style={{ marginRight: '10px' }}
variant="contained"
color={favorited ? 'success' : 'primary'}
size="small"
onClick={favorited ? this.handleViewCollectedColors : this.handleCollectColor}
>
收藏
</Button>
</Tooltip>
<Button
disableFocusRipple
tabIndex={-1}
Expand All @@ -268,18 +355,6 @@ class App extends Component<{}, AppState> {
>
配色
</Button>
<Button
disableFocusRipple
tabIndex={-1}
startIcon={<PaletteIcon />}
style={{ marginRight: '10px' }}
variant="contained"
color="primary"
size="small"
onClick={this.handleViewColorInfo}
>
查看
</Button>
</>
) : undefined}
key={messageData.key}
Expand Down
41 changes: 37 additions & 4 deletions plugins/color-helper/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,45 @@
# Changelog

## 1.1.0 - 2026-10-07

## 1.0.0 - 2026-06-09
### 修复

- 修复 `tsconfig.json` 的 `include` 路径,让 `tsc` 真正检查代码
- 修复渐变导出图片与预览不一致(渐变线长度算错)
- 统一明暗判定判据(原先混用三套阈值),修复传统色部分色块文字对比度不足
- 修复颜色输入识别:正则误判普通单词为颜色,主输入框里大写色值不匹配
- 修复 AI 调用硬编码了宿主解析不出的模型 ID,导致总是报「未找到 AI 模型配置」
- 修复图片色卡无法通过 `img` / `files` 命令带入图片
- 修复收藏颜色删除后残留索引、单个文档损坏导致整批丢失
- 修复剪贴板回退逻辑从未生效(接不住 Promise reject)
- 修复两处整页白屏:懒加载页面加载失败、图标库在 vite 8 下不兼容
- 修复提示条收藏:半透明色(8 位 hex)不再被误判为非法色值、失败时给出反馈、在收藏页上收藏后列表立即刷新;「查看」按钮与收藏星标不再跨页面显示旧状态
- 颜色页各格式复制按钮的 Tooltip 与实际复制的完整色值一致

### 变更

- 移除 AI 配色的颜色正则:与「颜色」完全重复,粘色值时只会多出冗余候选,只留「AI 配色」文本命令
- 侧边栏顺序与 `plugin.json` 的 features 对齐,AI 配色移到末位
- 颜色页各格式复制按钮改为走统一提示条,原样回显复制文本、剪贴板为该格式完整色值(原先一律变回 hex);在颜色页上不显示「查看」
- 渐变页复制增加反馈,浏览器下降级为下载 PNG
- 渐变色 / 图片色卡 / AI 配色改为懒加载
- 图片色卡四个版式重做,色值内嵌色块、字号按槽位自适应
- 构建链与依赖升级:vite 8 + vitest 4、Node 22、chroma-js 3、MUI 7、React 19,锁定 pnpm 版本,`pnpm audit` 归零
- 全库注释清理:删除上游残留、复述与叙事式、过期注释,为非直观处补「为什么」说明

### 新增

- 颜色助手:颜色、AI 配色、图片色卡、UI 色卡、传统色、渐变色、收藏颜色
- 提示条上增加「收藏」(已收藏显示星标,点击跳到收藏页)
- 引入 vitest 测试基建(`pnpm test` / `pnpm run test:run`)
- 抽出 `utils/gradient.ts`、`utils/collect.ts`(统一收藏颜色的文档形状与写入,供提示条与收藏页共用)
- `window.platform` 类型声明逐条对齐官方 `ztools.api.d.ts`

### 移除

### 根据提交记录补充
- 移除死代码与无用文件:`package-lock.json`、`react-router-dom`、`utils/storage.ts`、`context/AppContext.tsx`、`plugin.json` 的 `pluginName` 字段、quantize 死条件 `maxColors > 256`、`App.tsx` 无入口的 AI 配色 regex 分支与透传复制回调

- 修复代码审查反馈的问题。
## 1.0.0 - 2026-06-09

### 新增

- 颜色助手:颜色、AI 配色、图片色卡、UI 色卡、传统色、渐变色、收藏颜色
Loading
Loading