• 校园商铺-8商品模块-3Shop使用ImageHolder后的重构


    可以用ImageHolder封装的类对shop进行重构

    1 ShopService

    package com.csj2018.o2o.service;
    
    import java.io.InputStream;
    
    import com.csj2018.o2o.entity.Shop;
    import com.csj2018.o2o.exceptions.ShopOperationException;
    import com.csj2018.o2o.dto.ImageHolder;
    import com.csj2018.o2o.dto.ShopExecution;
    public interface ShopService {
    	/**
    	 * 根据shopCondition分页返回相应店铺列表
    	 * @param shopCondition
    	 * @param pageIndex
    	 * @param pageSize
    	 * @return
    	 */
    	public ShopExecution getShopList(Shop shopCondition,int pageIndex,int pageSize);
    	/**
    	 * 通过店铺ID获取店铺信息
    	 * @param shopId
    	 * @return
    	 */
    	Shop getByShopId(long shopId);
    	/**
    	 * 更新店铺信息,包括对图片的处理
    	 * @param shop
    	 * @param shopImgInputStream
    	 * @param fileName
    	 * @return
    	 */
    	ShopExecution modifyShop(Shop shop,ImageHolder fileName) throws ShopOperationException;
    	/**
    	 * 注册店铺信息,包括图片处理
    	 * @param shop
    	 * @param shopImgInputStream
    	 * @param fileName
    	 * @return
    	 */
    	ShopExecution addShop(Shop shop,ImageHolder fileName) throws ShopOperationException;
    }
    

    2 修改实现类

    package com.csj2018.o2o.service.impl;
    
    import java.io.InputStream;
    import java.util.Date;
    import java.util.List;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import com.csj2018.o2o.dao.ShopDao;
    import com.csj2018.o2o.dto.ImageHolder;
    import com.csj2018.o2o.dto.ShopExecution;
    import com.csj2018.o2o.entity.Shop;
    import com.csj2018.o2o.enums.ShopStateEnum;
    import com.csj2018.o2o.exceptions.ShopOperationException;
    import com.csj2018.o2o.service.ShopService;
    import com.csj2018.o2o.util.ImageUtil;
    import com.csj2018.o2o.util.PageCalculator;
    import com.csj2018.o2o.util.PathUtil;
    
    @Service
    public class ShopServiceImpl implements ShopService {
    	private Logger logger = LoggerFactory.getLogger(ShopServiceImpl.class);
    
    	@Autowired
    	private ShopDao shopDao;
    	@Override
    	public ShopExecution getShopList(Shop shopCondition,int pageIndex,int pageSize) {
    		int rowIndex = PageCalculator.calculatorRowIndex(pageIndex, pageSize);
    		List<Shop> shopList = shopDao.queryShopList(shopCondition, rowIndex, pageSize);
    		int count = shopDao.queryShopCount(shopCondition);
    		ShopExecution se = new ShopExecution();
    		if(shopList != null) {
    			se.setShopList(shopList);
    			se.setCount(count);
    		}else {
    			se.setState(ShopStateEnum.INNER_ERROR.getState());
    		}
    		return se;
    	}
    	
    	@Override
    	@Transactional
    	public ShopExecution addShop(Shop shop, ImageHolder thumbnail) {
    
    		// 控制判断,shop是不是包含必需的值
    		if (shop == null) {
    			logger.warn("shop== null");
    			return new ShopExecution(ShopStateEnum.NUll_SHOP);
    		}
    		// 增加对Shop其他引入类非空的判断
    		try {
    			// 给店铺信息赋初始值
    			shop.setEnableStatus(0);
    			shop.setCreateTime(new Date());
    			shop.setLastEditTime(new Date());
    			// 添加店铺信息
    			int effectedNum = shopDao.insertShop(shop);
    			logger.warn("添加结果:" + effectedNum + "shopId:" + shop.getShopId());
    			if (effectedNum <= 0) {
    				throw new ShopOperationException("店铺创建失败");
    			} else {
    				if (thumbnail.getImage() != null) {
    
    					// 存储图片
    					try {
    						addShopImage(shop, thumbnail);
    					} catch (Exception e) {
    						throw new ShopOperationException("addShopImg error:" + e.getMessage());
    					}
    					// 更新店铺的图片信息
    					effectedNum = shopDao.updateShop(shop);
    					if (effectedNum <= 0) {
    						throw new ShopOperationException("更新图片地址失败");
    					}
    				}
    			}
    		} catch (Exception e) {
    			throw new ShopOperationException("addShop error:" + e.getMessage());
    		}
    		return new ShopExecution(ShopStateEnum.CHECK, shop);
    	}
    
    	private void addShopImage(Shop shop, ImageHolder thumbnail) {
    		// 获取shop图片目录的相对路径
    		String dest = PathUtil.getShopImagePath(shop.getShopId());
    		String shopImgAddr = ImageUtil.generateThumbnail(thumbnail, dest);
    		shop.setShopImg(shopImgAddr);
    	}
    
    	@Override
    	public Shop getByShopId(long shopId) {
    		return shopDao.queryByShopId(shopId);
    	}
    
    	@Override
    	public ShopExecution modifyShop(Shop shop, ImageHolder thumbnail)
    			throws ShopOperationException {
    		if (shop == null || shop.getShopId() == null) {
    			return new ShopExecution(ShopStateEnum.NUll_SHOP);
    		} else {
    			try {
    				// 1.判断是否需要处理图片
    				if (thumbnail.getImage() != null && thumbnail.getImageName() != null && !"".equals(thumbnail.getImageName())) {
    					Shop tempShop = shopDao.queryByShopId(shop.getShopId());
    					if (tempShop.getShopImg() != null) {
    						//编写工具类,删除图片信息 
    						ImageUtil.deleteFileOfPath(tempShop.getShopImg());
    					}
    					addShopImage(shop, thumbnail);
    				}
    				// 2.更新店铺信息
    				shop.setLastEditTime(new Date());
    				int effectedNum = shopDao.updateShop(shop);
    				if (effectedNum <= 0) {
    					return new ShopExecution(ShopStateEnum.INNER_ERROR);
    				} else {
    					shop = shopDao.queryByShopId(shop.getShopId());
    					return new ShopExecution(ShopStateEnum.SUCCESS, shop);
    				}
    			} catch (Exception e) {
    				throw new ShopOperationException("modifyShop error:" + e.getMessage());
    			}
    		}
    	}
    }
    

    3 修改controller层

    package com.csj2018.o2o.web.shopadmin;
    
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import javax.servlet.http.HttpServletRequest;
    
    import java.io.InputStream;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.multipart.MultipartHttpServletRequest;
    import org.springframework.web.multipart.commons.CommonsMultipartFile;
    import org.springframework.web.multipart.commons.CommonsMultipartResolver;
    
    import com.csj2018.o2o.dto.ImageHolder;
    import com.csj2018.o2o.dto.ShopExecution;
    import com.csj2018.o2o.entity.Area;
    import com.csj2018.o2o.entity.PersonInfo;
    import com.csj2018.o2o.entity.Shop;
    import com.csj2018.o2o.entity.ShopCategory;
    import com.csj2018.o2o.enums.ShopStateEnum;
    import com.csj2018.o2o.exceptions.ShopOperationException;
    import com.csj2018.o2o.service.AreaService;
    import com.csj2018.o2o.service.ShopCategoryService;
    import com.csj2018.o2o.service.ShopService;
    import com.csj2018.o2o.util.CodeUtil;
    import com.csj2018.o2o.util.HttpServletRequestUtil;
    import com.csj2018.o2o.util.ImageUtil;
    import com.csj2018.o2o.util.PathUtil;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    // 首先在Controller里面定义了SpringMVC相关的标签,这个标签包含了Controller的访问路径以及registerregisterShop方法的访问路径,
    @Controller
    @RequestMapping("/shopadmin")
    public class ShopManagerController {
    	// 同时给它在执行的时候通过Spring容器注入之前实现好的ShopService实现类,用来提供addShop的服务。
    	@Autowired
    	private ShopService shopService;
    	@Autowired
    	private ShopCategoryService shopCategoryService;
    	@Autowired
    	private AreaService areaService;
    	
    	//6-7新增
    	@RequestMapping(value="/getshopmanagementinfo",method=RequestMethod.GET)
    	@ResponseBody
    	private Map<String,Object> getShopManagementInfo(HttpServletRequest request){
    		Map<String,Object> modelMap = new HashMap<String,Object>();
    		long shopId = HttpServletRequestUtil.getLong(request, "shopId");
    		if(shopId <= 0) {
    			Object currentShopObj = request.getSession().getAttribute("currentShop");
    			if(currentShopObj == null) {
    				modelMap.put("redirect",true);
    				modelMap.put("url","/o2o/shopadmin/shoplist");
    			}else {
    				Shop currentShop = (Shop) currentShopObj;
    				modelMap.put("redirect", false);
    				modelMap.put("shopId",currentShop.getShopId());
    			}
    		}else {
    			Shop currentShop = new Shop();
    			currentShop.setShopId(shopId);
    			request.getSession().setAttribute("currentShop", currentShop);
    			modelMap.put("redirect", false);
    		}
    		return modelMap;
    	}
    	@RequestMapping(value="/getshoplist",method=RequestMethod.GET)
    	@ResponseBody
    	public Map<String,Object> getShopList(HttpServletRequest request){
    		Map<String,Object> modelMap = new HashMap<>();
    		//确保user不为空
    		PersonInfo user = new PersonInfo();
    		user.setUserId(1L);
    		user.setName("test");
    		request.getSession().setAttribute("user", user);
    		user = (PersonInfo) request.getSession().getAttribute("user");
    		try {
    			Shop shopCondition = new Shop();
    			shopCondition.setOwner(user);
    			ShopExecution se = shopService.getShopList(shopCondition, 0, 100);
    			modelMap.put("shopList", se.getShopList());
    			modelMap.put("user", user);
    			modelMap.put("success", true);
    		}catch (Exception e) {
    			modelMap.put("success", false);
    			modelMap.put("errMsg", e.getMessage());
    		}
    		return modelMap;
    	}
    	
    	@RequestMapping(value="/getshopbyid",method=RequestMethod.GET)
    	@ResponseBody
    	private Map<String,Object> getShopById(HttpServletRequest request){
    		Map<String,Object> modelMap = new HashMap<String,Object>();
    		Long shopId = HttpServletRequestUtil.getLong(request, "shopid");
    		if(shopId > -1) {
    			try {
    				Shop shop = shopService.getByShopId(shopId);
    				List<Area> areaList = areaService.getAreaList();
    				modelMap.put("shop", shop);
    				modelMap.put("areaList", areaList);
    				modelMap.put("success", true);
    			}catch (Exception e) {
    				modelMap.put("success", false);
    				modelMap.put("errMsg", e.toString());
    			}
    		}else {
    			modelMap.put("success", false);
    			modelMap.put("errMsg", "empty shopId");
    		}
    		return modelMap;
    	}
    	@RequestMapping(value = "getshopinitinfo",method = RequestMethod.GET)
    	@ResponseBody
    	private Map<String,Object> getShopInitInfo(HttpServletRequest request){
    		Map<String,Object> modelMap = new HashMap<String,Object>();
    		List<ShopCategory> shopCategoryList = new ArrayList<ShopCategory>();
    		List<Area> areaList = new ArrayList<Area>();
    		try {
    			shopCategoryList = shopCategoryService.getShopCategoryList(new ShopCategory());
    			areaList = areaService.getAreaList();
    			modelMap.put("shopCategoryList", shopCategoryList);
    			modelMap.put("areaList", areaList);
    			modelMap.put("success",true);
    		}catch(Exception e) {
    			modelMap.put("success",false);
    			modelMap.put("errMsg", e.getMessage());
    		}
    		return modelMap;
    		
    	}
    	@RequestMapping(value = "/registershop", method = RequestMethod.POST)
    	@ResponseBody
    	private Map<String, Object> registerShop(HttpServletRequest request) {
    		Map<String, Object> modelMap = new HashMap<String, Object>();
    		//1.验证码校验
    		if(!CodeUtil.checkVerifyCode(request)) {
    			modelMap.put("success", "false");
    			modelMap.put("message", "输入了错误的验证码");
    			return modelMap;
    		}
    		//2.获取请求头的店铺信息 接收从前台传递的shopStr对象,将这个对象转换成Shop实体类
    		String shopStr = HttpServletRequestUtil.getString(request, "shopStr");
    		ObjectMapper mapper = new ObjectMapper();
    		Shop shop = null;
    		try {
    			shop = mapper.readValue(shopStr, Shop.class);
    		} catch (Exception e) {
    			modelMap.put("success", false);
    			modelMap.put("errMeg", e.getMessage());
    			return modelMap;
    		}
    		// 3.获取图片 将请求中的文件流从CommonsMultipartResolver解析为CommonsMultipartFile
    		CommonsMultipartFile shopImg = null;
    		CommonsMultipartResolver commonsMultipartResolver = new CommonsMultipartResolver(
    				request.getSession().getServletContext());
    		if (commonsMultipartResolver.isMultipart(request)) {
    			MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest) request;
    			shopImg = (CommonsMultipartFile) multipartHttpServletRequest.getFile("shopImg");
    		} else {
    			modelMap.put("success", false);
    			modelMap.put("errMsg", "上传图片不能为空");
    			return modelMap;
    		}
    		// 4.注册店铺
    		if (shop != null && shopImg != null) {
    			/*
    			 * 添加Session
    			 * 注册店铺或对店铺做操作,是需要登陆的
    			 */
    			PersonInfo owner = new PersonInfo();
    			owner.setUserId(1L);
    			request.getSession().setAttribute("user", owner);
    			PersonInfo owner2 = (PersonInfo) request.getSession().getAttribute("user");
    			shop.setOwner(owner2);
    			
    			/*
    			 * 由于addShop的第二个参数是File类型的,而传入的ShopImg是CommonsMultipartFile这样的一个类型,因此需要将CommonsMultipartFile转换成File类型
    			 File shopImgFile = new File(PathUtil.getImgBasePath() + ImageUtil.getRandomFileName());
    			try {
    				shopImgFile.createNewFile();
    			} catch (IOException e) {
    				modelMap.put("success", false);
    				modelMap.put("errMsg", e.getMessage());
    				return modelMap;
    			}
    
    			try {
    				inputStreamToFile(shopImg.getInputStream(), shopImgFile);
    			} catch (Exception e) {
    				modelMap.put("success", false);
    				modelMap.put("errMsg", e.getMessage());
    				return modelMap;
    			}
    			 */
    			ShopExecution se;
    			try {
    				ImageHolder imageHolder = new ImageHolder(shopImg.getOriginalFilename(),shopImg.getInputStream());
    				se = shopService.addShop(shop, imageHolder);
    				if (se.getState() == ShopStateEnum.CHECK.getState()) {
    					/*
    					 * 在店铺添加完成后,还需要做Session的操作。用户和店铺的关系是一对多的,即一个owner能够创建多个店铺。
    					 * 因此需要在Session里面保存一个店铺列表来显示用户可以操作的店铺。
    					 */
    					@SuppressWarnings("unchecked")
    					List<Shop> shopList = (List<Shop>) request.getSession().getAttribute("shopList");
    					if(shopList == null || shopList.size() == 0) {
    						shopList = new ArrayList<Shop>();
    					}
    					shopList.add(se.getShop());
    					request.getSession().setAttribute("shopList", shopList);
    					modelMap.put("success", true);
    				} else {
    					modelMap.put("success", false);
    					modelMap.put("errMsg", se.getStateInfo());
    				}
    
    			}catch (ShopOperationException e) {
    				modelMap.put("success", false);
    				modelMap.put("errMsg", e.getMessage());
    			}catch (IOException e) {
    				modelMap.put("success", false);
    				modelMap.put("errMsg", e.getMessage());
    			}
    			return modelMap;
    		} else {
    			modelMap.put("success", false);
    			modelMap.put("errMsg", "请输入店铺信息");
    			return modelMap;
    		}
    
    	}
    	
    	@RequestMapping(value = "/modifyshop", method = RequestMethod.POST)
    	@ResponseBody
    	private Map<String, Object> modifyShop(HttpServletRequest request) {
    		Map<String, Object> modelMap = new HashMap<String, Object>();
    		//1.验证码校验
    		if(!CodeUtil.checkVerifyCode(request)) {
    			modelMap.put("success", "false");
    			modelMap.put("message", "输入了错误的验证码");
    			return modelMap;
    		}
    		
    		//2.获取请求头的店铺信息 接收从前台传递的shopStr对象,将这个对象转换成Shop实体类
    		String shopStr = HttpServletRequestUtil.getString(request, "shopStr");
    		ObjectMapper mapper = new ObjectMapper();
    		Shop shop = null;
    		try {
    			shop = mapper.readValue(shopStr, Shop.class);
    		} catch (Exception e) {
    			modelMap.put("success", false);
    			modelMap.put("errMeg", e.getMessage());
    			return modelMap;
    		}
    		// 3.获取图片 将请求中的文件流从CommonsMultipartResolver解析为CommonsMultipartFile
    		CommonsMultipartFile shopImg = null;
    		CommonsMultipartResolver commonsMultipartResolver = new CommonsMultipartResolver(
    				request.getSession().getServletContext());
    		if (commonsMultipartResolver.isMultipart(request)) {
    			MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest) request;
    			shopImg = (CommonsMultipartFile) multipartHttpServletRequest.getFile("shopImg");
    		}
    		
    		// 2.修改店铺
    		if (shop != null && shop.getShopId() != null) {
    			//由于图片是可上传、可不上传的,因此图片非空判断去除;取而代之确保shopId不为空
    			//修改店铺信息不用从session获取用户信息
    			PersonInfo owner = new PersonInfo();
    			// 预期从Session获取,目前自定义,以后完善
    			owner.setUserId(1L);
    			shop.setOwner(owner);
    			// 由于addShop的第二个参数是File类型的,而传入的ShopImg是CommonsMultipartFile这样的一个类型,因此需要将CommonsMultipartFile转换成File类型
    /*
     * 这块代码,教程没有
    			File shopImgFile = new File(PathUtil.getImgBasePath() + ImageUtil.getRandomFileName());
    			try {
    				shopImgFile.createNewFile();
    			} catch (IOException e) {
    				modelMap.put("success", false);
    				modelMap.put("errMsg", e.getMessage());
    				return modelMap;
    			}
    
    			try {
    				inputStreamToFile(shopImg.getInputStream(), shopImgFile);
    			} catch (Exception e) {
    				modelMap.put("success", false);
    				modelMap.put("errMsg", e.getMessage());
    				return modelMap;
    			}
    */
    			
    			ShopExecution se;
    			try {
    				if(shopImg == null) {
    					se = shopService.modifyShop(shop, null);
    				}else {
    					ImageHolder imageHolder = new ImageHolder(shopImg.getOriginalFilename(),shopImg.getInputStream());
    					se = shopService.modifyShop(shop, imageHolder);
    				}
    				if (se.getState() == ShopStateEnum.SUCCESS.getState()) {
    					modelMap.put("success", true);
    				} else {
    					modelMap.put("success", false);
    					modelMap.put("errMsg", se.getStateInfo());	
    				}
    			}catch(ShopOperationException e) {
    				modelMap.put("success", false);
    				modelMap.put("errMsg", e.getMessage());
    			} catch (IOException e) {
    				modelMap.put("success", false);
    				modelMap.put("errMsg", e.getMessage());
    			}
    			return modelMap;
    		} else {
    			modelMap.put("success", false);
    			modelMap.put("errMsg", "请输入店铺Id");
    			return modelMap;
    		}
    
    	}
    	private static void inputStreamToFile(InputStream inputStream, File file) {
    		FileOutputStream os = null;
    		try {
    			os = new FileOutputStream(file);
    			int bytesRead = 0;
    			byte[] buffer = new byte[1024];
    			while ((bytesRead = inputStream.read(buffer)) > 0) {
    				os.write(buffer, 0, bytesRead);
    			}
    		} catch (Exception e) {
    			throw new RuntimeException("调用inputStreamToFile产生异常:" + e.getMessage());
    		} finally {
    			try {
    				if (os != null) {
    					os.close();
    				}
    				if (inputStream != null) {
    					inputStream.close();
    				}
    			} catch (IOException e) {
    				throw new RuntimeException("调用inputStreamToFile产生异常:" + e.getMessage());
    			}
    		}
    	}
    }
    

    4.修改测试类

    package com.csj2018.o2o.service;
    
    import static org.junit.Assert.assertEquals;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.InputStream;
    import java.util.Date;
    
    import org.junit.Ignore;
    import org.junit.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import com.csj2018.o2o.BaseTest;
    import com.csj2018.o2o.dto.ImageHolder;
    import com.csj2018.o2o.dto.ShopExecution;
    import com.csj2018.o2o.entity.Area;
    import com.csj2018.o2o.entity.PersonInfo;
    import com.csj2018.o2o.entity.Shop;
    import com.csj2018.o2o.entity.ShopCategory;
    import com.csj2018.o2o.enums.ShopStateEnum;
    import com.csj2018.o2o.exceptions.ShopOperationException;
    
    
    public class ShopServiceTest extends BaseTest{
    	@Autowired
    	private ShopService shopService;
    	@Test
    	@Ignore
    	public void testGetShopList() {
    		Shop shopCondition = new Shop();
    		ShopCategory sc = new ShopCategory();
    		sc.setShopCategoryId(1L);
    		shopCondition.setShopCategory(sc);
    		ShopExecution se = shopService.getShopList(shopCondition, 1, 5);
    		System.out.println("店铺列表总数:"+se.getShopList().size());
    		System.out.println("店铺总数为:"+se.getCount());
    	}
    	
    	@Test
    	@Ignore
    	public void testModifyShop() throws ShopOperationException,FileNotFoundException{
    		Shop shop = new Shop();
    		shop.setShopId(1L);
    		shop.setShopName("修改后的店铺名称");
    //		File shopImg = new File("/Users/chenshanju/Downloads/liuchuanfeng.jpeg");
    		InputStream is = new FileInputStream("/Users/chenshanju/Downloads/liuchuanfeng.jpeg");
    		ImageHolder imageHolder = new ImageHolder("流川枫.jpeg",is);
    		ShopExecution shopExecution = shopService.modifyShop(shop, imageHolder );
    		System.out.println("新的图片地址:"+shopExecution.getShop().getShopImg());
    	}
    	@Test
    	
    	public void testAddShop() throws FileNotFoundException {
    		Shop shop = new Shop();
    
    		PersonInfo owner = new PersonInfo();
    		Area area = new Area();
    		ShopCategory shopCategory = new ShopCategory();
    		owner.setUserId(1L);
    		area.setAreaId(2);
    		shopCategory.setShopCategoryId(1L);
    		
    		shop.setOwner(owner);
    		shop.setArea(area);
    		shop.setShopCategory(shopCategory);
    		shop.setShopName("测试de店铺4");
    		shop.setShopDesc("店铺描述4");
    		shop.setShopAddr("测试路4号1");
    		shop.setPhone("1234567892");
    		shop.setPriority(4);
    		shop.setCreateTime(new Date());
    		shop.setEnableStatus(ShopStateEnum.CHECK.getState());
    		shop.setAdvice("审核中");
    		File shopImg = new File("/Users/chenshanju/Downloads/cat.jpg");
    		InputStream is = new FileInputStream(shopImg);
    		ImageHolder imageHolder = new ImageHolder(shopImg.getName(),is);
    		ShopExecution se = shopService.addShop(shop, imageHolder);
    		assertEquals(ShopStateEnum.CHECK.getState(), se.getState());
    	}
    }
    
  • 相关阅读:
    [转载] IE8+兼容小结
    vue添加,删除内容
    vue跳转的两种方法
    checked 完整版全选,单选,反选
    网页特殊符号HTML代码大全
    利用css 实现 视觉差效果
    css 自定义滚动条样式
    js 模拟键盘
    css 动画 transition和animation
    浅谈浏览器垃圾回收机制
  • 原文地址:https://www.cnblogs.com/csj2018/p/12450804.html
Copyright © 2020-2023  润新知