26. 删除排序数组中的重复项

LeetCode

描述

给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

注意

  • 原地 删除重复出现的元素,表示必须在原数组上操作
  • 方法返回的是一个长度,表示过滤后的个数,但并不代表是过滤后的数组长度。
  • 1
    2
    3
    4
    5
    //给定 nums = [0,0,1,1,1,2,2,3,3,4],

    //函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。

    //你不需要考虑数组中超出新长度后面的元素。
  • 为什么返回数值是整数,但输出的答案是数组呢?
    输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
    1
    2
    3
    4
    5
    6
    7
    8
    // nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
    int len = removeDuplicates(nums);

    // 在函数里修改输入数组对于调用者是可见的。
    // 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
    for (int i = 0; i < len; i++) {
        print(nums[i]);
    }
    所以,这就是为什么返回的是一个长度,判别结果是一个数组的原因。
    下面这中写法由于没有修改原数组所以错误:
    1
    2
    3
    var removeDuplicates = function (nums) {
    return nums.filter((num, index) => index === nums.indexOf(num)).length;
    };

1.暴力解法逐个删除

  • 正向逐位依次和下一位比较,如果相等把当前位删除,因为必须在原数组上操作,所以是使用splice方法。
  • 因为使用 splice 方法对元素组删除,所以正向比较时需要注意数组的长度,如果删除了数组项,数组长度需要减1。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
 var removeDuplicates = function (nums) {
var len = nums.length-1,
i=0;
for (; i <len; i++) {
if (nums[i] === nums[i + 1]) {
nums.splice(i, 1);
// 因为splice改变了数组的长度,所以数组长度需要减1
len--;
// 在删除了数组项之后,下次还需要在当前为比较
i--
}
}
return nums.length;
};
  • 根据上面注释可以换一种写法
1
2
3
4
5
6
7
8
9
10
11
12
13
var removeDuplicates = function (nums) {
var len = nums.length-1,
i=0;
for (; i <len;) {
if (nums[i] === nums[i + 1]) {
nums.splice(i, 1);
len--;
}else{
i++
}
}
return nums.length;
};
  • 因为正向遍历需要考虑删除数组项对长度的影响,所以考虑反向遍历。
1
2
3
4
5
6
7
8
9
var removeDuplicates = function (nums) {
var i = nums.length - 1;
for (; i > 0; i--) {
if (nums[i] === nums[i - 1]) {
nums.splice(i, 1)
}
}
return nums.length;
};

2.双指针

  • 注意到最终结果的生成方式,是用返回的数组长度(length)遍历原数组,所以原数组不需要完全是过滤后的结果,只需要前length项是过滤后的结果即可。
  • 使用 i , j 两个指针,i指针表示过滤后的数组索引,j表示遍历时的索引
  • 如果 nums[i]!==nums[j], j指针向后移动,继续遍历,如果nums[i]===nums[j], i之后向后移动,并且要把nums[j]赋值给nums[i+1];
1
2
3
4
5
6
7
8
9
10
11
12
13
var removeDuplicates = function (nums) {
var i = 0,
j = 1,
len = nums.length;
for (; j < len; j++) {
if (nums[i] !== nums[j]) {
i++;
nums[i] = nums[j]
}
}
// 因为i是索引,最后要返回长度所以+1
return i + 1;
};

21.合并两个有序链表

LeetCode

1.暴力解题迭代

需要的数据结构

  • 返回结果为合并后的链表,所以需要一个链表保存合并后的结果,prehead={next:null}
  • 在链表合并的时候需要知道在什么位置插入节点,所以需要一个指针 prev={} 指向当前插入位置的节点
  • 最后需要l1,l2两个合并的链表

注意

  • 链表可能为空即: l1=null

思路

  • 需要一个占位节点,即:prehead={val:-1,next:null}, l1,l2,prev=prehead,都指向数据中的第一个节点

  • 比较l1,l2当前节点的值,把prehead.next指向值小的节点,同时把prehead = prehead.next,l2 = l2.next的指针移动到下一个节点,用于下一次比较

  • 依次比较直到链表l1.next===null, l2.next===null

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
var mergeTwoLists = function (l1, l2) {
var prehead = {
next: null
};
var prev = prehead
while (l1 || l2) {
if (l1 && l2) {
if (l1.val <= l2.val) {
prev.next = l1;
prev = prev.next;
l1 = l1.next;
} else {
prev.next = l2;
prev = prev.next;
l2 = l2.next;
}
}
if (!l1 && l2) {
prev.next = l2;
prev = prev.next;
l2 = l2.next;
}
if (!l2 && l1) {
prev.next = l1;
prev = prev.next;
l1 = l1.next;
}
}
return prehead.next;
};

