【UXP教程-5】插件面板实战-界面布局和功能开发

【UXP教程-5】插件面板实战-界面布局和功能开发

大家好,教程更新割了整整一年之后,我又回来了。现在是2025年春节假期期间,实在有点闲着无聊,就打算把我的CEP插件迁移到UXP,顺便把教程更新一下。从昨天开始重新去翻官方的文档,发现一年多过去了,UXP的功能支持没有什么新的变化,文档也没有什么新的内容,Adobe爸爸的更新速度真是让人捉急。

今天借着把我的切图插件从CEP迁移到UXP的契机,给大家逐步介绍一下我的整个UXP面板开发过程,期间我会穿插一些UXP的关键开发知识,让大家能够更加系统地了解UXP的开发。

1. 项目框架选择

虽然我前面的文章有写过UXP的一些开发框架选型,比如VUE,React,Webpack, Vite等,我最初选择的还是我自己从0搭建的ReactJs + Webpack + typescript的组合模式,但是昨天我决定重新开始,并想到这么多年过去了,市面上应该有一些大佬搞出来了更好的解决方案,于是我决定去找一找,还真的让我看到了一个蛮不错的解决方案bolt-uxp

bolt-uxp

这是一个开箱即用的UXP项目框架,你可以选择自己喜欢的诸如Svelte, React, Vue等框架,它集成了Vite + Typescript,非常适合我这种无TS不欢的开发者。使用的方法大家参考官方文档就好了,这里就不多说了。我由于习惯了使用React,所以选择了React作为项目框架,大家习惯用Vue的就选择Vue就好了,区别不大,下图是我用它启动的工程项目

bolt-uxp-project

2. UXP的主题适配

由于Ps有4个颜色主题,以前在做CEP开发的时候,我们通过监听Ps的主题变化,获取背景颜色来动态设置面板的背景颜色。但是到UXP就不用这么复杂了,官方提供了几个内置的CSS颜色变量,直接用就可以了。

uxp-theme

1
2
3
4
5
6
body {
background-color: var(--uxp-host-background-color);
color: var(--uxp-host-text-color);
border-color: var(--uxp-host-border-color);
font-size: var(--uxp-host-font-size);
}

它不仅仅只有上面几个颜色,还有其它一些包括字体大小等变量,大家参考官方文档就好。对应的颜色效果如下:

fonts-darkest

fonts-lightest

使用这些颜色好处是它可以和Ps的主题颜色融合在一起,让面板的视觉效果看起来更专业,还有的就是它可以自动随着Ps的主题变化和切换,不需要自己去处理不同主题下的颜色问题,非常省心。

3. Spectrum UXP兼容

Spectrum UXP是Adobe自己搞了一套UI的控件,它和传统的web控件不兼容,并且控件的数量也有限,不过它由于是内置的,渲染性能比较好,我关注到Adobe目前正在打算逐步迁移到**Spectrum Web Component
**,它应该会是下一代的UI控件库,目前已经部分可用了。

当你使用Typescript的时候,在使用sp-button这样的控件就会报错,提示找不到sp-button,这是因为Spectrum UXP的控件库还没有完全支持Typescript,所以需要手动去定义这些控件,比如sp-button的类型定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
declare global {
namespace JSX {
interface IntrinsicElements {
'sp-button': {
children?: React.ReactNode;
ref?: React.RefObject<HTMLElement>;
class?: string;
disabled?: boolean;
quiet?: boolean;
variant?: Spectrum.ButtonVariant;
};
}
}
}

这样每个控件都定义一遍也挺繁琐的,所以第一个想法就是去看看是不是别人已经帮你写好了……于是我找到了这个项目react-uxp-spectrum,它已经帮我们定义好了所有Spectrum UXP控件的类型,我们只需要安装它,然后就可以直接使用了。

1
npm install react-uxp-spectrum

并且它用React将这些控件包装了一遍,用起来就和传统的React组件一样,非常方便。不过它也有一些缺点就是它的属性只对标官方文档,但是官方文档其实并不完整,很多本来支持的属性并没有在文档中写出来,是的这个库也有部分的属性是缺失的,我后续准备fork一个出来补充一下。

1
2
3
4
5
<Textfield className='email' placeholder='请输入邮箱账号' value={email} onChange={(e) => setEmail(e.target!.value)} />
<Textfield className='password' type='password' placeholder='请输入密码' value={password} onChange={(e) => setPassword(e.target!.value)} />
<Button variant='cta' className='login-btn' disabled={loading} onClick={onSubmit}>
{loading ? '登录中...' : '登录'}
</Button>

4. 登录页面

基于这个脚手架,使用React就能很快的写出来一个登录页面,并且使用Spectrum UXP控件,效果如下:

login-page

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import * as React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUser, faLock } from '@fortawesome/free-solid-svg-icons';
import '../assets/css/login.scss';
import { Button, Link, Textfield } from 'react-uxp-spectrum';
import { login, User } from '../lib/http';
import { writeFile } from '../lib/storage';

export default function Login({ onLogin }: { onLogin: (user: User) => void }) {
const [email, setEmail] = React.useState('');
const [password, setPassword] = React.useState('');
const [error, setError] = React.useState('');
const [success, setSuccess] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const buttonRef = React.useRef<HTMLElement>(null);

React.useEffect(() => {
const button = buttonRef.current;
if (button) {
button.addEventListener('click', onSubmit);
return () => button.removeEventListener('click', onSubmit);
}
}, []);

const validateEmail = (email: string) => {
return email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
};

const onSubmit = async () => {
try {
setError('');
setLoading(true);

if (!email || !password) {
setError('请输入邮箱和密码');
return;
}

if (!validateEmail(email)) {
setError('请输入有效的邮箱地址');
return;
}

const response = await login(email, password);

if (response.errno !== 0) {
setError(response.info || '登录失败,请重试');
return;
}

await writeFile('user.json', JSON.stringify(response));

// 登录成功后的处理
setSuccess(true);
setTimeout(() => {
onLogin(response);
}, 1500);

} catch (err) {
setError('登录失败,请稍后重试');
console.error('登录错误:', err);
} finally {
setLoading(false);
}
};

return (
<div className='login'>
<div className='avatar'>
<FontAwesomeIcon icon={faUser} size={'4x'} />
</div>
<div className='title'>账号登录</div>
<Textfield className='email' placeholder='请输入邮箱账号' value={email} onChange={(e) => setEmail(e.target!.value)} />
<Textfield className='password' type='password' placeholder='请输入密码' value={password} onChange={(e) => setPassword(e.target!.value)} />
<Button variant='cta' className='login-btn' disabled={loading} onClick={onSubmit}>
{loading ? '登录中...' : '登录'}
</Button>
{error && <div className='error-message'>{error}</div>}
{success && <div className='success-message'>登录成功</div>}
<div className='link-box'>
<Link variant='overBackground'>注册账号</Link>
<span>|</span>
<Link variant='overBackground'>忘记密码</Link>
</div>
</div>
)
}

有了脚手架的加持,写一个页面布局还是比较简单的,就是uxp的控件能够支持的样式定义比较受限,不是所有的效果都能实现,只能尽量匹配系统主题的控件样式。

哦,如果你们还没有使用过Cursor编辑器的话,我这里强烈推荐一下,基本上我现在60%的代码,都是靠AI来写的,效率提升很多。

好了,今天的这篇文章就到这里,内容不是很多,下一篇会继续主面板的功能开发,然后给大家介绍一下UXP的本地目录存储和相关的操作,敬请期待。

评论