一個博主做的shiro筆記:https://www.guitu18.com/post/2019/07/26/43.html
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- shiro --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.7.0</version> </dependency> <!--mybatis-plus 持久層--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.1</version> </dependency> <!-- 整合swagger --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.8.0</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.8.0</version> </dependency> <!-- kaptcha 驗證碼 --> <dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> </dependency> </dependencies>
package com.unclebb.zlgl.config; import com.unclebb.zlgl.utils.CustomRealm; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.util.ThreadContext; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.HashMap; import java.util.Map; /**
* @program: zlgl
* @description: Shiro配置類:將SecurityManager以及Realm都注入到Spring容器中
* @author: LiuZhiliang
* @create: 2021-05-10 08:56
**/ @Configuration public class ShiroConfig { /**
* @Description: 代理生成器,需要借助SpringAOP來掃描@RequiresRoles和@RequiresPermissions等注解。生成代理類實現功能增強,從而實現權限控制。需要配合AuthorizationAttributeSourceAdvisor一起使用,否則權限注解無效。
* @Param:
* @return:
* @Author: Liuzhiliang
* @Date:
*/ @Bean public DefaultAdvisorAutoProxyCreator lifecycleBeanProcessor(){ DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } /**
* @Description: 上面配置的DefaultAdvisorAutoProxyCreator相當于一個切面,下面這個類就相當于切點了,兩個一起才能實現注解權限控制。
* @Param:
* @return:
* @Author: Liuzhiliang
* @Date:
*/ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager){ AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } /**
* @Description: Filter工廠,設置對應的過濾條件和跳轉條件
* @Param:
* @return:
* @Author: Liuzhiliang
* @Date:
*/ //權限管理,配置主要是Realm的管理認證 @Bean public DefaultWebSecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); ThreadContext.bind(securityManager); return securityManager; } //將自己的驗證方式加入容器 @Bean public CustomRealm myShiroRealm() { HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); //加密 matcher.setHashAlgorithmName("md5"); matcher.setHashIterations(1); CustomRealm customRealm = new CustomRealm(); customRealm.setCredentialsMatcher(matcher); return customRealm; } @Bean public ShiroFilterFactoryBean shiroFilter(DefaultSecurityManager securityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // Map<String,String> maps = new HashMap<>(); // maps.put("/logout","logout"); // maps.put("/**","authc"); // shiroFilterFactoryBean.setLoginUrl("/login"); // shiroFilterFactoryBean.setUnauthorizedUrl("/403.html"); // shiroFilterFactoryBean.setSuccessUrl("/index"); // shiroFilterFactoryBean.setFilterChainDefinitionMap(maps); return shiroFilterFactoryBean; } }
package com.unclebb.zlgl.config; import com.google.common.base.Predicates; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; /**
* @Author unclebb
* @Description Swagger配置類
* @Date 2021/5/9
**/ @Configuration @EnableSwagger2 public class Swagger2Config { @Bean public Docket webApiConfig(){ return new Docket(DocumentationType.SWAGGER_2) .groupName("webApi") .apiInfo(webApiInfo()) .select() //過濾掉admin路徑下的所有頁面 .paths(Predicates.and(PathSelectors.regex("/user/.*"))) //過濾掉所有error或error.*頁面 //.paths(Predicates.not(PathSelectors.regex("/error.*"))) .build(); } private ApiInfo webApiInfo(){ return new ApiInfoBuilder() .title("網站-API文檔") .description("本文檔描述了網站微服務接口定義") .version("1.0") .contact(new Contact("qy", "http://atguigu.com", "55317332@qq.com")) .build(); } }
package com.unclebb.zlgl.config; import com.google.code.kaptcha.impl.DefaultKaptcha; import com.google.code.kaptcha.util.Config; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import java.util.Properties; /**
* @program: zlgl
* @description: Kaptcha配置類
* @author: LiuZhiliang
* @create: 2021-05-12 09:03
**/ @Component public class KaptchaConfig { @Bean public DefaultKaptcha getKaptcha(){ DefaultKaptcha dk = new DefaultKaptcha(); Properties properties = new Properties(); // 圖片邊框 properties.setProperty("kaptcha.border", "yes"); // 邊框顏色 properties.setProperty("kaptcha.border.color", "105,179,90"); // 字體顏色 properties.setProperty("kaptcha.textproducer.font.color", "red"); // 圖片寬 properties.setProperty("kaptcha.image.width", "110"); // 圖片高 properties.setProperty("kaptcha.image.height", "40"); // 字體大小 properties.setProperty("kaptcha.textproducer.font.size", "30"); // session key properties.setProperty("kaptcha.session.key", "code"); // 驗證碼長度 properties.setProperty("kaptcha.textproducer.char.length", "4"); // 字體 properties.setProperty("kaptcha.textproducer.font.names", "宋體,楷體,微軟雅黑"); Config config = new Config(properties); dk.setConfig(config); return dk; } }
package com.unclebb.zlgl.pojo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.util.Set; /**
* @Author unclebb
* @Description 用戶實體類
* @Date 2021/5/9
**/ @Data @TableName(value = "user") public class User { @TableId(value = "id",type = IdType.AUTO)//指定自增策略 private int id; @TableField(value = "username") private String username; @TableField(value = "password") private String password; @TableField(exist = false) private Set<Role> rolesSet; private String salt; }
package com.unclebb.zlgl.pojo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data; import java.util.Set; /**
* @Author unclebb
* @Description 角色實體類
* @Date 2021/5/9
**/ @Data public class Role { @TableId(value = "id",type = IdType.AUTO)//指定自增策略 private int id; @TableField(value = "user_name") private String userName; @TableField(value = "role_name") private String roleName; @TableField(exist = false) private Set<Permission> permissionSet; }
package com.unclebb.zlgl.pojo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data; /**
* @Author unclebb
* @Description 權限實體類
* @Date 2021/5/9
**/ @Data public class Permission { @TableId(value = "id",type = IdType.AUTO)//指定自增策略 private int id; @TableField(value = "role_name") private String roleName; @TableField(value = "permission_name") private String permissionName; }
基本格式:
package com.unclebb.zlgl.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.unclebb.zlgl.pojo.User; import org.apache.ibatis.annotations.Mapper; /**
* @Author unclebb
* @Description 用戶映射
* @Date 2021/5/9
**/ @Mapper public interface UserMapper extends BaseMapper<User> { }
package com.unclebb.zlgl.service; import com.baomidou.mybatisplus.extension.service.IService; import com.unclebb.zlgl.pojo.User; import org.springframework.stereotype.Service; /**
* @Author unclebb
* @Description 用戶登錄接口
* @Date 2021/5/9
**/ @Service public interface LoginService extends IService<User> { public User getByUsername(String username); }
package com.unclebb.zlgl.service; import com.baomidou.mybatisplus.extension.service.IService; import com.unclebb.zlgl.pojo.Role; import org.springframework.stereotype.Service; import java.util.List; @Service public interface RoleService extends IService<Role> { public List<Role> getRole(String userName); }
package com.unclebb.zlgl.service; import com.baomidou.mybatisplus.extension.service.IService; import com.unclebb.zlgl.pojo.Permission; import org.springframework.stereotype.Service; import java.util.List; @Service public interface PermissionService extends IService<Permission> { public List<Permission> getPermissions(String roleName); }
package com.unclebb.zlgl.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.unclebb.zlgl.mapper.UserMapper; import com.unclebb.zlgl.pojo.Role; import com.unclebb.zlgl.pojo.User; import com.unclebb.zlgl.service.LoginService; import com.unclebb.zlgl.service.PermissionService; import com.unclebb.zlgl.service.RoleService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.HashSet; import java.util.List; import java.util.Set; /**
* @Author unclebb
* @Description 登錄實現類
* @Date 2021/5/9
**/ @Service public class LoginServiceImpl extends ServiceImpl<UserMapper, User> implements LoginService { @Autowired RoleService roleService; @Override public User getByUsername(String username) { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.eq("username",username); User user = this.getOne(wrapper); List<Role> roleList = roleService.getRole(user.getUsername()); user.setRolesSet(new HashSet<Role>(roleList)); return user; } }
package com.unclebb.zlgl.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.unclebb.zlgl.mapper.RoleMapper; import com.unclebb.zlgl.pojo.Permission; import com.unclebb.zlgl.pojo.Role; import com.unclebb.zlgl.service.PermissionService; import com.unclebb.zlgl.service.RoleService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.HashSet; import java.util.List; import java.util.Set; /**
* @program: zlgl
* @description: Role實現類
* @author: LiuZhiliang
* @create: 2021-05-10 16:16
**/ @Service public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService { @Autowired PermissionService permissionService; @Override public List<Role> getRole(String userName) { QueryWrapper wrapper = new QueryWrapper(); wrapper.eq("user_name",userName); List<Role> roleList = this.list(wrapper); for (Role role:roleList){ List<Permission> permissions = permissionService.getPermissions(role.getRoleName()); role.setPermissionSet(new HashSet<Permission>(permissions)); } return roleList; } }
package com.unclebb.zlgl.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.unclebb.zlgl.mapper.PermissionMapper; import com.unclebb.zlgl.pojo.Permission; import com.unclebb.zlgl.pojo.User; import com.unclebb.zlgl.service.PermissionService; import org.springframework.stereotype.Service; import java.util.List; import java.util.Set; /**
* @program: zlgl
* @description: Permission實現類
* @author: LiuZhiliang
* @create: 2021-05-10 16:18
**/ @Service public class PermissionServiceImpl extends ServiceImpl<PermissionMapper, Permission> implements PermissionService { @Override public List<Permission> getPermissions(String roleName) { QueryWrapper<Permission> wrapper = new QueryWrapper<>(); wrapper.eq("role_name",roleName); return this.list(wrapper); } }
package com.unclebb.zlgl.controller; import com.unclebb.zlgl.pojo.Role; import com.unclebb.zlgl.pojo.User; import com.unclebb.zlgl.service.RoleService; import com.unclebb.zlgl.utils.Result; import io.swagger.annotations.ApiOperation; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpSession; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; /**
* @Author unclebb
* @Description 登錄控制器
* @Date 2021/5/9
**/ @RestController public class LoginController { @Autowired RoleService roleService; /**
* @Description: 登錄接口
* @Param:
* @return:
* @Author: Liuzhiliang
* @Date:
*/ @ApiOperation(value = "登錄接口") @RequestMapping("/user/login") public Result login(@RequestBody User user){ System.out.println("進入/user/login API接口"); if (StringUtils.isEmpty(user.getUsername())||StringUtils.isEmpty(user.getPassword())){ return Result.fail("請輸入用戶名密碼"); } Subject subject = SecurityUtils.getSubject(); Session session = subject.getSession(); Object sessionId = session.getId(); Map ret = new HashMap<String,Object>(); ret.put("token",sessionId); UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(),user.getPassword()); try { //登錄成功 subject.login(token); return Result.ok(ret); } catch (AuthenticationException e) { e.printStackTrace(); //登錄失敗 return Result.fail("用戶名密碼錯誤"); } } /**
* @Description: 基本測試接口
* @Param:
* @return:
* @Author: Liuzhiliang
* @Date:
*/ @RequestMapping("/user/noauthority") public Result noauthority(){ return Result.fail("沒有權限"); } /**
* @Description: 測試session接口
* @Param:
* @return:
* @Author: Liuzhiliang
* @Date:
*/ @RequestMapping("/user/demoSession") @ResponseBody public String demoSession(HttpSession session){ System.out.println("測試session"); Enumeration<String> names = session.getAttributeNames(); while (names.hasMoreElements()){ String name = names.nextElement(); Object value = session.getAttribute(name); System.out.println(name + " ------- "+ value); } return "session 取值"; } /**
* @Description: 測試session接口
* @Param:
* @return:
* @Author: Liuzhiliang
* @Date:
*/ @RequestMapping("/user/demoSession2") @ResponseBody public String demoSession2(){ Subject subject = SecurityUtils.getSubject(); Session session = subject.getSession(); System.out.println(session.getHost()); System.out.println(session.getId()); System.out.println(session.getLastAccessTime().getTime()); System.out.println(session.getTimeout()); System.out.println(session.getAttribute("test")); return "session 取值"; } /**
* @Description: 測試session接口
* @Param:
* @return:
* @Author: Liuzhiliang
* @Date:
*/ @RequestMapping("/user/checkPermission") @ResponseBody public Result checkPermission(User user){ if (StringUtils.isEmpty(user.getUsername())||StringUtils.isEmpty(user.getPassword())){ return Result.fail("請輸入用戶名密碼"); } Subject subject = SecurityUtils.getSubject(); Session session = subject.getSession(); session.setAttribute("test","test"); UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(),user.getPassword()); token.setRememberMe(true); try { //登錄成功 subject.login(token); } catch (AuthenticationException e) { e.printStackTrace(); //登錄失敗 return Result.fail("用戶名密碼錯誤"); } try { subject.checkRole("admin"); return Result.ok("權限檢查成功"); } catch (AuthorizationException e) { e.printStackTrace(); return Result.fail("檢查權限失敗"); } } /**
* @Description: 根據token獲取用戶授權信息接口
* @Param:
* @return:
* @Author: Liuzhiliang
* @Date:
*/ @RequestMapping("/user/checkRole") public Result checkRole(@RequestParam String token){ Subject subject = SecurityUtils.getSubject(); Session session = subject.getSession(); List<Role> roleList = null; if (token.equals(session.getId().toString())){ String username = session.getAttribute("org.apache.shiro.subject.support.DefaultSubjectContext_PRINCIPALS_SESSION_KEY").toString(); System.out.println(username); roleList = roleService.getRole(username); } return Result.ok(roleList); } /**
* @Description: 測試kaptcha ,獲取驗證碼
* @Param:
* @return:
* @Author: Liuzhiliang
* @Date:
*/ @RequestMapping("/user/testKaptcha") @ResponseBody public String TestKaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException { byte[] captcha = null; ByteArrayOutputStream out = new ByteArrayOutputStream(); try { // 將生成的驗證碼保存在session中 String createText = defaultKaptcha.createText(); request.getSession().setAttribute("rightCode", createText); BufferedImage bi = defaultKaptcha.createImage(createText); ImageIO.write(bi, "jpg", out); } catch (Exception e) { response.sendError(HttpServletResponse.SC_NOT_FOUND); } captcha = out.toByteArray(); response.setHeader("Cache-Control", "no-store"); response.setHeader("Pragma", "no-cache"); response.setDateHeader("Expires", 0); response.setContentType("image/jpeg"); ServletOutputStream sout = response.getOutputStream(); sout.write(captcha); sout.flush(); sout.close(); return "測試Kaptcha"; } /**
* 校對驗證碼
*
* @param request
* @param response
* @return
*/ @RequestMapping(value = "/user/verifyKaptcha") public Result imgvrifyControllerDefaultKaptcha(HttpServletRequest request, HttpServletResponse response) { ModelAndView model = new ModelAndView(); String rightCode = (String) request.getSession().getAttribute("rightCode"); String tryCode = request.getParameter("tryCode"); System.out.println("rightCode:" + rightCode + " ———— tryCode:" + tryCode); if (!rightCode.equals(tryCode)) { model.addObject("info", "驗證碼錯誤,請再輸一次!"); model.setViewName("login"); return Result.fail("驗證碼錯誤"); } else { model.addObject("info", "登陸成功"); model.setViewName("index"); return Result.ok("驗證成功"); } } /**
* @Description: 測試 校驗權限 permission
* @Param:
* @return:
* @Author: Liuzhiliang
* @Date:
*/ @RequestMapping("/user/testShiroPermission") @RequiresPermissions("user:add") public Result TestShiroPermissions(){ System.out.println("訪問 TestShiroPermissions API"); Subject subject = SecurityUtils.getSubject(); String username = (String) subject.getPrincipal(); System.out.println(username); return Result.ok(username); } /**
* @Description: 登出功能API
* @Param:
* @return:
* @Author: Liuzhiliang
* @Date:
*/ @RequestMapping("/user/logout") public Result logout(){ Subject subject = SecurityUtils.getSubject(); String username = (String) subject.getPrincipal(); subject.logout(); return Result.ok(username); } /**
* @Description: 測試校驗 角色 role
* @Param:
* @return:
* @Author: Liuzhiliang
* @Date:
*/ @RequestMapping("/user/TestRole") @RequiresRoles("admin") public Result TestRole(){ System.out.println("測試TestRole"); return Result.ok("測試Role"); } }
package com.unclebb.zlgl.utils; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; /**
* @Author unclebb
* @Description 統一返回API格式
* @Date 2021/5/9
**/ @Data @ApiModel(value = "全局統一返回結果") public class Result<T> { @ApiModelProperty(value = "返回碼") private Integer code; @ApiModelProperty(value = "返回消息") private String message; @ApiModelProperty(value = "返回數據") private T data; public Result(){} public static <T> Result<T> build(T data) { Result<T> result = new Result<T>(); if (data != null) result.setData(data); return result; } public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) { Result<T> result = build(body); result.setCode(resultCodeEnum.getCode()); result.setMessage(resultCodeEnum.getMessage()); return result; } public static<T> Result<T> ok(){ return Result.ok(null); } /**
* 操作成功
* @param data
* @param <T>
* @return
*/ public static<T> Result<T> ok(T data){ Result<T> result = build(data); return build(data, ResultCodeEnum.SUCCESS); } public static<T> Result<T> fail(){ return Result.fail(null); } /**
* 操作失敗
* @param data
* @param <T>
* @return
*/ public static<T> Result<T> fail(T data){ Result<T> result = build(data); return build(data, ResultCodeEnum.FAIL); } public Result<T> message(String msg){ this.setMessage(msg); return this; } public Result<T> code(Integer code){ this.setCode(code); return this; } }
package com.unclebb.zlgl.utils; import lombok.Getter; /**
* @Author unclebb
* @Description API狀態信息
* @Date 2021/5/9
**/ @Getter public enum ResultCodeEnum { SUCCESS(200,"成功"), FAIL(201, "失敗"), SERVICE_ERROR(202, "服務異常"), DATA_ERROR(204, "數據異常"), SIGN_ERROR(300, "簽名錯誤"), PAY_PASSWORD_ERROR(401, "支付密碼錯誤"), REPEAT_ERROR(402, "重復提交"), INVEST_AMMOUNT_MORE_ERROR(501, "出借金額已經多余標的金額"), RETURN_AMMOUNT_MORE_ERROR(502, "還款金額不正確"), PROJECT_AMMOUNT_ERROR(503, "標的金額不一致") ; private Integer code; private String message; private ResultCodeEnum(Integer code, String message) { this.code = code; this.message = message; } }
package com.unclebb.zlgl.utils; import com.unclebb.zlgl.pojo.Permission; import com.unclebb.zlgl.pojo.Role; import com.unclebb.zlgl.pojo.User; import com.unclebb.zlgl.service.LoginService; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; /**
* @program: zlgl
* @description: 自定義Realm
* @author: LiuZhiliang
* @create: 2021-05-10 09:09
**/ public class CustomRealm extends AuthorizingRealm { @Autowired LoginService loginService; /**
* @Description: 授權配置
* @Param:
* @return:
* @Author: Liuzhiliang
* @Date:
*/ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = (String) principals.getPrimaryPrincipal(); User user = loginService.getByUsername(username); if (user == null){ return null; }else { SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); for (Role role : user.getRolesSet()){ simpleAuthorizationInfo.addRole(role.getRoleName()); for (Permission permission : role.getPermissionSet()){ simpleAuthorizationInfo.addStringPermission(permission.getPermissionName()); } } return simpleAuthorizationInfo; } } /**
* @Description: 認證配置
* @Param:
* @return:
* @Author: Liuzhiliang
* @Date:
*/ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = token.getPrincipal().toString(); User user = loginService.getByUsername(username); if (user == null){ return null; }else { //匹配密碼 SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,user.getPassword(),getName()); simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(user.getSalt())); return simpleAuthenticationInfo; } } }
前端我這里直接用了 開源的管理系統框架
附地址:
運行截圖如下
完事兒只需要改一下它的 返回狀態碼校驗,配置下跨域就可以了
驗證碼部分包含兩個API接口
/user/testKaptcha 獲取驗證碼信息
/user/verifyKaptcha 校驗
其中獲取驗證碼圖片信息相當于請求靜態圖片資源,直接將驗證碼圖片的src指向 該接口即可,前端源碼如下:
<el-image :src="kaptcha" @click="refreshCode()" alt="加載失敗" style="margin-left:10px;height:40px;margin-top:5px"> <div slot="placeholder" class="image-slot"> <i class="el-icon-loading"></i> </div> </el-image>
其中路徑定義為:
kaptcha:"http://localhost:8082/user/testKaptcha?t="+ new Date().getTime(),
后面加的時間參數是為了刷新url用的
前端的刷新函數就是將kaptcha重新賦值
refreshCode(){ console.log("測試切換驗證碼") this.kaptcha = "http://localhost:8082/user/testKaptcha?t="+ new Date().getTime() console.log(this.kaptcha) },
登錄首先會驗證 驗證碼的正確性,登陸成功進入主界面
驗證碼錯誤如下:
用戶名密碼錯誤如下:
登陸成功后請求校驗角色 校驗成功
請求校驗權限 校驗成功
退出,執行兩次,data為null
再次校驗權限,報出異常
藍藍設計建立了UI設計分享群,每天會分享國內外的一些優秀設計,如果有興趣的話,可以進入一起成長學習,請掃碼藍小助,報下信息,藍小助會請您入群。歡迎您加入噢~~希望得到建議咨詢、商務合作,也請與我們聯系。
文章來源:csdn 作者:黑胡子大叔的小屋
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
我們打開visual studio code , 選擇文件------------->將文件夾添加到工作區,導入我們的項目
導入后,我們安裝以下element
官網:https://element.eleme.cn/#/zh-CN/component/installation
安裝命令:npm add element-ui或者也可以用yarn
安裝完成后,我們在main.js中引入Element
import Vue from 'vue'
import App from './App'
import router from './router'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.config.productionTip = false
/* eslint-disable no-new */
Vue.use(ElementUI)
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
我們把components改名為views,并在目錄下添加3個頁面:Login.vue、Home.vue、404.vue。
頁面內容類似:
<template>
<div class="page">
<h2>Login Page</h2>
</div>
</template>
<script>
export default {
name: 'Login'
}
</script>
配置路由
打開router/index.js,添加3個路由分別對應主頁、登錄、404頁面
import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/views/Login'
import Home from '@/views/Home'
import NotFound from '@/views/404'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: Home
}, {
path: '/login',
name: 'Login',
component: Login
}, {
path: '/404',
name: 'notFound',
component: NotFound
}
]
})
配置完后啟動項目,在瀏覽器訪問測試
說明我們的配置已經生效了
安裝依賴:
npm uninstall sass-loader //卸載當前版本) npm install sass-loader@7.3.1 --save-dev //卸了重新安裝了一個低版本 npm install node-sass@4.14.1 --save-dev //安裝node-sass
安裝的時候注意對應版本,版本不對應,啟動會報錯
安裝后修改404頁面
<template>
<div class="site-wrapper site-page--not-found">
<div class="site-content__wrapper">
<div class="site-content">
<h2 class="not-found-title">404</h2>
<p class="not-found-desc">抱歉!您訪問的頁面<em>失聯</em>啦 ...</p>
<el-button @click="$router.go(-1)">返回上一頁</el-button>
<el-button type="primary" class="not-found-btn-gohome" @click="$router.push('/')">進入首頁</el-button>
</div>
</div>
</div>
</template>
<script>
export default {
name: '404'
}
</script>
<style lang="scss">
.site-wrapper.site-page--not-found {
position: absolute;
top: 60px;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
.site-content__wrapper {
padding: 0;
margin: 0;
background-color: #fff;
}
.site-content {
position: fixed;
top: 15%;
left: 50%;
z-index: 2;
padding: 30px;
text-align: center;
transform: translate(-50%, 0);
}
.not-found-title {
margin: 20px 0 15px;
font-size: 8em;
font-weight: 500;
color: rgb(55, 71, 79);
}
.not-found-desc {
margin: 0 0 30px;
font-size: 26px;
text-transform: uppercase;
color: rgb(118, 131, 143);
> em {
font-style: normal;
color: #ee8145;
}
}
.not-found-btn-gohome {
margin-left: 30px;
}
}
</style>
再瀏覽器訪問http://localhost:8080/#/404
可以看到樣式改變了
命令:npm install axios
安裝完成后修改Home頁面,進行一個簡單的測試
<template>
<div class="page">
<h2>Home Page</h2>
<el-button type="primary" @click="testAxios()">測試Axios調用</el-button>
</div>
</template>
<script>
import axios from 'axios'
import mock from '@/mock/mock.js'
export default {
name: 'Home',
methods: {
testAxios() {
axios.get('http://localhost:8080').then(res => { alert(res.data) })
}
}
}
</script>
可以看到我們的請求已經成功了
為了模擬后臺接口提供頁面需要的數據,引入mock.js
安裝依賴:npm install mockjs -dev
安裝完成,在src新建一個mock目錄,創建mock.js,在里面模擬兩個接口,分別攔截用戶和菜單的請求并返回相應數據。
import Mock from 'mockjs'
Mock.mock('http://localhost:8080/user', {
'name': '@name', // 隨機生成姓名
'name': '@email', // 隨機生成郵箱
'age|1-12': 7, // 年齡1-12之間
})
Mock.mock('http://localhost:8080/menu', {
'id': '@increment', // id自增
'name': 'menu', // 名稱為menu
'order|1-10': 6, // 排序1-10之間
})
修改Home.vue,在頁面添加兩個按鈕,分別觸發用戶和菜單請求。成功后彈出返回結果
注意:要在頁面引入mock import mock from '@/mock/mock.js'
Home.vue
<template>
<div class="page">
<h2>Home Page</h2>
<el-button type="primary" @click="testAxios()">測試Axios調用</el-button>
<el-button type="primary" @click="getUser()">獲取用戶信息</el-button>
<el-button type="primary" @click="getMenu()">獲取菜單信息</el-button>
</div>
</template>
<script>
import axios from 'axios'
import mock from '@/mock/mock.js'
export default {
name: 'Home',
methods: {
testAxios() {
axios.get('http://localhost:8080').then(res => { alert(res.data) })
},
getUser() {
axios.get('http://localhost:8080/user').then(res => { alert(JSON.stringify(res.data)) })
},
getMenu() {
axios.get('http://localhost:8080/menu').then(res => { alert(JSON.stringify(res.data)) })
}
}
}
</script>
點擊獲取用戶信息
點擊獲取菜單信息
可以看到我們已經得到響應數據,這樣mock就集成進來了
看完記得點贊哦
藍藍設計建立了UI設計分享群,每天會分享國內外的一些優秀設計,如果有興趣的話,可以進入一起成長學習,請掃碼藍小助,報下信息,藍小助會請您入群。歡迎您加入噢~~希望得到建議咨詢、商務合作,也請與我們聯系。
文章來源:csdn 作者:Java璐到底
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
今天彩云就跟大家分享一些Dribbble上,我這些年一直珍藏的10位圖標設計大佬們,我建議你一定要收藏學習。
首先推薦的就是這位大佬了,他在Drbbble上堅持了365天打卡練習,每天更新一張高質量圖標或者矢量小插圖設計,圖形簡單好看,非常值得借鑒和臨摹練習。
https://dribbble.com/dmitrilitvinov
一個在圖標設計上非常有造詣的自由設計師,他在圖標的造型和風格一致性上做的非常好,而且能看出來他的作品大部分都是能落地的稿子,值得學習。
https://dribbble.com/kairevicius
這位大神在設計圖標Logo的時候擅長用嚴謹的比例,而且他會把他的作圖過程和輔助線放出來,我覺得從這些作品中可以學到很多設計的思路,推薦給大家。
這些大神的作品喜歡加噪點,很有自己的風格特點,圖標都做的比較可愛。如果是做些偏萌系的設計,包括一些小表情之類的可以參考他的作品了。
專業的圖標設計師,在圖標設計這個領域相當專注,看他的個人主頁幾乎全是圖標的設計,以商業圖標設計居多。做項目的時候,非常值得參考了。
https://dribbble.com/lineicons
這位其實是一個圖標平臺的賬號,會定期更新大量的圖標資源,把它收藏起來當成靈感庫還不錯。
https://dribbble.com/lobanovskiy
這位大佬(也可能是一個團隊賬號)非常的全面,不僅僅只是在圖標領域,在3D、插畫、品牌、網頁、UI都頂級優秀。我個人非常喜歡他在圖標這塊的創意,能看到他展示了很多圖形的方案,過程稿,我覺得可以學習到很多,比如用到自己的作品集展示中,就很實用了。
這位大佬非常擅長動態圖標的設計,他做的圖標動效并不夸張,但卻是非常容易的落地到實際項目中。在動效創意如此流行的今天,他的這些作品一定可以給你提供靈感,專注在圖標動效上的作品并不多見,值得收藏。
https://dribbble.com/ZachRoszczewski
這位大佬在圖標的多種風格方面把握的都比較好,想要同時看多種風格搭配的可以收藏了。
最后推薦一位矢量風格圖標設計大神,他的作品風格獨特,擅長在圖標上疊加很多插畫形式的明暗紋理,形式感比較強,值得學習。
文章中列出來的這些是我從關注列表中再三篩選出來的比較有代表性的圖標設計大神,在我的工作學習過程中,他們給了我很多的靈感。當然,這份推薦名單只是我自己的個人喜好,無關粉絲數量,排名也不分先后。
這篇分享,一定是值得收藏的,不論是找靈感,還是臨摹學習,不用到處找,這10位大佬的作品就足夠你研究了。
在看的過程中,要學會去分析優秀的圖標,并用到自己的設計中。不管是練習還是實際項目,好的設計一定是有道理的,多想想別人為什么這么做。比如:為什么對方用這個顏色?顏色的配比怎樣?輔助圖形用的哪些?一套圖標中的輔助圖形如何做到豐富又不失一致性等等問題。不要只是依葫蘆畫瓢,要思考背后的道理,這個很重要。
最后,對于圖標設計本身,最重要的還是要保持練習,多看多做才能真正提升。
原文地址:彩云譯設計(公眾號) 作者:彩云Sky
轉載請注明:學UI網》講真,我從這些講真,我從這些Dribbble頂尖圖標大神的作品中學到很多,建議你一定要看看
藍藍設計建立了UI設計分享群,每天會分享國內外的一些優秀設計,如果有興趣的話,可以進入一起成長學習,請掃碼藍小助,報下信息,藍小助會請您入群。歡迎您加入噢~~希望得到建議咨詢、商務合作,也請與我們聯系。
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
優秀的設計師不僅要會設計,更要會包裝。經過設計師精心包裝的設計作品,總是更容易被甲方接受,經過精心包裝的作品集,自然也更受招聘公司的青睞。通過對國外優秀UI設計作品的歸納,以下總結出了下面三種UI作品的展示方法,希望能對大家有些幫助。
將部分模塊分離出頁面,通過陰影效果進行前后位置的劃分,有助于表現你對整個頁面層級的理解。UI不僅僅是簡單的時間工作,多展示你對整個框架和層級的思考,會讓作品集更加出彩。
藍藍設計建立了UI設計分享群,每天會分享國內外的一些優秀設計,如果有興趣的話,可以進入一起成長學習,請掃碼藍小助,報下信息,藍小助會請您入群。歡迎您加入噢~~希望得到建議咨詢、商務合作,也請與我們聯系。
文章來源:深海公眾號 作者:深海
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
西藏有一個六時書修行方法,確定要改進的一兩個弱點(比如懶惰,拖延),每天分六個時辰反省自己(間隔兩三個小時的樣子),在每個時間段有好的,不好的念頭或者行為都記下來反省總結,以便在下一個時間去調整)和鼓勵自己。這個弱點改的差不多了,就繼續下一個弱點作為目標去改正。讀富蘭克林的自傳,他基本上也是用這種方法,但沒有分每天六次這么細。
數據表單是一種常見的平臺設計樣式,在看似乏味呆板的設計中,沉淀著許多優質的設計體驗方法。尤其是在信息組織、信息傳達、信息承載和閱讀性方面,數據表單蘊含了許多設計規則和設計模式,使用戶能夠輕松地獲取、處理信息。該篇文章介紹了固定表頭、固定側欄、自定義欄、分頁器、過濾器、數據排序、多選項同時操作、簡單且簡約、普通的字體樣式、項目鏈接、鼠標懸停這 11 個設計指南,為大家提供有關數據表單設計的實用性建議。
當然,在實際的數據表單設計中,還需要根據產品要求和用戶目標進行相應的調整。你對數據表單設計有什么經驗體會?有過哪些很棒的設計案例?歡迎交流分享~
對于大多數 SaaS 平臺,數據表單是必不可少的組件,可讓用戶獲得相關數據和洞察,從而采取正確的決策。
作為一個前企業家,我使用過各種 SaaS 平臺,例如 Mailchimp,Shopify,Klaviyo,Zendesk 等。它們提供不同的服務 —— 電子郵件、訂單管理、客戶服務等解決方案,其共同點是,都需要將數據反饋給用戶。而數據表單則是傳輸大量數據最好的方法。
數據表單之所以有效,是因為它們能夠有序地組織信息和數據,使用戶能夠輕松地掃描、比較和分析自己選擇的信息。這篇文章介紹了我在設計數據表時遵循的關鍵設計模式。
這一點對于超過 30 行的表單尤其重要。當用戶必須向下滾動才能查看所有可用信息時,如果沒有固定表頭,用戶將很難理解和區分多行數據 —— 其中大部分可能是隨機數。一個固定的表頭可以幫助他們輕松地使用數據表,避免了向上滾動查看字段含義 。
設計提示
我傾向使用 8px 網格系統進行設計,將表頭尺寸保持在 16px(最小值)—— 防止你的設計看起來過于沉重和擁擠。
△ 當用戶不理解某個數據字段時,固定的表頭使得用戶無需重復向上滾動查看表頭內容
對于數據表單,需要兩個組件對所有信息進行排序。一是表頭,用于理解顯示的數據。二是固定側欄 ,與每一行數據相連接 ,常用于項目名稱,例如活動名稱、產品名稱、股票名稱等。
當數據表單需要水平滾動展示隱藏列時,通過固定第一列項目名稱,可以獲得與固定表頭相同的組件優勢,提升信息傳達效率。
設計提示
設計固定側欄時,請在該列的右側添加陰影和垂直分隔線,提示用戶該表支持水平滾動。
△ 當數據表有太多列時,通過固定第一列項目名稱,使表單更易理解
自定義欄允許用戶根據自己的偏好選擇表單顯示內容。當涉及多個指標和數據集時,該功能可以滿足不同的用戶目標 。常用于自助廣告平臺,例如 Facebook Ad Manager、Google Ads、AdRoll 等,在這些平臺上有多種營銷指標,每個用戶的優先級都不同。
△ 自定義欄允許用戶根據自己的喜好對數據表進行個性化設置
我從開發朋友那里學會了對表單進行分頁,通過限制正在處理的信息量,減少加載時間。另一種方法是使用漸進式加載,當鼠標滾動到最后一行時,表單自動加載一組新的數據。對比后者,分頁器允許用戶一次跳過好幾個組數據,滿足用戶非連續性瀏覽的需求。
設計提示
大多數表單每頁顯示行數可能超過 30 行,因此,將分頁器固定在表單頂部或底部會更加友好,方便用戶在頁面之間切換,無需過度地快速滾動。
△ 使用分頁可以減少加載時間,因為它限制了正在處理的信息量
過濾器組件對于篩選目標信息、屏蔽無關數據量至關重要。日期篩選是最基本的過濾器,能夠根據用戶指定日期來顯示信息。當每列具有固定的展示字段,這意味著信息不是隨機的,而是固定的選擇,您還可以設置單項信息的過濾器。
最好在過濾器下拉列表中提供復選功能,允許用戶選擇多個變量 —— 過濾系統越靈活,用戶就越容易操縱他們的信息。
△ 過濾器組件對于減少根據用戶要求顯示的數據量至關重要
排序類似于過濾,可以根據用戶的需要重新排列信息,調整信息展示順序。在大多數情況下,左列會對表單進行默認排序,用戶可以單擊標題對表單進行相應的排序設置。
您可以將排序添加到表頭中,例如按數字或字母順序對各個數據進行排序。但請不要濫用此功能,它對于狀態或類別等特定指標,可能是多余的 —— 過濾器處理這些數據會更合理。
設計提示
盡量避免使用線型圖標,選用面型圖標來增加可見性。懸停狀態能夠傳達整個區域可單擊的視覺提示 。
△ 排序類似于過濾,根據用戶的需要重新排列信息
復選框允許用戶選擇多個項目,并對所選項目執行某種操作 。幫助用戶節省時間和精力,不必重復相同的步驟。想象一下,所有的行都有相同的選框,這些選框會重復出現 —— 這會使你的表單看起來雜亂無章。
設計提示
我通常將復選框的大小保持在 24px(最小尺寸),居中布局,提高可用性。此外,高亮顯示被選定的行,增強對比性 。
△ 復選框允許用戶選擇多個項目并對所選項目執行操作
“極簡主義” 這個詞已經被廣泛地使用,空白似乎是現在的趨勢,但在這種情況下,少就是多。在設計數據表單時,重點應該放在數據本身而不是用戶界面上。用戶已經在與大量的數字和信息交互,復雜的界面只會增加用戶的認知負荷。
設計提示
沒有必要添加額外的視覺干擾,例如不必要的圖標、斑馬行、隨機顏色等。
△ 當你讓你的 UI 設計師瘋狂的時候會發生什么?
在設計中,排版是樣式指南中的一個關鍵元素,對于品牌推廣至關重要。但在設計表格時,您應該遵循上面的指示(簡單和簡約),不要在表格中使用任何復雜的字體樣式。
設計提示
沒有推薦的字體,但建議盡量避免使用襯線字體,因為它們往往會吸引人的注意力,導致額外的視覺負擔。此外,避免出現大寫單詞,它會使你的設計看起來沉重。
△ 襯線字體在表格上看起來很奇怪 —— 不知道你們是否看到過使用襯線字體的數據表單
對于特定的表單,項目名稱還可以充當鏈接,這是一種符合用戶習慣的交互形式,用戶很容易理解鏈接會將其帶到何處。
設計提示
設計文本鏈接時,請使用不同的顏色向用戶展示此鏈接 —— 僅在文本上加粗或設置下劃線并不能提供足夠的視覺提示。
設計文本鏈接時,請使用不同的顏色向用戶展示此鏈接
表單的操作通常放在最后一列。當沒有太多的信息列,需要水平滾動信息時,這種模式就很適合。也可以將操作放在第一列或第二列,這樣用戶就不需要在滾動時跟蹤這一行,但操作較多時,可能會產生認知過載,導致不必要的錯誤。
鼠標懸??梢员3趾喖s的外觀 —— 只有當用戶將鼠標懸停在相應的行上時, 操作圖標和文本才會出現。
以上內容只是原則性說明,主要為你提供一般性的建議,在實際的數據表單設計中,還需要根據具體的產品要求和用戶目標進行相應的調整。
文章來源:優設網 推薦:TCC翻譯情報局
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
面對國內外競品崛起,QQ 音視頻如何尋求突破,制定對用戶有價值的策略,贏得數據增長和用戶口碑。
要突破用戶增長停滯的困境,需從全局角度思考,對問題所處的系統進行分析與洞察,找出拓展應用場景的機會點,提供技術或價值創新的功能與服務。
首先,針對 QQ 音視頻所處行業的競爭態勢進行全面、系統的分析,包括內部優劣勢,以及外部機會和威脅,以此制定相應的策略與計劃。
關注競爭對手,更要研究用戶,通過用戶調研洞察用戶需求,找出 QQ 音視頻應持續鞏固優化,以及需考慮突破的使用場景。
綜觀音視頻的行業發展與科技趨勢,有三個主要的發展方向:
超高清視頻
5G 高帶寬、低時延將開拓更多玩法和業務場景,超高清視頻通話將帶來更豐富的感官體驗,猶如面對面溝通。
應用場景多元化
從使用場景來看,音視頻已走出會議室,從單純的溝通交流,擴展至豐富的應用場景,廣泛服務于人們的日常生活。
智能終端+音視頻云服務
未來智能終端將具備聯網連接的能力,搭配音視頻服務提供商的云服務,使得通話無所不在,可隨時通過智能終端進行音視頻通話。
綜合前述分析,提出 QQ 音視頻的用戶體驗愿景(UX Vision)和價值主張(Value Proposition)。
QQ 音視頻的本質在于溝通,其聲影重現的獨特優勢,消弭了空間距離。未來隨著通信技術的演進,高畫質、低時延、實時互動的感官體驗,將實現遠距臨場(Telepresence)的終極目標,使用戶感受到近乎面對面交流的體驗,就像在眼前〝一起做〞某些事。
2. 價值主張
綜上所述,提出 QQ 音視頻為用戶傳遞的價值——社交臨場感(Social Presence)。
臨場感相關理論與研究,源自 Short、Williams 和 Christie 等三位學者于 1976 年所提出的社會臨場感理論(Social Presence Theory),指的是雙方通過傳播媒介進行溝通的過程中,所能感受到對方真實存在的程度。QQ 音視頻憑借高度的媒介豐富性,提供實時響應、語言/非語言線索交互,使用戶能輕易地在社交互動中感知對方真實存在,進而在心理上獲得歸屬感與認同感。
根據過往文獻與媒介特性,將 QQ 音視頻的社交臨場感分為四個層次:
界面質量(Interface Quality)
界面可用性與視覺設計不會干擾操作,能讓用戶專注于音視頻溝通。
真實感(Realness)
音視頻的聲畫體驗及與人交流的方式,能貼近于真實世界。
互動性(Interactivity)
用戶之間的互動交流越強,越能感受到對方的存在。
情感傳達(Affective Expression)
通過情緒表達、環境氛圍的營造與對方建立連接,在心理上感到彼此的存在。
圍繞社交臨場感的價值主張,制定突破當前困境的目標與設計策略。音視頻產品除了廣泛應用在通訊場景,正逐漸賦能更多創新場景。基于 Q 群生態和調研結果,聚焦于生活、辦公、娛樂等場景進行探索。
兩個主要的設計目標:
重塑體驗
回歸通訊本質、夯實基礎體驗,對通話界面進行改版設計,讓用戶專注于音視頻溝通。
場景探索
開拓應用場景,通過社交臨場感賦能生活、辦公、娛樂等場景需求,為用戶創造價值。
針對 QQ 音視頻的應用場景,制定相應的設計策略。
界面質量是影響社交臨場感的關鍵要素,然而調研結果顯示,QQ 音視頻的通話質量和體驗落后競品,包括主界面功能復雜、通話流程痛點多、核心能力落后競品等體驗問題。
針對上述問題,提出 QQ 音視頻改版的設計思路:
1. 化繁為簡,回歸本質
分析不同通話類型的用戶習慣,以及各個功能的使用數據,區分功能優先級,分為核心功能、輔助功能、高級功能等三個層次,重新定義功能架構與布局。
2. 鏈路排查,解決痛點
運用認知走查(Cognitive Walkthrough)快速排查通話過程存在的用戶痛點,并將缺失的功能與信息補齊,最終解決超過 16 個以上的可用性問題。
3. 補齊短板,超越競品
對于視頻畫面的思考:
QQ 采用 1 大 N 小的畫中畫模式,適合 1 人主講、他人觀看的場景,主客位明顯,缺少一種與他人共在的感覺;反觀微信是 9 路宮格畫面,視頻畫面僅聚集在上半部,有利于節省流量,但同時也限制了屏幕利用率。
兼合上述兩者的優點,設計了 16 人視頻通話的功能:
16 人視頻通話也延伸了不同的創意玩法,例如祝朋友生日快樂、拼字表白或求婚。
QQ 視頻通話重度用戶的通話對象,主要來自戀人伴侶,這類用戶最希望獲得〝在一起〞的陪伴感,而社交臨場感的〝真實感〞是營造音視頻陪伴感的關鍵因素——彷佛對方就在眼前,兩個人一起聊天、一起做某些事。
1. 讓他 ? 她看到最好的你
聲影的真實感,是讓對方感受到你就在眼前的必要條件。真實感包括流暢度與清晰度,通過用戶行為分析,發現在不同場景下,用戶對視頻清晰度與流暢度有不同偏好 。
為此,QQ 音視頻開創了的高清 / 流暢模式,用戶能根據自身需求做選擇,切換自動、流暢 480P、高清 720P 三種模式,針對不同模式定制碼表,匹配不同的幀率和碼率。除此之外,QQ 音視頻也與手機廠商合作,開發 QQ 5G 1080P 超高清視頻通話,提前布局 5G 應用。
2. 記錄〝在一起〞的時刻
創造情感連接,有助于拉近彼此距離,營造陪伴感。有什么是情侶、閨蜜平時會做的事,因為分隔兩地不能一起做了?
循著這個思路,設計了全新的雙人合拍模式。距離遙遠的兩人,通過 QQ 音視頻實現留影合拍,讓彼此的情感得以沉淀。另外,為了增添合拍時的樂趣,該模式結合了 P 圖資源與人臉識別的能力,讓用戶可選用場景模版與掛件,創造屬于兩人的高光時刻。
3. 〝一起做〞某些事
情緒之間的感染,更能強化陪伴感與互動感,據此設計了第一款支持視頻聊天的一起看應用 ——視頻包廂。有別于Q群一起看的廣播形式,視頻包廂訴求更親密、1v1的陪伴感,彷佛對方坐在身邊,一邊看劇、一邊聊天。視頻包廂的價值,除了提升QQ音視頻的互動性,也為關系鏈活躍拓展應用場景。
線上會議是辦公用戶主要的使用場景,一方面將聚焦用戶核心需求,思考辦公場景的功能設計,另一方面增強用戶之間的互動性,讓會議更具社交臨場感,就像面對面溝通一樣高效。
1. 溝通更多元
為什么需要文字聊天?當主持人開啟全員靜音,此時無法回應講者;多人通話時,最多同時聽到 6 個人的聲音;不方便開麥或攝像頭,又想參與互動。
為了解決痛點、完善溝通形式,QQ 音視頻新增了文字聊天的功能,可通過手勢快速展開 / 收起聊天界面,并支持發送表情和文字消息;這些消息只會在通話界面沉淀,不影響未參與會議的群成員,避免不必要的打擾。
2. 輕松會議管理
對辦公用戶而言,線上會議需要完善的管理機制。QQ 音視頻嘗試結合 Q 群管家機器人,小成本實現通話預定。用戶能設置通話主題、開始時間、會議提醒,并支持分享會議鏈接到微信、QQ 空間等其他渠道;獲取鏈接的用戶,能快速拉起 QQ 加入會議。
3. 以我所?為你呈現
面對面分享是信息共享最有效率的方式,分享者能夠當面指出要點,有效地表達與溝通。QQ 音視頻借助系統鏡像投屏,實現邊通話、邊共享的能力。在商務會議中,將手機中的信息與操作,實時同步給其他參會成員,例如文件、照片、郵件內容等,并支持同步畫面、設備聲音。另外,諸如游戲分享、作業輔導、操作協助等場景,都能通過屏幕共享滿足需求,為用戶帶來實質性的效益。手機端最后一塊拼圖完成后,QQ 多端皆可實現屏幕共享,現已成為 QQ 音視頻的口碑能力。
疫情期間,線下聚會被迫停止,線上娛樂應運而生。在此契機之下,借助音視頻實時互動、聲影重現的特性,開啟了新的娛樂形態——QQ一起派對。
QQ 一起派對是基于音視頻展開的實時社交游戲,旨在打造真實聚會游戲體驗,讓游戲過程更貼近線下真實場景,就像面對面一起玩聚會游戲,能與好友邊玩游戲、邊聊天。通過環境氛圍的營造,包括場景具象化、增強代入感等方法,實時感知好友的情感變化,從而獲得社交臨場感,在心理上感到彼此的存在。
此外,有別于競品需通過點擊按鈕作答,QQ 一起派對利用實時語音識別,讓用戶通過更自然的人機交互方式,在游戲過程中進行語音搶答,彷佛置身于真實世界中與好友互動。
QQ 音視頻經過一系列的體驗重塑,獲得不錯的成果?;谏?、辦公、娛樂等場景深挖需求,新增許多實用功能和創新玩法,為用戶體驗和價值創新奠定良好的基礎,且歸功于這些積累,以致于在疫情期間,QQ 音視頻能不畏競品的挑戰。除此之外,多項新增能力,例如屏幕共享、視頻包廂、QQ 一起派對,獲得用戶廣泛好評;調研顯示,用戶樂于將 QQ 一起派對推薦給其他人,足見該功能獲得不錯的用戶口碑。
對于音視頻行業來說,疫情迎來的爆發式增長,無法保持高速成長態勢,會有一定程度的回落,但能提高大眾接受度和習慣養成,認識到音視頻能在一定程度上替代線下溝通,不僅限于通訊功能,音視頻應用擁有更多的可能性,正逐漸賦能更多創新場景。為此,QQ 音視頻基于社交臨場感,探索并賦能生活、辦公、娛樂等應用場景,提供技術或價值創新的功能與服務,為用戶創造獨特的價值。
面對市場上同類競品的崛起,作為一個 UX 設計師,除了因應產品思維設定目標,亦應思考如何以價值驅動解決問題:對于如何解決問題、邁向未來愿景,提出最適切、有效的方法。換言之,要關注的不只是當前面臨的問題,更重要的是擘畫用戶體驗愿景,提出該產品的價值主張,并制定相應的設計策略。
藍藍設計建立了UI設計分享群,每天會分享國內外的一些優秀設計,如果有興趣的話,可以進入一起成長學習,請掃碼藍小助,報下信息,藍小助會請您入群。歡迎您加入噢~~希望得到建議咨詢、商務合作,也請與我們聯系。
文章來源:優設 作者:騰訊ISUX
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
聽到<夜配色>這個詞,你腦海中會聯想到什么樣的色彩?或者什么樣的畫面?
可能最容易被人聯想到的就是黑色,因為夜晚給人的印象通常就是一片漆黑。如果繼續聯想,可能有的人也會想到藍色,因為真實的夜晚也并非純黑,但是這種藍一定是暗色的藍。
但是夜晚也并非什么都看不見,有時候也會有光亮,比如自然的星光或者人造的燈光,這種光可以是冷色的。
當然也可以是暖色的。這些情況基本上就是人們通常會聯想到的關于<夜配色>的色彩形象。
而讓大家失望的是,這些通常會想到的色彩印象其實都不屬于<夜配色>的形式,那到底什么是“夜配色”呢?
電影《綠皮書》海報
這個需要從第 91 屆奧斯卡金像獎最佳影片《綠皮書》說起,《綠皮書》的海報色彩很美:水藍色的汽車與天空色相呼應,司機也穿著藍色的上衣,整個畫面都散發著明凈的藍青色調,奇妙的是,后座的“主人”卻穿著一件咖啡色系的套頭衫,但是露出的襯領同樣也藍色系的,其實這種色彩組合就屬于<夜配色>。不過大家先別急,我們接著往下看。
電影《綠皮書》劇照
不僅電影海報如此,整部劇中的色彩搭配也始終保持著<夜配色>的基調。比如女演員明亮的藍色針織外套與褐色的裙子就形成了夜配色,其他場景也是如此,這正是它迷人的地方。電影《綠皮書》講述的是一段跨越種族、階級的友誼故事,而<夜配色>恰恰具有民族的、世界的、神秘的、共通的色彩屬性。
具有以上這些屬性,很大程度上是因為這種色彩搭配的不常見性,不常見不代表就沒有規律可循,任何色彩搭配必定有它的內在規律,我們不能只看表面,抓到本質才能掌握它的內核。我們把剛剛提取的這幾組顏色排列出來分析一下。
我們看這些色彩有什么共性:上方的顏色純度或明度都是在偏低的范圍,而下方顏色的純度或明度都普遍偏高。如果再結合冷暖色來看我們會發現,上方都是暖色系,而下方都是冷色系。
因此我們可以總結出:低純度或低明度的暖色搭配高純度或高明度的冷色,這種配色形式就是<夜配色>,也叫<影印色>。
如果大家難以理解,我們可以再通俗一點,就是暗濁的暖色搭配鮮亮的冷色。嚴格來講<夜配色>并不算什么真正的配色技巧,無非屬于冷暖色搭配范疇,但是因為搭配在一起很特別,所以我們就把它單獨拿出來作為一種搭配原則來探討。
異域情調
最早引入<夜配色>這一概念的是服裝領域。很多咖啡色系的衣裙如果只是搭配黑色或米色等色系,整體上雖然和諧但也會顯得單調和乏味,所以就會加入藍色系,這時就會有不一樣的感覺。并且<夜配色>的搭配具有高級感,比如藍色的牛仔褲就可以試試搭配駝色上衣。
復古紳士
而且<夜配色>的搭配男女通用,比如男士的話可以是水綠色的毛衣搭配棕色西裝外套,或是是領帶、胸花都可以。男士使用夜配色,很有時尚感,會顯得精致典雅,細節處彰顯低調華麗的質感,整體具有復古紳士的浪漫氣息。
魅力碰撞
使用<夜配色>也可以考慮面積配比,想要冷暖碰撞的效果,通常情況下就是在上半身和下半身使用一對夜配色,比如葡萄酒色配天青藍,充滿異域風情。
點睛之筆
當然也可以用點綴的方式來使用<夜配色>,比如用鮮亮的冷色搭配低明度的咖啡色,用鮮亮的冷色來點綴,展示高級感,對于服裝搭配而言,衣領、袖口、包、鞋這些都可以是點綴色的應用載體。
對于這些<夜配色>在服裝領域的應用,我們可以將它們提取出來,按照暖色和冷色重新擺放,然后旋轉角度,不知道大家發現什么沒有,其實<夜配色>的獨特美感,像極了天空色與大地色的碰撞。
雖然到這里大家對<夜配色>已經有了一些基本的了解,但是想要良好的運用到設計當中,還需要繼續加深理解,畢竟我們之前所看到的都是電影或者服裝領域的<夜配色>,而我們切入的角度,就是先排除那些不屬于<夜配色>的形式。
回到之前我們總結的,首先<夜配色>包含一種冷色和一種暖色,然后色調上分為暗濁的和鮮亮的,所以它們兩兩組合就會出現四種形式:也就是暗濁的冷色搭配鮮亮的冷色、暗濁的冷色搭配鮮亮的暖色、暗濁的暖色搭配鮮亮的暖色、暗濁的暖色搭配鮮亮的冷色。
暗濁的<冷色>——鮮亮的<冷色>
首先第一種就是暗濁的冷色搭配鮮亮的冷色,這種配色形式很常用,比如通常我們確定一個背景色之后,上方的文字色或元素色就會在背景色的基礎上改變純度或明度。
暗濁的<冷色>——鮮亮的<暖色>
第二種就是暗濁的冷色搭配鮮亮的暖色,這種比第一種更常見,因為這種形式就是我們通常所講的冷暖搭配,一冷一暖,一亮一暗。
暗濁的<暖色>——鮮亮的<暖色>
第三種形式就是暗濁的暖色搭配鮮亮的暖色,這種搭配形式整體方向也是比較偏向統一協調的,不會有太大的問題,但也很難給人眼前一亮的視覺沖擊。
暗濁的<暖色>——鮮亮的<冷色>
最后一種就是暗濁的暖色搭配鮮亮的冷色,也就是我們這期教程中所講的<夜配色>,這種配色形式運用到平面設計領域,整體上還是可以保留它的氣質和風格的:神秘、充滿異域風情,當然我認為這些屬性并不是一成不變的,但是不變的是<夜配色>會給人不一樣的感覺,因為通常人們都不太會這么配色,所以我們什么情況下去使用<夜配色>,相信大家心理已經有答案了吧。接下來我們來嘗試幾個案例。
這是一個人物插畫,首先我們先將人物皮膚的顏色填充進來,然后是箱子。
接下來就是為人物的穿著填色,這時候我們就可以回想之前我們列舉的服裝搭配的例子了,我們可以將褲子的顏色填充一個大地色。
然后是上衣的顏色,如果是常規的人物插畫配色,通常情況下衣服都會選擇一個跟褲子顏色相統一和諧的顏色,但是如果我們想要一些不一樣的效果,這時就可以使用<夜配色>,將衣服的顏色填充一個天藍色,也是我們之前所講的碰撞色,因為面積相當。
當然,我們也可以將褲子填充水藍色,有些類似牛仔褲的顏色,衣服填充一個咖色,這也是一種<夜配色>。
或者我們也可以使用點綴色的形式,比如整體顏色是咖色,但是我們可以在衣領、袖口、褲腳這些小面積區域填充亮冷色。
再或者是鞋子的顏色也可以,或者再大膽一些給頭發染個色。
我們也可以嘗試一個網頁首屏,比如畫面中這種色調的圖片現在很流行,本身就很適合我們這期內容所講<夜配色>。
如果我們添加白色文字,雖然識別度很好,但是不會給人眼前一亮的感覺,會稍顯平庸。
這時我們就可以嘗試<夜配色>的搭配,當然文字色可以有選擇,都可以嘗試一下,甚至是偏暖一些的文字也不是說就不可以,我們總結出的規律要為我們所用,而不要被規律所限制,希望大家能明白這個道理,千萬不要理解的太死板。
最后我又嘗試了一個海報,海報的主題是“南非當代攝影展”,這個名字就會讓我腦海中浮現出異域風情,所以我認為可以使用夜配色。
先將圖片置入到版面當中,而這張圖片本身就是大地色調,因此也是適合<夜配色>搭配的。
所以上方的背景色我們就可以填充一個天藍色,現在整體上下色調對比就是<夜配色>的形式。
文字這里我們也可以不使用黑色,替換成大地色,下方的小字使用上方的藍色。但是做到這里我感覺畫面還缺少點什么,也就是整個版面的構圖和文字編排都很規矩和工整,這時候就需要有一些破壞這種工整的元素。
所以我就在右下方這里添加了一個手寫體的英文,為這個畫面增加一些靈動,當然顏色上也是吸取上方的藍色,形成呼應。
藍藍設計建立了UI設計分享群,每天會分享國內外的一些優秀設計,如果有興趣的話,可以進入一起成長學習,請掃碼藍小助,報下信息,藍小助會請您入群。歡迎您加入噢~~希望得到建議咨詢、商務合作,也請與我們聯系。
文章來源:優設 作者:研習社
分享此文一切功德,皆悉回向給文章原作者及眾讀者.
免責聲明:藍藍設計尊重原作者,文章的版權歸原作者。如涉及版權問題,請及時與我們取得聯系,我們立即更正或刪除。
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
很多網站仍然在使用老舊的頁面設計,比如國內一些企業官網,萬年不變的相類似的模板,外國的則是hero頁面,帶CTA按鈕,三欄式的布局。這些設計不能說是不好的設計,很實用,用戶能夠預測展示的內容,也容易找到需要的內容。但是正因為可預測,用戶沒有新鮮感,沒有期待,所以我們找了一些不僅打破常規,也依然有良好用戶體驗的網頁設計。
藍藍設計(北京蘭亭妙微科技有限公司)是一家專注而深入的UI設計公司,公司對UI設計的追求一向很高,致力于為卓越的國內外企業提供卓越的手機app/安卓ui設計、軟件界面設計、網站設計,用戶研究、交互設計服務。
接下來是精彩的UI設計賞析
藍藍設計秉承設計優秀,不斷超越的理念,誠信敬業、專業耐心的工作作風,一直堅持注重用戶心理體驗及“設計與營銷”等領域的理論與實踐相結合。10余年專注努力,300+案例磨練。我們在ui創意設計,用戶體驗與交互設計,各種類型軟件界面設計,國際化標準和流行趨勢,進行過不斷的學習和實踐。藍藍設計提供的是可以信賴的ui設計服務,我們內部有一套管理要求,比如去客戶現場每周一次的檢視和溝通、內部提案會議、每天下班前的整理反饋成果發郵件、隨時溝通的qq、電話,階段性的匯報和進度記錄整理。多勞多得的獎勵機制,客戶滿意度評價獎勵機制,鼓勵大家用心、平和、耐心、勤奮、創新的做事.
(以上圖片均來源于網絡)
(精美流程圖設計)
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
更多精彩文章:
ui界面設計之網站設計案例欣賞(一)
表格和網格一直是產品和后臺面板的重要UI組件。然而,即使到了今天,也很容易找出設計不當或用戶體驗不好的數據表。
今天我們邀請了 Gil Bouhnick ,來跟大家一起聊一聊「如何設計出一個可觀的數據表」。
接下來,就進入正題吧~
我使用過 30 多種 SaaS 工具和 SDK ,經常用它們的后臺面板看數據,觀察到了一些問題并做了反饋。實話說,它們在UI設計和基本功能上,對表格的展現形式還挺糟糕的(雖然它們都是很好的Saas產品)。
鑒于我開發使用表格 20 年的經驗,決定為大家來總結一下最基本的處理表格的UI / UX規則了。
本文將從功能、UI設計和美觀性,這三個方面來闡述。
1.從排序和篩選開始:
是的,我也知道排序和篩選很煩人,我也討厭他們,但必須先說它們。除非你打算做一個不到十條記錄或者類似情況的精美表格。
排序和篩選不僅是可用性或易用性的功能,更是摘要數據的關鍵:了解趨勢,比較記錄,查找特定值,沒有排序和篩選就無法完成這些工作。
如果添加基于列的篩選(如日期,布爾值,字符串等元數據)成本太高,那么可以從更基本的內容開始篩選,例如現代搜索框或一些常用的硬編碼。
2.確保列的大小可調整
這是一種查看信息的方法,無需深入研究每條記錄。
不管你的列寬有多大,也不要將列表固定住,因為有時候用戶需要進一步擴展。
3.允許列重新排序
對于大型表格,不能一種大小適用于所有情況。
通過重新排序列,用戶可以設置符合它們特定需求的表格。這個相對小的功能可以減少混亂,并節省一些來回滾動。
這時,列的名稱體現了大量信息。重新排序列能夠使用戶可以專注于特定區域并理解小塊信息。
4.內聯的編輯
有許多用于編輯單元格的UI解決方案:內聯框,彈出窗口,可擴展節等。
我不認為上面的方案是最佳的,因為它依賴于功能、數據類型、使用案例。從用戶角度出發,我最喜歡的方案是側視圖(快速視圖)。
一旦選擇一個項目,它就會從側面彈出的方法。
這個方案,我最喜歡的是它可以保持上下文(與彈出窗口不同),易于使用,即使是在垂直滾動視圖中顯示大量字段的情況下也效果良好。
5.小屏幕設計
當然,大顯示器看表格,設計和開發都會很爽,但是還是有人在用筆記本電腦或iPad!他們又做錯了什么!/手動狗頭
不停的滾動、縮小的列、隱藏的文本,這些都會干擾用戶體驗,所以一定要在小屏幕上驗證你的設計,并且盡可能使用上一段時間去切實體驗用戶使用情況(而不是隨意測試)。
如果確實很糟糕,你可能需要在解決問題上發揮自主創造力,不過前提是你很了解它。
6.彩色交替行
使用大型數據表時,很容易丟失。
斑馬紋的表格可以幫助用戶保持其位置,但是彩色的行必須非常淺,否則會引起誤導,看起來像選定的行。
使用淺色時,應該將所有內容設置為淺灰色,避免使用黑色線條和深色邊框。
7.使用固定表頭和“凍結”列錨定一些標識符
我認為在任何屏幕上顯示大量數據時,表格都應該能正常瀏覽。
要瀏覽數據,用戶就需要經常滾動,這意味著需要一些錨點來輔助:
向下滾動時,列標題必須固定(這是最基本的)
第一列應該被鎖定(MS Excel和Google Sheets稱其為“凍結”),因為,當你水平滾動時,需要該行聯系上下文。
一個全行選擇選項,用于在水平滾動時標記重要行。
8.設置固定的行高
表格和網格是體現結構化信息的,但是,當表格的列寬、行高不同時,就會變得混亂,信息也就會變得沒有體系。
因此,為了整體的可用性和美觀性,我覺得不管內容如何,所有行都應具有完全相同的高度。
為了更好地支持多行文本塊,請考慮以下事項:
用換行替換為空格,并將整個文本變成一行(通過調整列寬的大?。?/span>
將(所有行的)行高設置為2行而不是1行(可以解決某些情況)
使用工具提示(可以用,但是最好不要用)
通過單擊行來展開/折疊(剛需)
采用浮動側視圖顯示所選行的詳細信息。
美學設計可以帶來更好的使用感。
以下是通過簡單的UI設計修改來消除雜亂并提高內容可讀性的幾種方法:
9.增加單元格填充
使用空格。
加載信息的表,正是用戶想要看到更多空白的地方,即使這會花費他們一些額外的滾動時間。
10.消除不必要的邊框
一旦數據結構良好并且留有空白,就該擺脫那些多余的邊框,或者讓邊框線條變得超細且顏色淺。
文章來源:優設網 推薦:墨刀_MockingBot
藍藍設計( www.syprn.cn )是一家專注而深入的界面設計公司,為期望卓越的國內外企業提供卓越的UI界面設計、BS界面設計 、 cs界面設計 、 ipad界面設計 、 包裝設計 、 圖標定制 、 用戶體驗 、交互設計、 網站建設 、平面設計服務
藍藍設計的小編 http://www.syprn.cn