2.优化迭代

  • 在比较的过程中,l1,l2中最多有一个会先为空。由于输入的两个链表都是有序的,所以不管哪个链表是非空的,它包含的所有元素都比前面已经合并链表中的所有元素都要大。这意味着我们只需要简单地将非空链表接在合并链表的后面,并返回合并链表即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var mergeTwoLists = function (l1, l2) {
var prehead = {
next: null
};
var prev = prehead
while (l1 && l2) {
if (l1.val <= l2.val) {
prev.next = l1;
l1 = l1.next;
} else {
prev.next = l2;
l2 = l2.next;
}
prev = prev.next;

}
prev = prev.next;

prev.next = l1 ? l1 : l2;
return prehead.next;
};

复杂度分析

  • 时间复杂度:, 分别为两个链表的长度。因为每次循环迭代中, 只有一个元素会被放进合并链表中, 因此 while 循环的次数不会超过两个链表的长度之和。所有其他操作的时间复杂度都是常数级别的,因此总的时间复杂度为

空间复杂度: 。我们只需要常数的空间存放若干变量。

3.算法思维 递归解法

识别结构,为什么可以使用递归?

因为题目是求的合并,假如的第一个节点小于的第一个节点,问题可以转化为,即list1.nextlist2 的合并,其结果为list[0].next

  • 如果 L1 或者 L2 一开始就是空链表 ,那么没有任何操作需要合并,所以我们只需要返回非空链表。
  • 判断 l1 和 l2 哪一个链表的头节点的值更小,然后递归地决定下一个添加到结果里的节点。
  • 如果两个链表有一个为空,递归结束。
1
2
3
4
5
6
7
8
9
10
11
var mergeTwoLists = function (l1, l2) {
if (l1 === null) return l2;
if (l2 === null) return l1;
if (l1.val <= l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}

复杂度分析

  • 时间复杂度:O(n + m),其中 n 和 m 分别为两个链表的长度。因为每次调用递归都会去掉 l1 或者 l2 的头节点(直到至少有一个链表为空),函数 mergeTwoList 至多只会递归调用每个节点一次。因此,时间复杂度取决于合并后的链表长度,即 O(n+m)。

  • 空间复杂度:O(n + m),其中 n 和 m 分别为两个链表的长度。递归调用 mergeTwoLists 函数时需要消耗栈空间,栈空间的大小取决于递归调用的深度。结束递归调用时 mergeTwoLists 函数最多调用 n+m 次,因此空间复杂度为 O(n+m)。

20. 有效的括号

LeetCode

给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串,判断字符串是否有效。

有效字符串需满足:

  • 左括号必须用相同类型的右括号闭合。
  • 左括号必须以正确的顺序闭合。
  • 注意空字符串可被认为是有效字符串。
1
2
输入: "()"
输出: true
1
2
输入: "(]"
输出: false

1.使用栈

判断括号的有效性可以使用「栈」这一数据结构来解决。

我们对给定的字符串 s 进行遍历,当我们遇到一个左括号时,我们会期望在后续的遍历中,有一个相同类型的右括号将其闭合。由于后遇到的左括号要先闭合,因此我们可以将这个左括号放入栈顶。

当我们遇到一个右括号时,我们需要将一个相同类型的左括号闭合。此时,我们可以取出栈顶的左括号并判断它们是否是相同类型的括号。如果不是相同的类型,或者栈中并没有左括号,那么字符串 s 无效,返回 False。为了快速判断括号的类型,我们可以使用哈希映射(HashMap)存储每一种括号。哈希映射的键为右括号,值为相同类型的左括号。

在遍历结束后,如果栈中没有左括号,说明我们将字符串 s 中的所有左括号闭合,返回 True,否则返回 False

注意到有效字符串的长度一定为偶数,因此如果字符串的长度为奇数,我们可以直接返回 False,省去后续的遍历判断过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    var map = {
"(": ")",
"[": "]",
"{": "}",
}
var isValid = function (s) {
var stack = [],
i = 0,
length = s.length;
if (length % 2 === 1) return false;
for (; i < length; i++) {
if (map[stack[0]] !== s[i]) {
stack.unshift(s[i])
} else {
stack.shift();
}
}
console.log(stack)
return !stack.length
};

复杂度分析

  • 时间复杂度:,其中 是字符串 的长度。

  • 空间复杂度:,其中 表示字符集,本题中字符串只包含 6 种括号,。栈中的字符数量为 ,而哈希映射使用的空间为 ,相加即可得到总空间复杂度。

14.最长公共前缀

LeetCode

需要注意的坑

  • 公共的意思是数组中所有项公共的部分,["aa",'aabb','aabbcc'],最长公共前缀是aa,而不是aabb,因为并不是每一项都包含aabb

  • 最长公共前缀而不是最长公共子串,["xbbcc","xaabbcc","xbbccdd"],最长公共前缀是x,最长公共子串 bbcc

  • 在输入的数组长度为0时返回空字符串

1.纵向扫描

纵向扫描是最容易想到的方法步骤为:

  • 依次遍历每一个数组,检查同一列上的字符是否相同
  • 如果相同记录并累加字符串结果,遍历下一列
  • 如果不同跳出循环,返回结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var longestCommonPrefix = function (strs) {
if (!strs.length) return '';
var result = '', //记录公共前缀结果
i = 0, //表示字符串索引,从第一位开始检查
j = 1, // 输入数组的索引
str = strs[0]; //输入数组中的第一个
for (i = 0; i < str.length; i++) {
for (j = 1; j < strs.length; j++) {
if (strs[j][i] !== str[i]) return result;
}
result += str[i]
}
return result;
}

复杂度分析

  • 时间复杂度:,其中 是字符串数组中的字符串的平均长度, 是字符串的数量。最坏情况下,字符串数组中的每个字符串的每个字符都会被比较一次。

  • 空间复杂度:。使用的额外空间复杂度为常数。

2.横向扫描

  • 依次遍历字符串数组中的每个字符串,把前两个公共前缀的结果和输入数组中的下一个进行比较,数组遍历完成后即得到结果
  • 如果在尚未遍历完所有的字符串时,最长公共前缀已经是空串,则最长公共前缀一定是空串,因此不需要继续遍历剩下的字符串,直接返回空串即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var longestCommonPrefix = function (strs) {
if (!strs.length) return '';
var i = 0,
len = strs.length - 1,
result = strs[0];
for (; i < len; i++) {
var temp = prefixCompare(result, strs[i + 1]);
if(!temp) return temp;
result = temp;
}
return result;
}
// 找出两个字符串的最大公共前缀
function prefixCompare(first, second) {
var i = 0, //索引
len = first.length;
for (; i < len; i++) {
if (first[i] !== second[i]) return first.substring(0, i);
}
return first;
}

复杂度分析

  • 时间复杂度:O(mn),其中 mm 是字符串数组中的字符串的平均长度,nn 是字符串的数量。最坏情况下,字符串数组中的每个字符串的每个字符都会被比较一次。

  • 空间复杂度:O(1)。使用的额外空间复杂度为常数。

3.分治

注意到 的计算满足结合律,有以下结论:

其中是字符串的最长公共前缀,

基于上述结论,可以使用分治法得到字符串数组中的最长公共前缀。对于问题 可以分解成两个子问题,,其中。对两个子问题分别求解,然后对两个子问题的解计算最长公共前缀,即为原问题的解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var longestCommonPrefix = function (strs) {
if (!strs.length) return '';
if (strs.length < 2) return strs[0];
var mid = Math.floor(strs.length / 2),
left = strs.slice(0, mid),
right = strs.slice(mid);
return prefixCompare(longestCommonPrefix(left), longestCommonPrefix(right))
}

// 找出两个字符串的最大公共前缀
function prefixCompare(first, second) {
var i = 0, //索引
len = first.length;
for (; i < len; i++) {
if (first[i] !== second[i]) return first.substring(0, i);
}
return first;
}

复杂度分析

  • 时间复杂度:O(mn),其中 mm 是字符串数组中的字符串的平均长度,nn 是字符串的数量。

  • 空间复杂度:,其中 mm 是字符串数组中的字符串的平均长度,nn 是字符串的数量。空间复杂度主要取决于递归调用的层数,层数最大为 ,每层需要 m 的空间存储返回结果。

4.二分法

显然,最长公共前缀的长度不会超过字符串数组中的最短字符串的长度。用 minLength 表示字符串数组中的最短字符串的长度,则可以在 [0,minLength] 的范围内通过二分查找得到最长公共前缀的长度。每次取查找范围的中间值 mid,判断每个字符串的长度为 mid 的前缀是否相同,如果相同则最长公共前缀的长度一定大于或等于 mid,如果不相同则最长公共前缀的长度一定小于 mid,通过上述方式将查找范围缩小一半,直到得到最长公共前缀的长度。

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
var longestCommonPrefix = function (strs) {
if (!strs.length) return '';
var minlength = Math.min.apply(null, strs.map(item => item.length)),
low = 0,
high = minlength;
while (low < high) {
var mid = ~~((high - low + 1) / 2 + low);
if (prefixCompare(strs, mid)) {
low = mid;
} else {
high = mid - 1;
}
}
return strs[0].substring(0, low);
}

function prefixCompare(strs, mid) {
var str0 = strs[0].substring(0, mid);
var count = strs.length;
for (var i = 1; i < count; i++) {
var str = strs[i];
for (var j = 0; j < mid; j++) {
if (str0[j] != str[j]) {
return false;
}
}
}
return true;
}

复杂度分析

  • 时间复杂度:,其中 mm 是字符串数组中的字符串的最小长度,nn 是字符串的数量。二分查找的迭代执行次数是 ,每次迭代最多需要比较 个字符,因此总时间复杂度是 .

  • 空间复杂度:。使用的额外空间复杂度为常数。

B端产品设计流程和规范

B端产品和C端产品

理解B端和C端

  • B端(Business) :面向商业和企业,是为帮助企业集团等实现商业目的而设计的软件、工具或者平台。
  • C端(Consumer) :表示为消费者、个人用户或终端用户设计,直接面向普通用户提供服务来帮助他们实现个人需求。
  • C端产品后台产品线(BtoC) :比如淘宝卖家平台、饿了么商家版等。除此之外,还有面向商家、企业、业务部门提供的企业级服务产品,以及面对企业或者个人提供的平台级工具产品等。

C端的产品核心功能突出,主要为了实现较单一的功能,比如: 购物,听音乐,上网,12306购票,附加功能可以增加用户体验,但如果缺失并不会影响产品的核心功能。

作为C端用户很少会接触到B端的产品,接触的平台类产品,也可以叫做C段产品的后端产品线,即(BtoC)。

B端产品包含多个主要功能,用于链接C端和B端。

B端和C端产品的区别

  • 用户角度
B端 C端
B端产品追求的是效率和效益的提升。解决需求,上手缓慢 上手较快、用户体验,流量
  • 开发角度
B端 C端
用户量少 用户量多
端周期长 周期短
竞品较少 竞品较多
逻辑复杂 逻辑简单
PC 端 移动端
解决问题驱动 户体验驱动
决策权则在客户手中 使用决策权在用户手中
B端产品的实用性大于美观性,能切实解决问题
配置资源的 B 端产品才是一个好的 B 端产品.
产品经理要具有更强的逻辑思维能力
C端产品除了产品的体验以外,也要让产品更美观,让活动更有趣,让用户更舒服,产品经理有很强的同理心。

后台产品常见分类

  • C 端产品的后台产品线,如淘宝商家版,饿了么商家版,对订单和用户进行管理,支持 C 端产品的业务进展。
  • 平台级工具产品,如微信公众平台,对文章和读者的数据统计和管理;各大互联网公司的开放平台,如微博开放平台等。
  • 企业级服务产品,虚拟主机系统(VMware),云主机管理系统(深信服、xensystem、腾讯云)以及各种云SaaS。
  • 企业的业务处理平台,对内有考勤、报销等 OA办公系统,对外有 CRM 客户管理系统,ERP 资源及供应链管理系统。

后台产品设计思路

问题所在

  • 竞品较少,难以参考
  • 深入行业,对行业有自己的理解
  • 权限众多,关系复杂
  • 需要多和产品经理沟通,加深对产品的思考和探索

后台UI设计工作流程

  • 需求分析

    1. 对行业和产品有基本的认知
    2. 要了解产品的基本情况,比如产品目标、用户人群、产品定位、需求分析、功能模块、主要竞品和产品特色。
    3. 解决什么问题?想实现什么目标?使用这个系统的用户有哪些?不同角色的用户有哪些具体的权限?是否需要对每一个用户的行为都生成操作日志?产品有哪些主要的功能模块?产品的业务流程是怎样的?有哪些同类型的产品?和他们相比我们的产品有什么特色和特点?
  • 设计执行阶段

    1. 参照 PM 给出的功能清单做原型和流程的梳理,需要关注的有当前版本规划、功能模块、功能类型、功能描述、任务优先级、完成时间等,交互原型则伴随着功能描述、规则判定条件、触发条件等内容。
    2. 原型设计完成,开始做 UI 视觉方面的设计,而这时后端同步构思需求的实现方案。UI 设计师向前端了解实现框架,方便交接和沟通。
    3. 当界面实现,UI 设计师应该是最早进行测试的,力求视觉设计和代码实现无误差。在完成设计执行后,从信息层级、文字、图标、图片等方面进行设计走查,进行多次设计验证与测试。
  • 数据分析
    数据分析是产品优化迭代的重要依据。进行多番测试和评审后交付客户(或内部)使用,根据产生的具体问题进行不断迭代,且观察产品能否通过准确的数据反映问题、体现能力,应虚心接纳使用者的使用建议并严谨思考其合理性,用户的使用和反馈是优化产品的重要途径。只有达到了管理和运营的指标,后台产品才是真正产生了价值。

制定设计规范的作用

  • 对产品:在项目完成第一版较为稳定的版本时,着手制定设计标准,统一公司视觉设计规范及某些特定交互设计规范。同一个项目会有多个设计师的参与,规范化的设计语言,避免因设计控件混乱而影响设计输出。
  • 对自己:组件化同类元素,提高工作效率,建立公司产品的组件库,以便不同项目的复用及设计延展。在同一个项目中也能更好的把控该项目的视觉规范,提高效率。
  • 对团队:设计规范的制定,规范了设计团队的输出,同时方便了与开发团队的交接和协作。通过设计规范的制定,前端实现也能拥有一套可供复用和扩展的组件库,助力上下游交接及团队协作。
  • 对客户:制定设计规范的同时需要考虑设计延展性,为不同客户的定制化需求提供更高效的输出。同时在进行产品迭代时,设计规范的组件化调整也大大提高了工作效率。

后台产品设计规范

页面布局
统一尺寸

据统计,目前 PC 端用户屏幕分辨率占比排名前三的是 19201080、1366768、1440*900,以 1440 来设计的话,向上适配或者向下适配误差会比较小。

适配方案:面向多个客户,后台产品设计功能型页面的尺寸统一为 1440900,按照栅格系统原则向上或向下适配。展示型页面以 1440900 为主,同时设计出极端情况(宽度为 1280 以及宽度为 1920)的效果图,力求实现前端实现效果和高保真设计图误差最小。面向公司内部的后台系统,由于各个职工电脑屏幕是统一采购、统一尺寸,所以开发适配的分辨率可以统一尺寸进行设计,这个尺寸根据公司内部采购屏幕的尺寸和分辨率选择即可(提前和前端沟通好)。

页面框架

页面框架主要分为左右栏布局和上下栏布局,还有其他的布局。左右栏布局包括顶部栏、左侧菜单栏、主体内容三大区域,其中顶部菜单栏、左侧菜单栏为固定结构,右侧主体内容根据分辨率进行动态缩放;上下栏布局包括顶部菜单栏和主体内容两大区域,其中顶部菜单栏为固定结构,主体内容进行动态缩放且需定义主体内容左右两边空白区域最小值;左右栏布局时,左侧菜单可收缩展开,收缩状态下固定宽度。

栅格布局

栅格系统的使用是为了解决自适应和响应式问题,从而更好地进行产品设计和产品开发。响应式栅格采用 24 列栅格系统实现,以满足 2,3,4,5,6 分比布局等多种情况。固定宽度 Column,将间隔 Gutter 进行动态缩放。

需要栅格化处理的内容的总宽度=23列(1列=1宽度Column+1间隔Gutter)+1宽度Column=24宽度Column+23间隔Gutter。

谷歌规定模块和结构之间要以 8px 为基准,布局间相对间距可采用 8px 以及 8 的倍数,但一些小组件(按钮、间隔、输入框)可以以 4 为基准。栅格布局是为了辅助设计,灵活运用,不要被它所局限。

尺寸设定

一般在整体区域左上角放置产品 LOGO 及产品名称,大部分系统顶部栏高度 48+8n,侧边栏宽度 200+8n。我常用的是顶部栏高度 56px,侧边栏宽度 200px,侧边栏收缩状态宽度 56px,右侧的侧浮窗宽度 400px。

相对间隔

定义主体内容的上下左右边距,定义主体区域内各模块的边距及安全宽度,超出内容区域的部分采用区域内滚动或整屏滚动,视情况固定导航栏。

标准色

颜色分为品牌色、辅助色、中性色。根据不同产品的不同需求,可能也会将统计图、标签等进行统一标准色设定。

品牌色即产品主色,产品主色的设定直接影响产品气质和直观感受,也是产品直接对外的形象。品牌色要根据产品特性、用户使用场景、产品定位等进行选取,尽量做好色彩的延伸性,可支持换肤。品牌色的应用场景包括操作状态、按钮色、可操作图标等。

辅助色用于提示其他场景,比如成功、失败、警告、无效等。

中性色常用于文本、背景、边框、分割线等,需要考虑深色背景和浅色背景的差异,可以选择同一色相控制透明度变化,用来表现不同的层级结构。

其他色如统计图、数据可视化、多个标签的不同配色方案根据项目情况单独设定。

标准字

后台系统常用的字体:windows 系统,中文 Microsoft YaHei,英文 Arial;Mac 字体,中文 PingFang SC,英文 Helvetica;除此之外可以选择的字体还有 segoe UI、思源黑体、Hiragino Sans GB等。

后台系统中常用字体大小为 12px、13px、14px、16px、18px、20px、24px、30px。

行高设定,根据文字大小及使用场景设置行高,一般行高=文字大小+6px/8px。

图标

图标是 UI 设计中重要组成部分,一般分为功能图标和应用图标,以图形的方式传达概念,可以降低理解成本,使得界面更加协调美观。在后台产品中,图标的功能则更偏向辅助性,辅助用户对功能的认识。

除了某些常用的图标,有一些专业性的操作和词汇则需要设计师进行绘制,现在比较高效方便的方法是在 iconfont 提供的图标模板上用 AI 绘制,画板 1024*1024,提供圆形、正方形、矩形形状。图标尺寸按照 8 的倍数进行延展,绘制完成后生成 svg 格式文件,提交到阿里巴巴矢量图标库的项目组里,方便前端调用,调整大小和颜色更为方便,且能够优化系统内存和性能。

按钮

按钮是后台产品进行交互设计是重要元素,提供给用户进行点击操作,是视觉上最引人注目的控件,具有一定的视觉受范性。常用按钮可分为填充按钮、线性按钮、文字按钮。

按钮的交互状态包括默认、悬停、点击和不可用。

按钮根据需求分为不同尺寸,大中小三个级别用在不同的场景,一般按照 8 的倍数设定。如高度分别设定为 24、32、40px。

规范整理时要规定不同类型按钮的宽高、圆角及文字大小,同时还要将按钮的不同状态展现出来。

填充按钮之间间距最小为 10px。

导航

导航的类型有很多种,常用的比如顶栏菜单、侧栏菜单、折叠菜单、下拉菜单、面包屑、分页、步骤条、时间轴、tab标签页、胶囊菜单、徽标数等。

各类导航中的字体大小可进行统一设定。

顶栏菜单多为一级菜单,点击切换,或作为下拉菜单的父级,将子级菜单合理分类。

侧栏菜单为垂直导航菜单,可以内嵌子菜单。

下拉菜单的触发方式一般有鼠标悬停和鼠标点击两种。

步骤条引导用户按照流程来完成任务,一般步骤不得少于两步。

分页的高度设定为 24px、30px、32px,根据应用场景适当增减内容,比如设定每页展示数据的条数、跳转至指定页等。

面包屑用于说明层级结构,使用户明确当前所在位置,并且可以回到任一上级页面。

徽标数用来通知用户当前有未读消息,一般出现在图标的右上角或者跟在文字后面。

表单

表单多由一条或多条列表项组成,单一列表项的类型有字段输入框、条件选择器。

字段输入框的标题和输入框分布方式包括左右、上下、无标题。左右分布是常见的对齐方式,比较适合 PC 端的使用;上下分布增加了表单的整体高度,视情况选择使用;无标题经常应用在登录注册,虽然减少了面积,但是增加了理解难度。

输入框的交互状态包括默认、输入结果、提示错误、禁用、获取焦点。

输入框的尺寸可按照8的倍数进行设定,比如 24px、32px,也可根据系统实际情况进行设定,我常用的输入框高度为 30px,宽度视情况而定,无圆角。上下布局的多个输入框上下间距为 20px,有错误提示时候竖向增加 10px 或横向显示在输入框右侧(预留出位置)。

表单中标题文字左对齐,输入框左对齐,标题文字距离输入框20px(多个长度不同的输入框算最长的);标题文字右对齐,输入框左对齐,也是常用的方式。输入框内正文字体 14px,文字和左右两边边框的边距 10px。

选择器包括单选、多选、时间选择、开关切换、下拉选择、滑块选择、旋钮等。单选框多为圆形,复选框多为方形。

搜索框和选择框的高度为 30px 或按照 8 的倍数自行设定,通常和输入框保持一致。搜索框距离右侧按钮 4px,内部文字 14px。

单选多选框尺寸 16*16px,多个选项横向排列间距 16px,纵向排列间距 8px。

开关按钮外框 4020px,内部圆形 1616px。

表格

表格在后台产品 UI 设计中占比非常大,用来展示数据、统一管理、作为详情入口,是最清晰、高效的形式之一。在设计规范中需设定表头高度、表格行高、表格列宽范围,同时也包括表格中的按钮样式、标签样式。

表格主要分为五大区域:选择搜索区、操作区、表头、正文、底栏。选择搜索区放置筛选框和搜索框,为用户提供按需搜索,可以大大提高用户效率;操作区指各种对表格内容进行增删改查、批量处理、配置列的动作;表头展示列标题,一般具有排序功能;正文主要展示各种各样的数据,要注意行高、对齐、分割、信息层级等,要考虑是否提供行内操作;底栏显示分页、总数统计等。

表格信息一般主要功能为增删改查,查看和编辑是最基本的功能,表格信息支持筛选、搜索、排序、分页。对可批量操作的表格数据在第一列增加多选框。

行高

表格行高可设置为表格内字体高度的 2~3 倍,主表格会间隔显示不同颜色,用于区分不同行数据、加强视觉流引导,展开单行的内置表格可采用纯色,选中行应有视觉上的反馈。表头要和表格内容有视觉上的区分。表格行高可采用 36、40、48、60 等。

行数

表格行数太多加载速度会降低,延长用户等待时间;行数太少会导致用户不断翻页,降低使用效率。比较合适的默认表格行数是 20 或 50,用户可以根据自己需求选择默认的行数。设定行数之后,如果每页行数多于每屏行数,可在表格内引入滚动条,这时可以固定表头滚动内容。

列宽

列宽根据内容字段长短需要有不同且合理的默认值,使得表格字段有良好的展示效果。列内容的长度固定时,列宽应大于固定宽度(比如时间、MD5、SHA1);列内容不固定时,能预判最大宽度的按照最大宽度设定列宽(比如IP地址、MAC地址、姓名),不能预判最大宽度的设定列宽按照常用宽度,多于内容省略以「…」展示,鼠标悬停出现完整内容(比如详情、描述)。

列数

表格列不应过多,列数比较多的情况下应该合理进行合并、隐藏、删除或进行优先级处理。常用的方法有引入配置列,用户可自定义展示必需列以外的其他列;只展示重要信息,下拉展开列查看完整信息;在表格中引入横向滚动条,根据实际情况选择是否要始终固定基本信息列(如第一列是文件名)和操作列(最后一列的操作)。

对齐方式

表格内的文本应按照文本类型不同进行统一规范,如金额类数值保留相同位数小数,SHA1 虽然是一串数字但是其实那并不是数据而是一串编码,所以可以像文本一样左对齐。根据文本内容不同,对齐方式也应灵活调整,可采用文本左对齐、数据右对齐、金额小数点对齐的方式。数据前面有标签的,将标签前置对齐。类似 IP 地址、MD5、SHA1、域名这样的信息,也可以根据产品需要在文本前面增加「复制」图标,方便用户调用。

详情入口

表格内部数据的详情入口,将能点击下钻查看详情的内容以不同颜色表示,同时在表格行最后一列操作按钮部分放置一个查看按钮。

反馈

包括弹框、侧滑框、骨架屏、全局提示、警告提示、消息提醒、加载状态等。分为模态框和非模态框,区别是是否会打断用户工作流。

弹框又称对话框,是叠加在应用主窗口上的弹出式窗口,以对话的方式使用户参与进来。

弹框

弹框出现时,主题内容增加一层遮罩 #000,透明度 50%,避免使用双层弹框,可同时采用有关闭图标的弹框和无关闭图标的弹框,引导用户对内容进行正确操作。如果设定系统内所有弹框均可以点击弹框外区域关闭, 则需要为用户新增或编辑内容的弹框弹出二级确认的弹框,或者再次进行交互梳理。

侧滑框

侧滑框又称抽屉,出现在右侧,固定宽度 400px,高度覆盖在主题内容之上,点击侧滑框以外的区域则收起侧滑框。

骨架屏

为某些特定数据提供数据加载等待时的占位图形组合。

全局提示

建议停留时间 3s,可根据文字字数调整停留时间,文字内容限制在 30 以内。

警告提示

用不同颜色和样式展示需要关注的信息。

通知提醒

消息通知和警告信息用通知提醒框,单个消息从页面右侧以抽屉的方式划出,用户可手动关闭,或停留 3s 后自动关闭。

缺省状态

绘制不同类型的情感化插画表示缺省状态,如404、500、暂时没有数据、没有新消息等。

页面需要一个默认的底色,错误文字使用 14px,与情感化插画间距 20px,与按钮间距 30px。

数据可视化

数据可视化部分可能是后台产品中对视觉设计要求较高的部分,使用情境为各类统计图、大屏展示页面等。

功能型页面的数据可视化可以引入图形化设计组件,Echarts、G2、d3等;展示型页面的数据可视化则可以做得更有趣,比如立体的统计图、粒子地球效果、灵活有趣的网络拓扑图等。

考虑到数据可视化可能会需要深色浅色不同的背景,在数据可视化统计图的色彩搭配上要注意颜色的拓展性。

总结

不管是做 C 端产品还是 B 端产品,都是为了实现用户的需求、帮用户解决问题。

刚接触后台产品的时候,最希望能把产品做的炫酷、美观,工作中慢慢地发现项目的背后思考更为重要,产出的设计成果也应该有理有据、支撑整个设计体系。网上供大家使用和学习的资源非常多,对一些公司来说、专门去制定一套自己的后台设计规范不免显得费时费力,合理引入 antdesign 和 element 等开源的设计组件,会使得设计师以及前端事半功倍,有助于设计师把更多的精力投入到沉淀行业知识、研究产品架构、梳理交互方式和创新视觉表现上。

生产环境处理

提取css文件

webpack4中可以使用 mini-css-extract-plugin

css兼容新处理

postcss-loader 需要安装 postcss-preset-env

配置postcss.config.js

1
2
3
4
5
module.exports = {
plugins: [
require('postcss-preset-env'),
]
}

添加浏览器版本配置文件 .browserslistrc

1
2
3
4
5
[production]
> 1%
ie 10
[modern]
last 1 chrome version

webpack.config.js中使用loader

post默认使用的是 .browserslistrc 生产环境的配置,开发环境需要手动修改 process.env.NODE_ENV='devlopment'webpack.config.js中的mode没有关系

压缩css

**css-minimizer-webpack-plugin**直接使用无需配置

编译时检查语法规则

eslint-webpack-plugin

一定要在webpack.config.js中配置 exclude: /node_modules/,只检查自己的代码

eslint-plugin-react-hooksreact-hooks中使用

js兼容处理

配置babel-loader 需要安装@babel/core 添加 .babelrc 文件

使用 preset-env 同样存在问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"presets": [
[
"@babel/preset-env",
{
// 把用到的polyfill按需引入,依赖core-js
"useBuiltIns": "useage",
"corejs":3
}
],
"@babel/preset-typescript",
"@babel/preset-react",
"@babel/plugin-transform-runtime"
]
}

压缩html

添加html-webpack-plugin配置项

1
2
3
4
5
6
7
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src/index.html'),
minify:{
collapseWhitespace: true,// 移除空白
removeComments: true,// 移除注释
}
})
  • Copyrights © 2015-2026 SunZhiqi

此时无声胜有声!

支付宝
微信