找回密码
 加入怎通
查看: 262|回复: 6

前端设计走查平台实践(前端篇)(走前端要学java吗)

[复制链接]
我来看看 发表于 2023-03-19 07:13:29 | 显示全部楼层 |阅读模式
5 Z& q' B6 {- X" B& w( r0 |

项目背景

4 F9 X' x8 d' {7 d

随着前端业务的不断发展,前端对设计稿的还原程度也成为了影响用户对产品体验的一个关键指标,作为最靠近用户侧的研发,前端工程师通常需要和设计师同学通力配合来提升用户体验其中,设计走查是设计同学最常见的测试前端同学是否完美还原了自己设计理念的方式,本文旨在通过设计走查平台在前端侧的实践总结下在前端上游研发链路中的一些基础建设,以期能够为其他有相关需要的同学提供一些实践思路。

) o( W. P9 v7 H5 V

方案

) u: R2 I. z1 s

一个前端工程师的主要目标是寻找一款贴近浏览器原生的框架,svelte 是你不二的选择前端架构选型,从整体的产品业务形态来看,属于较为简单的单一页面形态,并且考虑到要支持浏览器的插件生态,因而前端部分选择使用了。

: f v, J8 P# \% v

svelte的框架方案选择 svelte 作为本业务的前端技术选型主要考虑到以下两方面原因:一是考虑到业务形态较为简单,只有一个上传图片的页面;二是由于浏览器插件相关的编写还是更加偏向原生js一些,使用大型框架有些大材小用。

! w& q& l' {7 P9 k0 W( K/ e

综合近几年 svelte 的迅猛发展,小型业务还是考虑使用它作为一个框架使用的,其在编译时利用位掩码做的脏值检查的思路其实还是可以作为框架开发者借鉴的一个思路的(ps:对这个感兴趣的同学,可以看一下新兴前端框架 Svelte 从入门到原理

. R: e0 ]4 Y8 x4 S

这篇文章的介绍),但是目前发展相对还是比较初期,整个生态相对还不够完善,同时也是给广大开发者提供了很好的蓝海空间,比如:目前还没有出现类似 Element UI 和 Ant Design 这种十分好用的组件库系统,虽然有几个,但是个人感觉很不好用,作者在本项目中对需要用到的几个组件做了简单的封装,有需要的同学可以参考借鉴一下。

( [! K& t; S Q; O' o% x g

目录public buildbg.jpegfavicon.pngglobal.cssindex.htmlmanifest.jsonscripts setupCrxScript.jssetupTypeScript.js

4 }2 }: P; ~: U

src components Button.svelteDialog.svelteIcon.svelteInput.svelteMessage.svelteTooltip.svelteUpload.svelte

4 ]4 N* X0 o& t4 _/ x7 S( a. |) G

utils function.jsimage.jsindex.jsApp.sveltemain.jsrollup.config.js实践

- i2 z+ C" o/ @/ }

设计走查平台提供了管理平台及对应的Chrome插件,可提供给测试及UI同学用于图片的比对,提升研发效率源码

! c4 t: ]+ J. u. Y5 a% N

svelte作者--前端著名的轮子哥Rich Harris,同时也是rollup的作者,因而本项目中就选择了 rollup 作为打包构建的工具,同时为了将Chrome插件发布到内网中(ps:本项目主要用于项目内部基建应用,因而未在公有云及Chrome官方平台去发布,服务端涉及到了图片的比对计算);在scripts目录下内置了两个脚本,一个用于生成ts,一个用于向云平台发送压缩包;由于

# Y; w3 g; B) O0 k3 n

svelte 的组件库生态相对不是特别丰富(ps:业界常见已经开源的 svelte 组件库可以参看这篇文章Svelte的UI组件库),对比了业界几个相关的组件库后,决定自己实现下业务中需要用到的组件,具体组件放在了components目录下

% T( H7 s. U; a( U2 C# B. c

rollup.config.jsimportsveltefromrollup-plugin-svelte;importcommonjsfrom@rollup/plugin-commonjs;import

! S0 S$ c1 X$ b; S$ p( H8 y

resolvefrom@rollup/plugin-node-resolve;importlivereloadfromrollup-plugin-livereload;import{terser}from

! B) Q& f; ~8 I- l+ x) b2 D

rollup-plugin-terser;importcssfromrollup-plugin-css-only;constproduction=!process.env.ROLLUP_WATCH;function

9 x7 ~6 z* j9 {

serve(){letserver;functiontoExit(){if(server)server.kill(0);}return{writeBundle(){if(server)return;server

- h* [% k( j9 p! J& q

=require(child_process).spawn(npm,[run,start,--,--dev],{stdio:[ignore,inherit,inherit],shell:true});process

# }8 R6 \3 d! ]" X$ U

.on(SIGTERM,toExit);process.on(exit,toExit);}};}exportdefault{input:src/main.js,output:{sourcemap:true

7 L4 Z3 @, a' `% E; _

,format:iife,name:app,file:public/build/bundle.js},plugins:[svelte({compilerOptions:{// enable run-time checks when not in production / `' j$ i) S. y* o

$ Y/ B# U; _" {! _9 K* P2 q; o

dev:!production}}),// well extract any component CSS out into 7 n6 }9 D S' q+ j' b // a separate file - better for performance 6 ~) o9 a. k4 u1 M* l4 E5 x

% O: f1 [. T6 A# D6 o2 F

css({output:bundle.css}),// If you have external dependencies installed from9 f8 e- w4 j! ` // npm, youll most likely need these plugins. In " g4 i' [; N% Q$ z6 v+ b

/ Y. }0 n5 Z7 G

// some cases youll need additional configuration - 4 W4 w% _' d7 h* ^* x; g3 k // consult the documentation for details: 9 X2 D6 l) ^) B // https://github.com/rollup/plugins/tree/master/packages/commonjs: T3 ~6 B! C1 }+ z2 z W7 m3 u

1 ? Z6 G4 N5 e, j2 j: ?3 a

resolve({browser:true,dedupe:[svelte]}),commonjs(),// In dev mode, call `npm run start` once& q5 e7 x5 W, i$ g/ i // the bundle has been generated / S3 x2 s3 m, ~6 \. ?; H% X

4 V& \3 r! @+ g

!production&&serve(),// Watch the `public` directory and refresh the1 T6 H% W2 ]4 P9 e9 h: @ // browser on changes when not in production0 i& Y8 ?3 r& w) ~$ K

4 X4 c. w) s; B8 `7 @/ _

!production&&livereload(public),// If were building for production (npm run build+ L! f4 o$ d; i# k+ [ // instead of npm run dev), minify ( T8 U% s9 K, X6 r: v3 v- ~9 g

/ A* b4 T8 E6 o/ a8 [- C$ a- z

production&&terser()],watch:{clearScreen:false}};scriptssetupCrxScript.js通过minio这个库来进行私有云平台的对象存储库上传,archiver

2 [# }2 A# y6 Q' @0 v$ E1 _

这个主要用于压缩constfs=require(fs);constpath=require(path);constarchiver=require(archiver);constMinio=require

$ r2 a9 s- @! ~5 ]- p

(minio);constminio=newMinio.Client({endPoint:,port:80,useSSL:false,accessKey:,secretKey:})constoutput

3 y3 Y3 g* r; P' H, S% ]

=fs.createWriteStream(path.resolve(__dirname,../pixelpiper.zip));constarchive=archiver(zip,{zlib:{level

8 R' p0 u0 n( [1 k- L+ q1 r

:9}});output.on(close,function(){console.log(archive.pointer()+ total bytes);console.log(archiver has been finalized and the output file descriptor has closed.

5 f! }! Q& x/ T2 a

);// 压缩完成后向 cdn 中传递压缩包 ( B0 S3 Y! D' a constfile=path.resolve(__dirname,../pixelpiper.zip);fs.stat(file,function(error

- \3 V3 P* ]& ~) a& c: b

,stats){if(error){returnconsole.error(error)}minio.putObject(cdn,pixelpiper.zip,fs.createReadStream(file

$ R$ d. J- l8 D& Y2 G

),stats.size,application/zip,function(err,etag){returnconsole.log(err,etag)// err should be null' ]: P; E( K% H( C, i })})

3 P; O8 C: i3 Q. e5 C. {

});output.on(end,function(){console.log(Data has been drained);});archive.on(warning,function(err){if

- Q: V* n0 B; A1 {+ |

(err.code===ENOENT){}else{throwerr;}});archive.on(error,function(err){throwerr;});archive.pipe(output

/ `8 g- f! q3 o7 T5 M4 j

);archive.directory(path.resolve(__dirname,../public),false);archive.finalize();setupTypeScript.js// @ts-check ' u: `, @3 p- v# ^

& s' S/ J( z& H& k1 `

/** This script modifies the project to support TS code in .svelte files like:2 y; S1 `& [' }' W' q( h- R1 p 3 O q2 C% O j- x6 f1 N export let name: string;; ~! I' n, B$ L& P% l. j% I2 e8 F! z

7 t9 w7 h* F" S8 l$ \

3 x0 N/ X9 l5 s$ o0 P) z As well as validating the code for CI.- C( Y' I/ w* `% a& _ *//** To work on this script:. _' w( J# C6 X4 @; S7 q rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template * P2 J# Y; k9 N

5 [2 X2 ^" ~; \( ^

*/constfs=require("fs")constpath=require("path")const{argv}=require("process")constprojectRoot=argv[2

( z6 @7 [( S: H5 x( |) O

]||path.join(__dirname,"..")// Add deps to pkg.json 5 ~# u4 m! R; l2 l3 l0 w5 a constpackageJSON=JSON.parse(fs.readFileSync(path.

: c: M T8 T* W/ f1 a9 E2 R

join(projectRoot,"package.json"),"utf8"))packageJSON.devDependencies=Object.assign(packageJSON.devDependencies

$ r1 Y* A0 `, h2 h' b1 v/ s" |

,{"svelte-check":"^1.0.0","svelte-preprocess":"^4.0.0","@rollup/plugin-typescript":"^8.0.0","typescript"

' a/ K A/ @( ~/ P% y \% A

:"^4.0.0","tslib":"^2.0.0","@tsconfig/svelte":"^1.0.0"})// Add script for checking 4 Z) @5 E/ s4 _0 ?: r+ x8 v packageJSON.scripts

- n2 f! S/ N5 G# n

=Object.assign(packageJSON.scripts,{"validate":"svelte-check"})// Write the package JSON) I5 Z/ d3 W8 I: ~- k fs.writeFileSync

, S y/ ~( L% O; R+ |

(path.join(projectRoot,"package.json"),JSON.stringify(packageJSON,null," "))// mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too 7 B4 Z) J5 o3 x9 ?+ R7 C m6 r

, \+ W, T4 G4 O$ D, N

constbeforeMainJSPath=path.join(projectRoot,"src","main.js")constafterMainTSPath=path.join(projectRoot

3 l3 Q4 S* a. `7 E7 s8 Y7 s7 f

,"src","main.ts")fs.renameSync(beforeMainJSPath,afterMainTSPath)// Switch the app.svelte file to use TS8 ]/ }+ r d: v3 p

3 g. h. C- F2 g& `/ O# i

constappSveltePath=path.join(projectRoot,"src","App.svelte")letappFile=fs.readFileSync(appSveltePath,

$ j$ h5 u8 x5 E: S/ b. ~

"utf8")appFile=appFile.replace("",)appFile=appFile.replace("export let name;"

/ Y0 e2 F+ q, N. B3 ?8 _

,export let name: string;)fs.writeFileSync(appSveltePath,appFile)// Edit rollup config 3 C7 @! s3 k* h0 d' R constrollupConfigPath

2 ?( L ]3 V' ]2 [. D

=path.join(projectRoot,"rollup.config.js")letrollupConfig=fs.readFileSync(rollupConfigPath,"utf8")// Edit imports * q/ {7 U, R' H( {

/ z1 b2 R( ~4 N7 m

rollupConfig=rollupConfig.replace(`rollup-plugin-terser;`,`rollup-plugin-terser; $ d1 C$ a6 M5 r& q( @ import sveltePreprocess from svelte-preprocess; % _' J2 P0 [( N! k8 d5 \

8 s# f( c" F/ w+ n/ {. u7 h9 Y/ e9 e

import typescript from @rollup/plugin-typescript;`)// Replace name of entry point 9 e6 o* h, r4 e; _3 r' ] rollupConfig=rollupConfig

, ]8 s6 y1 Q5 \6 K% e

.replace(`src/main.js`,`src/main.ts`)// Add preprocessor 9 `7 U* N& W$ N5 D* N- O* Y } rollupConfig=rollupConfig.replace(compilerOptions:

6 ]+ v, p; U& j

,preprocess: sveltePreprocess({ sourceMap: !production }),\n\t\t\tcompilerOptions:);// Add TypeScript8 {. z8 U: S7 |" r& _

; P$ h9 N* g% L, j* J( E2 y

rollupConfig=rollupConfig.replace(commonjs(),,commonjs(),\n\t\ttypescript({\n\t\t\tsourceMap: !production,\n\t\t\tinlineSources: !production\n\t\t}),

0 W7 ^& l0 P; H* h! |* \1 N

);fs.writeFileSync(rollupConfigPath,rollupConfig)// Add TSConfig+ J6 ?5 C& D& d' E# `8 v consttsconfig=`{9 ^* X+ N5 A* [" H "extends": "@tsconfig/svelte/tsconfig.json", ; Q }2 R$ u: ^& w

$ }8 e5 x' Z d# a( g1 m: D

"include": ["src/**/*"],3 m: _: g0 E% F4 v5 H# _ m "exclude": ["node_modules/*", "__sapper__/*", "public/*"]2 q3 K4 I2 I3 k1 O& |" l$ n* v$ } }`consttsconfigPath

/ A5 t! T9 u5 n$ K3 E$ g; \" R# u

=path.join(projectRoot,"tsconfig.json")fs.writeFileSync(tsconfigPath,tsconfig)// Delete this script, but not during testing 7 V$ m* b, K3 L* J0 e

% Y- ?4 K# |! \& M

if(!argv[2]){// Remove the script+ `, ?/ r& m0 M' D4 V fs.unlinkSync(path.join(__filename))// Check for Macs DS_store file, and if its the only one left remove it: T) J. w2 L5 ?

. ?/ B" R4 }0 g- p

constremainingFiles=fs.readdirSync(path.join(__dirname))if(remainingFiles.length===1&&remainingFiles[

$ Z8 F; l& ^$ |0 h

0]===.DS_store){fs.unlinkSync(path.join(__dirname,.DS_store))}// Check if the scripts folder is empty " T' o9 l( E1 V5 K P2 a. `

3 P- h+ P1 m9 ]9 B1 y. g5 {" u

if(fs.readdirSync(path.join(__dirname)).length===0){// Remove the scripts folder, N0 ^" Q* J, M4 z j W fs.rmdirSync(path.join

( v; M. i; P8 p

(__dirname))}}// Adds the extension recommendation$ ]) y* R; s; U' H; r" q fs.mkdirSync(path.join(projectRoot,".vscode"),{recursive

, Z. N% P9 `) K$ Q' k2 E4 K

:true})fs.writeFileSync(path.join(projectRoot,".vscode","extensions.json"),`{ 0 U' Z" b& j2 t# i/ ` "recommendations": ["svelte.svelte-vscode"]$ V' ~- j7 _" K4 q

4 e$ o* `/ {0 v1 l

} 5 `: o4 i6 U4 L/ ` h; S `)console.log("Converted to TypeScript.")if(fs.existsSync(path.join(projectRoot,"node_modules"))){console

0 i8 Z+ I; K' P. k- I, _

.log("\nYou will need to re-run your dependency manager to get started.")}components项目中用到了一些通用组件,借鉴了下

- H V. `% y. t$ r0 D

Element UI的组件样式和思路,主要封装了 Button(按钮) 、 Dialog(对话框) 、 Icon(图标) 、 Input(输入框) 、 Message(消息) 、 Tooltip(提示工具)

A! j% q. h b

、 Upload(上传) 几个组件Button.svelte6 |$ d( i9 t8 X$ m' e4 _# _ import Icon from ./Icon.svelte;3 O( e( a: d' F4 a, t import { createEventDispatcher } from svelte; 5 L0 r3 H: z" E8 t/ Z const dispatch = createEventDispatcher(); a$ E |+ P( L5 ]* O1 d export let icon, type=default; ; d5 c e& b; G- E$ ^- ]. { function handleClick() { 8 `( L2 C$ k! t- r9 \ n" ^ dispatch(click) 8 q# l) i& `, E3 C } s7 U5 n. Q. \' Q: p" g function computedButtonClass(type) {# x( ?6 K; ~: U) M% e switch (type) { ( J2 n) \: s, w+ _& R case primary:, |+ g( Q' C% h8 e" ]* B: i. _# @/ | return button button-primary; ! R, H" O8 C" f6 B case default:' X, G2 m9 G# z' a! x' M8 s return button button-default; Y! }, `' i" W1 x6 I case text:+ k2 e, |9 D8 b return button button-text; " g! H) Y7 c) p: W. v default:! ~! Z! {' F; @( {+ L# n return button button-default; , A" F2 Y6 C z" Q3 t. L }+ o% }) `* h- S5 I3 n' G }) ^/ d- T5 ^. q2 q( I % |5 S# p- Y% L4 {& ^! w- J. x1 F9 ^ 8 ^" R- \ C6 c( Z/ d: r$ T .button { $ Z( |& x5 H6 d5 g8 H2 o, { border: 0;0 P0 k) h# a6 Y" h2 G border-radius: 2px;0 n6 R2 p/ u) E2 |. V } $ f: z1 g+ G( J. w) E5 ]" j4 W" j7 q! p2 P- r/ p5 N: p' q5 F .button:hover {' F1 r' Z* n4 z cursor: pointer;7 }4 c( z6 Z7 @0 i5 W } * t6 R. L2 d' m" e: d, c 4 x) j5 C: C J" x4 F, r .button-primary {/ F' V, ~, N/ G background-color: rgb(77, 187, 41, 1); ) E# O, s7 q5 { color: white;' k0 C" ] H* @0 k border: 1px solid rgb(77, 187, 41, 1);: b; i3 g( {0 w }( y$ D1 y5 H6 T & Y% s1 C! g3 R& N2 d( {5 O .button-primary:hover { v. A* U% @% h+ i: S1 _ background-color: rgba(77, 187, 41,.8);/ I$ f1 P* y2 c0 `( R }' |1 I- S: i3 W/ T; _- C : {7 ?/ @. h6 ?, [8 d& ]+ B2 e% k .button-default {- @1 V- ]3 R5 A2 e: @ background-color: white; 2 J* M7 p( H3 g7 g0 B4 O color: #999;7 t i' p( S2 G$ S border: 1px solid #e0e0e0;/ ~' A& q3 t6 f9 T0 S } & G/ }1 h: N( T7 _ , o, s8 J9 S9 t4 F' J0 C4 y .button-default:hover {9 Y( I3 p) `0 k, P! M! s. z background-color: rgba(77, 187, 41,.1);8 E! o: W+ s u- f% p$ \" q! z* S9 C color: rgba(77, 187, 41,.6); $ q+ g; e* w& N% W$ Y+ t border: 1px solid #e0e0e0; l, A0 Q# C9 F2 E# a) ^( C7 p } , b" d( w! }# q5 h" V( Z( G& G7 C: b2 n) g .button-text {2 J1 z* x" t0 C+ p/ O- W+ I& V8 p background-color: transparent;2 u3 @% P' a s* F border: none; * c) f( ]) k; A$ _8 F9 [ }: P o4 d: @( | 2 y& N( m/ ]% E* |4 c- Z+ X3 X- N6 ?) Z ) z4 D) E9 i9 Z- a, a% m1 D {#if icon}$ Y) U, a# z6 x( F 6 J/ I' {, c. m3 f {/if}0 F8 B; o: |( @$ Q $ @, N9 W1 A) S. h/ U

Dialog.svelte0 r- h$ }9 j: i* H8 u import Icon from "./Icon.svelte"; ; S6 N1 L3 t4 Vimport Button from "./Button.svelte"; ; T" t4 m* I1 Yimport { createEventDispatcher } from svelte;6 U, U x! N0 h- D/ i3 ^ const dispatch = createEventDispatcher();# p6 Z. h) g" f6 l export let title, visible = false;: s) I: W& W; g+ x function handleClose() { ; G+ J: t: W2 e) m visible = false; / x( {; v/ w: z# O! }4 P' S} u5 T# l5 u: p4 K2 T. \ function handleShade() {6 r W4 Z8 W! W+ V" G1 W! T visible = false; 1 V9 g0 [1 @% p% D} A/ B+ N" {$ P7 z7 t2 y function handleCancel() { " K0 w- W& M% C! O3 m. J) l$ |: Y visible = false; 3 R, C* \9 a u5 T* _5 b- \}8 {! V8 m7 j! V) i6 I function handleSubmit() {4 q# N7 B7 m& f( ]8 M/ r dispatch(submit)' q4 b, I2 Z6 F, b' ^, [" ` }- q) ]# r4 f) S- D9 K3 O % T6 H6 ~2 U, t- x+ d5 x) X; ^ 2 G }3 j$ e# R: Z6 [7 o( ]( C7 v+ D .dialog-wrapper {. L& M2 h5 u% F. U; G0 L$ m width: 100vw;: A) Y* r3 C$ H4 i4 s height: 100vh;+ M4 P7 U4 h" [* M C# L. D+ i) I position: absolute; % L- J6 B% f d% _' f9 v8 L z-index: 100000;) w& k0 d) o0 {' H2 M3 z/ j background-color: rgba(0, 0, 0, .3);3 y4 ]" p0 ~2 ^& S. y' K- S0 B; @, ` } ! K9 h) ^9 Z* z0 p- c8 h- z+ q $ z; |: b# s- x6 C.dialog { 5 B! `$ ]8 p, r" E% g width: 400px; q' W9 u' h9 {9 V6 M height: max-content;# p% n* K+ R$ w7 i6 a" ?0 t background-color: white;4 s1 ?# t* _4 I, F4 n box-shadow: 0 0 10px #ececec; / A! o& F- S9 r) |& C. x position: absolute; * L0 P s- l0 Q% G2 u$ i z-index: 100001; 2 ]" ~- l, X# C- R7 Q: U. c border-radius: 2px; 7 G$ k R$ `( d6 D" ~ b3 X9 Q- o margin: auto;' U: H) w# S0 o3 C4 U top: 0;6 ?6 i& [) H( ^4 |& { bottom: 0;5 q3 _* n! K! j: F" t; x8 ^& m- n* A left: 0;1 R; X. a; g4 m' | right: 0; & e* h: A1 x" c9 N9 y* ^}0 c$ r* A9 E1 `* | .dialog-header {+ j1 L- [ B5 X: C( H7 c( ?$ ?+ G display: flex;% F8 c% \, w4 V& M x* T v align-items: center; 3 {' G3 A$ v2 E! Y$ s justify-content: space-between; # m! w1 ]! V5 L5 ~1 m! _1 q4 K border-bottom: 1px solid #ececec;- x" M, E; U6 |3 p padding: 10px; - ]* M2 P5 n7 O3 V; D' b. [% r}, U8 y+ ?) l! R5 Q; u 8 H5 l% h# ]# o9 s.dialog-header .dialog-title {' e2 p' a0 u8 m! M0 z font-size: 16px; 0 i4 w \- H9 N% k} 4 w/ }6 x9 ?/ I t4 Y 7 F U* b6 |5 m' F" q4 x0 W w9 _.dialog-body {( e% S' b' ^5 C8 G padding: 10px; * A4 \ X5 J" v( [% m% y} & q, n6 @9 W; F! K+ k) x! ~, E ( \ e) n# w/ o.dialog-footer {! p1 w& k$ m* I( ~9 B display: flex; 6 U3 d- t" Y$ T# F+ D/ Q justify-content: right; 0 b' J. E% v- ?1 R0 a padding: 10px; : l& l/ @; n3 T. }' o! ?$ G( D' \} 6 q( a, S5 t! m" a" R! s X8 J L3 G 0 r, a9 ]; D0 U/ C. u{#if visible} 4 s9 S3 l% y% S+ u$ v; e) L$ q

" p- W5 _5 r6 l& u' m& B
6 {1 n, ]; w, }, {; P
_( L2 c2 j- B2 C: x* L 4 Y5 a6 V$ O4 l# B! {/ T { title } 8 B7 Y* [! f( C4 ~5 y ; A @) |/ \2 F* F u' q9 n - X/ `1 q# R2 q3 p5 R 4 i1 W/ `+ o7 u& k! m0 M( v9 C " n. i# p% J( {, j ' ~2 w5 B+ ^5 f" \
' l1 ~+ l" A! x 2 u" `' \7 K0 k6 Z% |% R9 K
* |& d5 s. A6 i. i& k0 D: g
% Y1 y: K# w6 e: y! E3 _ 取消7 [0 w7 m. w6 G9 j$ Y5 u4 v$ R& z 确定 ( b2 R$ l3 \& E: N# _! X6 ?
* u7 O9 l2 M; D# C
* H3 Y/ }% H0 Q& E/ }, `" n+ |' x
! A$ ~- P* m) B3 j0 z. U{/if}

Icon.svelte常用的icon主要通过iconfont来引入 E9 g' ^& R S3 O) D8 F export let name;" v& r& u1 X: i$ M0 ^2 F . I3 P9 o+ }7 E5 T9 n4 X" R. g& E3 ]4 k" A6 t; g

Input.svelte7 ?. p/ X% H6 f b: z* d a/ X export let value;2 v4 `& z: c6 E* q import { createEventDispatcher } from svelte;( |9 `. G$ T# @ const dispatch = createEventDispatcher(); % |! `1 O+ I! l) p* f/ H function handleChange() { d2 U- z: l! {" t dispatch(input); @ C) J; n1 N' f2 |1 G: { } . B' X5 T/ O* m . G8 h/ n& h0 J% l2 W6 z! N; }% h% k3 S- {9 a. {) \ ( r) B( w1 t% d6 h1 G6 y: ` .input {1 B4 O3 q7 j7 g# C border: 1px solid rgb(77, 187, 41, 1);4 H3 ?% U- i. y# @+ t K, j width: 100px; ; ^6 ~& l2 u R( [ }+ x& o7 N+ t2 r2 r& P0 o & ?) @, R \8 ?0 ]! r .input:focus { * |. @: u7 @3 p1 j border: 1px solid rgb(77, 187, 41, .5);1 ~2 t; I7 b6 V9 s! I outline: none;( i: U5 Y( \0 P } 1 a1 Z" S' J0 V* v5 V% M/ V+ ^8 Y& `7 c4 g* {. v& w 3 t* E4 [! [+ ?4 T" j; k0 k' M

Message.svelte! t2 b0 w) \' A import Icon from ./Icon.svelte; % K" ~, J# G9 Y8 kexport let type = info, show = false;, m! F- D+ K8 s* ?5 o , q( a+ f: d2 f const computedMessageClass = type => { 5 Q, H$ v8 _3 t) u9 G1 e switch (type) {) L$ k+ ]3 L# C- {9 x case success:0 K, t9 q) z! m return message message-success;' X+ l" q9 y- \+ v$ h9 T) R case warning:+ ~2 C: _# U/ p: S# s return message message-warning;0 i Q* O7 J1 ]+ B1 ?" L case info:; K& Z m, O& O' z return message message-info;$ h4 o- Q# a4 s1 s% x$ ^ case error:7 O1 w) m2 H! ] return message message-error; " a* Y* {+ r" ?2 {9 t9 m$ w/ M default:" L2 S2 x7 h! b' D return message message-info; + u- T* s Y1 j: X! J' |8 [ }# X$ `' U. ?+ ^4 w! T; I* i }6 @# i/ c6 N1 f) `. l , @ S2 a8 L- r$ I. T6 v const computedIconName = type => { - R4 `& ^. {% J3 \ switch (type) {6 k( W: Q1 k2 k) {' P$ @7 | case success: ' C1 e# J7 y; |& \ return iconsuccess;! }' X2 C# F5 T3 L6 j9 j( R case warning:9 U, j1 A j' h: _3 O return iconwarning; ; q! i$ h3 s/ |6 W case info: ; M/ ]. j# A8 l* ]# v! I; \ return iconinfo; - a% n- S8 e/ |" ]. H case error: 7 h# | z- n- Y: T; B return iconerror; ' E4 L* c6 J& T4 G default: . X+ n. F$ f, }9 @1 t# E return iconinfo; ! G+ z, o# R1 V3 [ } r: p! g( j% `7 R* ~+ h$ ]9 q} * s$ ]& u! ?4 K0 N7 B1 e $ ?, M' G, `) E$ r; q) }# a0 F , F: V+ y6 e1 _1 Z9 E# T% K8 u" @+ }7 u) c0 G! z; ?* j/ r! y .message { + d" s7 Q0 w0 w# z position: absolute;* E+ b z; O4 Y3 w6 L z-index: 100000;* _0 O2 E2 ^/ |3 q* {& m- j9 J( { width: 200px; ( n9 `( q2 R7 X' I. S0 z height: max-content;9 k1 [: d2 n3 \$ k' [1 X left: 0; 6 R4 r& s8 b$ g4 y5 n right: 0; 9 }8 p0 n: b# m9 k9 p margin: auto;) h. I9 \1 B& \/ h5 } i* I$ O padding: 4px 10px; % D) ]9 P W, r2 s+ r animation: show 2s ease-in-out forwards;5 t h5 u4 C$ A) ~' V display: flex; 4 _ K' Y9 W, V `$ ]' _ justify-content: left;3 [( F: S2 ^" p! w0 [( q& F( `0 H6 O align-items: center; F' B1 [2 N7 c, u) [8 g6 O$ b2 b' G border-radius: 4px; 0 p& A9 A: [! c8 S/ M}! K, ^, h( ^: `% T " |! v7 E: _! T4 `6 y .message-content { / C+ M l J9 ^4 @ margin-left: 10px;- Y0 O8 ^8 }+ c2 f$ A9 q) S }: b5 h2 _# E/ r0 V $ A. @* T) ]' w8 b- _' G @keyframes show {& M6 J5 u1 U; Z7 { from { # L g" n& w) q$ X# _" m opacity: 1;( a( G C% X- l2 f- H5 h top: 0; 0 v8 f: z- J3 i M) O } : Z1 W' t$ I# A+ L' G- D+ ` 7 ^, r5 k+ Y7 |* q' ]6 x- J$ B: q to {! g. P, J: ^2 j8 g e opacity: 0;4 e G' R8 S4 z. V7 ]& i: g top: 100px;7 p" w8 i5 L" Q' C! i* D; F3 O } + c' f; W3 t* ^2 q( G3 e} ' `+ H9 G5 {+ P+ g2 c0 `7 o# Q4 B ; p. V6 W# l+ T9 {6 `* `.message-success { 2 T4 {( K9 q. U$ H$ | background-color: rgba(77, 187, 41, .2); ; j* J/ n* O: \1 t color: rgba(77, 187, 41, 1);5 i X: W* C2 | e1 V' f } ) n; J" O- s( [: v X. I: q" a! ^6 O& D, `6 \+ m .message-info { [2 K3 q F3 b" r S background-color: rgb(144, 147, 153, .2);/ ?& b; X* ]- E color: rgb(144, 147, 153, 1); + Z% t3 l5 y. v! T} % U8 _7 ?+ U. i3 j9 w% i 8 X( P6 |# R; ~! F& F: g.message-warning { % c Z% V# ? ^! A( r& z background-color: rgb(230, 162, 60, .2); * ~9 {/ l. ^# O/ r: `2 t% N color: rgb(230, 162, 60, 1); - p0 ]& Q- H) i}' Q. H* y1 }, M; [ 6 J4 R1 F) b0 b, B .message-error { 7 v9 B1 P0 d; b2 A; [1 i% t" n$ P background-color: rgb(245, 108, 108, .2); 3 [7 n" g$ p( v6 i1 M- i. V color: rgb(245, 108, 108, 1);! M( @4 ] z( }/ _ } 8 } Z$ G$ u; }- Y4 x9 x, b0 d3 n j( F" j- @5 | % k7 O* l3 _. x! Y" ?( o+ Y{#if show}; z/ Y3 G( ~: ~

y5 b8 y) N) w( C2 m {/if}

Tooltip.svelte 4 j4 ~, i, ]6 ?2 w( L3 ?/ f export let content, tooltip; 2 A0 F B1 W7 K- g5 N & \" U/ b5 O E: Q/ `3 j6 V9 ?$ G5 f: P3 N0 q; o }; i2 Q ( n' v& a! T" v! }: E0 S 9 D/ D5 f/ `4 R .tooltip { 0 H$ j8 C- T9 U* Z, `. l position: relative;( y0 y" c) i! r# @2 F" [$ O } - n) E! q- x4 g& i; P, p1 q+ R0 S( J, w# v0 t2 r- I( X* A, j .tooltip .tip-container { 4 P& }( F+ U1 N8 q, O6 k# R" | position: absolute; / u, }0 W7 M' H4 m. v7 A background: #666; ' i: Y3 o/ k" }7 J: J padding: 0 10px;7 i0 S7 V2 N$ ~7 i border-radius: 4px;1 K: r! E. ~3 ^6 d right: -180px;6 n* z7 v$ _. ~( k$ _; X top: 50%;8 X8 Z1 S& A% H( i margin-top: -24px; ( D/ ?" [9 N4 b} ! r. E ]/ `$ U+ ]% X # {# Y& C8 h( W6 T1 y8 |4 e& }5 d.tip-container .tip-triple { & ]8 P8 O1 Z1 o F9 ~( _) w! J width: 0; $ v/ i$ S; b3 _8 m( {% i! \5 _ { height: 0;2 Y) }/ |! T' H3 K. ^; P border: 8px solid transparent; ! W5 e0 p' K) m+ E5 t border-right-color: #666;0 X) E- ]- u; d6 A7 O position: absolute;& V1 [: D" n; m* w left: -16px; . i5 I: ~0 ^+ J- K1 g# \" C top: 16px; . p- F8 [* P* m5 U- a8 O1 m+ \- k5 g} , N5 y6 _! B5 s( N* _0 P1 X: g 9 @& z7 r9 |5 u6 a8 d.tip-container .tip-content { ( n Z& @$ c) _* f2 @6 |. k line-height: 24px; , q' m5 n, d1 H font-size: 12px;5 F. _+ x$ q% D: ?2 K5 r" [$ [$ _ color: white;) {8 b$ j7 W G9 V& {1 | }; W) I2 t# Y# l- I+ x4 o $ v4 l0 b) z" O! d / s, M, Q& s2 ~! q6 C

/ d q7 }4 Y' F7 P 0 Q( K3 r3 X4 x* t+ @$ E {#if tooltip}7 i$ }1 _2 z/ p0 J& j3 l2 O
1 N. f4 T9 r8 v+ h$ e% t+ \ L ( F$ K" v& z) J- W4 I

{content}

& N x; V" h% ~- f
; D0 ?; ~. Z& f: x& O) S: v {/if}7 @; ?4 h3 t L8 N: Z/ O

Upload.svelte ?/ E& R z+ ]1 A5 i" ] export let action, onSuccess, beforeUpload, id; 2 N, r( b/ G% X/ \* Q function ajax(options) { 9 G5 h T! g0 F7 b5 h7 ^ const xhr = new XMLHttpRequest();/ Q4 U4 M6 M4 l: `6 \9 X$ l5 f- L const action = options.action; 5 k. n, e! G; ] X# ~ let fd = new FormData();: E! p: h; @- Z. Z5 } + W1 m: a2 L' K( r$ N+ T fd.append(options.filename, options.file); # d: z w4 A* p3 B3 Y( _( u& o& I m# w xhr.onerror = function (err) {" N2 b% D8 R1 e# q& W; c- s console.error(err) $ J- g: G+ N% M5 o/ h: L } * K7 A' H& h' N/ O' G, C' T/ a; V2 g3 m: D4 U) Z# X xhr.onload = function() { 8 e1 `: t# ^! V+ P$ r: a const text = xhr.responseText || xhr.response; 4 J9 k' C! _4 o5 k& N: H# ?5 T( X5 a console.log(text, text); e1 n/ z- d# G+ [7 R g; l text && options.success(JSON.parse(text))) {. e8 v, H m; `, r) B }) e4 J7 b* L$ p# g) B4 N 6 \; h/ U7 a3 p& G6 f0 i& O xhr.open(post, action, true); % l, b( K" K) P' \ xhr.send(fd); * k5 V& c, [0 ^* i" l * _2 B! _% _1 b c) ]" I return xhr;- u2 c% _% O5 D8 t/ T I } : _5 R6 S5 \ O1 ]3 ~ function post(rawFile) {* H7 M0 h$ S5 T+ ^ const options = { ! X1 p4 {+ U% a4 n4 ` id: id," D @+ ?0 P* D9 G0 @, s file: rawFile, 2 U h' o# u$ d( _6 }/ S, R. { filename: img,$ i% `5 V: k7 e }! Q action: action, ) w1 i: U6 r& Z success: res => onSuccess(res, rawFile, id) ! \% s& P8 t- A+ q5 Y w+ u } 8 F" d }2 ]9 @ const req = ajax(options);/ v* U# ^: t* t+ x4 \ if(req && req.then) {4 `0 A7 d5 D+ @$ P% r$ n2 P$ ` req.then(options.onSuccess) ) z; u) p) M+ [. K } : n1 m. `' X& i7 _, v } - F; F* K ~( l1 K; m6 i async function handleChange(e) {' ^0 W0 _& M! Q2 j7 C9 N. M const rawFile = e.target.files[0]; . E1 {; ?! ]0 | if(!beforeUpload) {) \' B3 L. y! w0 c) T" ? return post(rawFile)) Q( j" `0 K% N3 m- f1 y+ D. t } % F9 Q7 {. N" Q* Z4 h" S let flag = await beforeUpload(rawFile, id);% ~) z# p$ ^) V E if(flag) post(rawFile) 4 J- c0 K2 b$ _& M T3 ? }2 ~# d f$ }" V: p' g4 D7 y function handleClick() { 0 k, s' s- g& ?( a/ d6 k const plus = document.getElementById(id); 2 ~5 j! U& V' W h7 D plus.value = ; 4 K$ O/ H: l5 T/ C! X( R plus.click()# ?; F" T" b+ q2 b! i* q }& D4 ]( G% A( A% }) `* l$ A: L( j1 T ( I) L. O0 J! ~% e 5 S. g% {% Q: z2 }3 q. v6 `' w0 J8 t! ^) @+ g* p x/ x( t .upload {' R) M% X- w- h width: 250px;3 m$ ]* ~7 N& G) P$ @7 T" m height: 250px; - y; O4 t5 Y+ O9 E- y border: 1px dashed #ececec; ' B9 d/ K" t1 r& l) r text-align: center;& t y2 k* A, m: b0 Z4 j, ] display: flex; 4 e2 _8 Z4 `/ X% E( _" e$ L justify-content: center;$ f, z: S# V% p) D* w7 H9 x) R align-items: center; 9 p# J2 |8 d) [ } 9 n7 Y& r4 F- O5 i+ o # V- h) v& E' V2 h .upload:hover {9 u q/ Q& O- [( f: k5 A4 q cursor: pointer;' X O' e* L! ^ }0 }7 h# m/ B2 b0 q) ^ 6 x+ m9 f' Y3 S9 A, e .native-input { ' k5 C% w9 E) N8 \' Y% E) s& t display: none; / H7 A6 F: v( C! e, M }1 w2 g9 g. R6 j" G4 l3 T ; X: w3 E( p4 d+ P" c+ R7 M 7 h' v* z# t. m9 l

utils通用工具库主要封装了图片及函数式编程需要用到的一些工具函数function.jsexportconstcurry=(fn,arr=[])=>(...args)=>(arg=>arg.length

===fn.length?fn(...arg):curry(fn,arg))([...arr,...args]);exportconstcompose=(...args)=>args.reduce((prev

,current)=>(...values)=>prev(current(...values)));image.jsexportconstgetBase64=file=>{constreader=new

FileReader();reader.readAsDataURL(file);returnnewPromise((resolve)=>{reader.onload=()=>{resolve(reader

.result);};});};exportconstgetPixel=img=>{constimage=newImage();image.src=img;returnnewPromise((resolve

)=>{image.onload=()=>{constwidth=image.width;constheight=image.height;resolve({width,height});};});}App.svelte

( s* i3 c b1 g6 ?7 c import Icon from ./components/Icon.svelte;5 Q8 W1 J5 a) w( I/ s' L import Button from ./components/Button.svelte;* J2 p, @ o( {3 s# A import Upload from ./components/Upload.svelte; 4 k m* U; {8 Y, f9 Y; U import Input from ./components/Input.svelte;& q2 ^, J" {& E+ y9 X6 s* @/ m import Message from ./components/Message.svelte; 9 d. D( \. T4 G6 c& ^$ W import Tooltip from ./components/Tooltip.svelte;) S& Z+ M/ G0 v* x1 { import axios from axios;" U2 x! z- _4 E/ ?! H import { getBase64, getPixel } from ./utils;, t. ^& l9 ^4 H5 n% u . @% r' j$ c8 l* _+ o( [ const bgUrl = ./bg.jpeg, a$ s4 i" v" X% u% L4 [ logoUrl = ./favicon.png,7 c# p) `4 J0 ?6 [+ T actionUrl = , . S* F2 q) J1 ^/ x j compareUrl = , # S, N K" l. h5 \; S9 h downloadUrl = , " T3 P L" ^! V x# n crxUrl = ;/ W3 F/ L% v; b2 r4 t! V8 V let width = 0, height = 0, flag, compareName, errorMsg, successMsg, show, tooltip;+ J: ^# u$ F! a5 v let uploaders = [ : ?% `% z9 f* |2 x$ g/ h {- A2 {+ x& R- n: A& X5 D id: design,6 v& v) {2 x; d$ a# k6 l title: 设计图,0 q2 m- ~5 d1 V. D4 j4 s' A filename: ,: R8 F5 t# e( J6 C! g url: 5 b7 g1 U' {3 `; N+ Q5 F# } },9 j7 Y1 g. d' ]" j: D3 ?8 U. k { 2 z9 f- W0 S2 k9 B% ` h id: code," V% |! s ?& b: w. a7 b D6 g& I9 P& M# l title: 实现图, 3 ^7 `5 _3 J& R2 F/ N filename: , 6 `$ M2 c* ~2 l$ R1 O4 Z url: ( i) M. C# U/ Z. z7 N# i/ f& x }% m" F0 G( L& V( D ]; : S$ ?* }" g4 l6 l) [' ]$ O% F4 E; O const handleCompare = () => { $ t, h; |1 S! e/ Y) w# I. Y show = false; / M" J- s5 [& ~ const len = uploaders.filter(f => !!f.filename).length; $ U; P; A: V) s ?! V# Z# {" C if(len == 2) {6 U0 [' D' F; P axios.post(compareUrl, {4 h9 n8 m4 J6 \, O2 Z& ?5 v9 Z1 z designName: uploaders[0][filename],* s& }" U# S+ r8 n! Z3 D codeName: uploaders[1][filename],1 N: @8 n( u6 f8 k) W9 u }).then(res => { ! |. f) _1 G ]& k5 j console.log(compare, res) , x& g; w U; j: r9 O9 U if(res.data.success) { 1 k2 X+ H+ j- J; G# X" N0 [; G# y compareName = res.data.data.compareName;+ | }5 p" ~. S3 W% b% N8 [ return true' a" k- M5 @5 H' O! z& N* j } else {- a" D( A8 U# U9 k0 q flag = error; % p7 T0 D' U& ~- e; V4 i" t+ A show = true;0 p. c- c) F% J3 D errorMsg = res.data.data 8 K" ^' }' B+ d/ T } ; E) @% E7 s: Y! c' ? }).then(c => { % D) k% [! A' @8 y M5 Y4 _ if(c) { - @+ d/ o# Z% } Y; u- P flag = success;& J, v+ z6 w; a; _: x successMsg = 对比成功;& p! l1 x2 m: [ show = true;$ z) ^) I4 k! n. J handleDownload()8 ^& S/ c# S. K+ q handleDelete()% f& ~1 p) E' q } " J0 g t, e4 i" {% u! k5 u }) - O S* H5 L8 n {+ Y } else if(len == 1) {* l5 D3 U" H& P2 m+ m5 E$ ^0 V window.alert(设计图或开发图缺少,请确认已全部上传后再进行比较!) 1 k. k) P4 Q* W0 v9 v1 @$ Q1 M } else {6 `/ Z8 a8 p7 e7 Q% D9 P window.alert(必须有图片才能进行比较!) F3 O! s7 w% O1 z1 g3 q y }5 H% M% h* B3 w( K/ L. e) y };$ o3 x" ]( s8 b, d; q* F, P3 h 9 J& O+ X1 z3 L* o* k8 p const handleBeforeUpload = async function(rawFile, id) {+ y+ ]( q1 X. D* u! a9 k) ?( e: a1 r const fileBase64 = await getBase64(rawFile);) @" c0 \6 C- M, h( }6 r) ` const res = await getPixel(fileBase64); K9 g4 v: ^0 x' d6 O // console.log(res, res) 0 C. u$ _& P2 z2 [- V. o if(res.width == width && res.height == height) {$ ~6 N1 d9 a6 O8 ]" c switch (id) {& S5 G9 \& O2 S- C case design: 7 ]' y/ @7 _1 g# e, H+ [ uploaders[0][url] = fileBase64 $ k) A* n2 {6 z$ ]. y& d break; ' V' q! w% ?/ r. `$ H case code: $ }) t, |- n5 o& F4 e8 U uploaders[1][url] = fileBase644 n }! c- |! ? break;: r0 [4 P" M) H default: & C+ N" O: r" K$ s7 Y break;# Q4 n" D+ P A6 X4 V } : }2 ]/ ` h% g% e ^ return true;+ |# l1 A7 V: y; k. {1 u( M$ @ } else { M1 ?1 ^+ {9 z2 c( A0 s0 R window.alert(上传图片不符合分比率要求); 2 Z& Q7 u( l! o ]* u return false; - f# g7 b1 c9 w* d% X( p" J }3 L0 d1 h6 G3 \; t0 ?# G+ _2 H3 P }# w% [% q- Y4 Z/ M. r + U7 b6 l. E7 R8 v1 p3 T6 G const handleSuccess = (response, rawFile, id) => {1 u9 G1 R. C: E. J3 W4 ` console.log(response, response, rawFile, id); 6 H0 S1 K& A4 Z% u) @/ G if(response.success) { : \" q0 f1 e5 A0 k( l6 Z; F/ R switch (id) { 2 Z+ \, K, H, O+ k1 U5 m case design: 0 W8 z; l; b4 ]2 { uploaders[0][filename] = response.data.filename 2 R( L$ H$ |; ~5 b- n- F break;5 X1 K$ o% G* m* \8 _: |/ E' E! W case code:8 _) B( S, `, R. R uploaders[1][filename] = response.data.filename % _" e9 W: B' C0 f1 u8 }: s6 C# ` break;9 ]" @* @9 b8 z! g4 e default:9 I0 A' z" j! c' n; M4 `$ A break; 5 C& p; K6 g) Q/ `; l& B4 P }% i3 D z# j# b! @' w } 6 r* l0 [$ \) s } $ n( M! `8 k6 v: V; q2 O) n1 t * c x0 G- @1 v' O1 ?1 | function handleDownload() {/ S0 W- `0 t [0 L8 N7 ]) G5 U+ x8 {* } axios({0 B8 l! c8 l) G& Z: e! J method: POST, 3 |: q# v: f# b8 D url: downloadUrl, 5 v3 V; j" ]4 C- H. v7 I3 D0 K responseType: blob,+ }9 P6 y. b4 a5 b% M data: { ; P* h6 O: J5 K4 W5 q- x. e* g2 \ compareName: compareName 3 C- J$ g* i6 X# ~, R; `' `' Y } 7 M! o; W2 q$ y }).then(res => {/ W0 A- C& {* a! F; C console.log(download, res) & i( Z! O: M% m0 S/ b if(res.status == 200) { B- O/ @( a+ p% N* J' U6 t) f1 w var blob = new Blob([res.data]); , G% Z- ^& M9 p# S( F; g // 创建一个URL对象6 H( Q2 }8 H8 f var url = window.URL.createObjectURL(blob); 4 J5 B6 T# m; P: }! k$ ^2 ~ console.log(url, url) ! a0 F/ }- C3 d" o r- J // 创建一个a标签 4 h- c+ t* T) R7 ]. ] var a = document.createElement("a"); 6 n) H/ {3 y1 P% m& o6 H' S a.href = url; % x! W0 X: }. Z+ A8 O a.download = compareName;// 这里指定下载文件的文件名 7 K- J6 M- v: x7 N. _ a.click(); 3 |% g/ p7 Z: K% i1 w8 U. ^# `# T // 释放之前创建的URL对象5 I4 S8 U! R( J) Y window.URL.revokeObjectURL(url); ! g. c3 {0 }8 N' O W+ L } ( @' c1 D4 f! d5 D P" `3 [; N }), j$ \' r4 x+ `& Z7 n& o% h } % p- M2 k' m; G3 X % Y& H; x9 c/ u. a& k( N. D/ S: O function handleDelete() { . h2 H6 }' |% { uploaders = [9 u4 f3 \2 s; l0 F {* b: G. |( Q S0 Z$ S/ Z id: design, # g% i; x$ `2 c4 X title: 设计图, 7 S! |, F5 [( m3 m$ u3 n filename: , % V; o* @ C2 E# k9 L url: 1 R/ p3 n9 e5 e- { },0 V& @0 f$ E9 t0 Q) y; G {( q* K y# p1 P$ R1 d b id: code,+ i7 m* c) s9 K. ]4 h: X6 E: ^ title: 实现图, 6 y+ P5 `; k, s9 c filename: , * N- w ~" _1 u l% M url: 8 z4 [$ D# ?, h0 z/ } }. [6 z% u! e3 K* x ]; 6 r( Y1 Z0 x8 X, { e8 V } 3 z; ^9 G7 j. f* P& R6 ^; Z. E2 w( B$ b , P/ l) a O) S! f 4 R) F$ t n2 H' ~ .pixel-piper {0 r7 S# Z0 D7 K% [% t; g8 P( l; F width: 100%;8 i [' K/ N$ ?- s; t. T1 `2 M height: 100%;5 [; O4 y% O. Z0 e display: flex; E0 L" U; e( T justify-content: center;4 t: H) K I9 t. l& m, { align-items: center;! b1 B9 h" I' T+ W( L. A6 i }4 Z# X }5 s+ u- @0 a% M, f! K / [, k- {( c$ x* S1 i+ D .main {/ E% O8 D. d( t% I2 H width: 600px; 0 ?- L. B4 Z. g D! b margin: 0 auto; 6 [ s: C$ p% k8 l$ r3 [ padding: 20px;5 e/ T! H' [: L border: 1px solid #eee;, a0 n* d+ x) p6 K background-color: #fff;9 B. e* @ ?$ E) t" v+ s4 h" X border-radius: 4px;9 L6 D3 \, s/ w$ r& @ box-shadow: 0 0 10px #e0e0e0; ) b' S) ? z+ |. N! G }! J: \9 P( w: Y4 A) O ) w( g8 t/ C0 J6 w! n6 X1 | .main .logo-container {& X- L" W6 z) k O+ ~7 k display: flex;" U" g* ?5 j% L' M) e0 } justify-content: center; & q$ s" S/ M, M: w6 t% Q: S3 s align-items: center; & p! h- i, ^. U7 ^0 M# M; P- @ } 3 K. X! }) C1 _) w$ S; T" l8 \ 4 ]" n( d1 J/ j6 b8 } .logo-container .logo:hover { 0 O- h- ?; v& r6 B. y opacity: 90%; 9 z0 y9 x6 }+ B6 V- U cursor: pointer; , a1 R3 X/ }/ s* M- K6 f } 8 s! p$ Z5 Y9 U5 G + |; a; K. N2 d4 g' Y .main .select-container { 6 Q3 E5 w0 x( f. _# x! s" a display: flex; * m7 S- y* s3 A align-items: center;+ U5 q4 ?$ u: ` align-content: center;/ d4 E' u" u" [$ q justify-content: center; + N. C/ D- p8 {1 Q9 b) U, ? justify-items: center; ' t! d+ v% ?' h) T' s line-height: 40px; 2 m6 i, X9 q* u- R }% p, @+ r1 `" ~3 M0 S e" j2 S4 b: ^8 X .main .upload-container {" `% Z4 W4 G5 v4 d) \2 U2 D; i display: flex; - k' T; X+ g2 X! H padding: 0 0 10px 0;: M( E) D# f; U' n0 I u8 I justify-content: space-between;0 g" ]7 Z c4 T$ U text-align: center;( T8 ^0 S) z2 y. _$ K } 1 @" g8 X' S( W/ }$ [$ A. d1 Q 3 Z! v. X" W0 E5 l/ A2 o .main .button-container { : Q v# I* V4 X display: flex; % `: J, ?1 P5 a4 [: S3 ?" B justify-content: center;; j2 H5 I2 c! Q* J align-items: center; - X' ?$ M+ N0 a( A } 7 E' \) b K" U2 G. a6 g8 [% a, G, \6 I/ o* n' U .main .info-container { 1 c. c0 f% ~3 o$ N! e text-align: center; 1 `' w- a5 K9 ?; ?, [5 K color: red;) E# ?/ J$ H/ U+ n' \ font-size: 12px;/ x* U& n3 I( ?) N' D# @' e margin: 10px 0; % l5 X( M* l. [0 t ~& I }' ~% m, B* v0 h# t! L7 T! w & E4 x* f5 Y) ?! |* ?9 ? ! c( m1 s# w& Y# ~) }' [- y/ ~% u

, r# H" Y# P1 f9 ` # s2 j2 R0 _8 u4 f! i( g- F
2 o+ N5 Q! m l" o5 ] L$ ? 9 _* k" W U1 g7 G- B5 V - A# ^. M# @% n: {& F5 z& g tooltip=true} : Y1 |- y) M7 S& l, h" P T on:mouseleave={() => tooltip=false}; x2 L6 m0 |* q& t: o > ! T; p" ?: x3 b3 u 3 o9 v: v' [" S# b9 W: _ , [, P0 m/ b3 }4 \" I
1 t# L: z& h W" G
' h3 c" {0 A y2 J) r

x

- q( o' m, A+ k J6 ]
. Z* |) p) r( j7 Y
) O* p9 @6 O7 s* E# _+ P8 \ {#each uploaders as uploader}' b3 ]8 @1 L) q+ ^& k
! x2 @# |# v, J- Y" z ; C% S. q" i; G( {% @# l6 r* l {#if !uploader.url} * i, M% p/ h- o+ G ) I3 w* q9 i; y4 c0 {: } {:else}0 ~4 A, u) S7 N5 O6 n G' C f5 w$ h1 Y {/if} - p- J/ X; S _* S. k4 ?6 R # A( Y" |5 y2 w1 t" r {uploader.title}9 B+ S* |* L, ]& ?5 W$ s
; E2 _) j$ W8 g {/each}' ]/ k. H) H, a
5 A/ x g% g2 J" F5 h# H) i7 a
- c4 N: A7 C; }5 j6 X; U R8 ~" u {#if uploaders.filter(f => !!f.filename).length == 2}% U* s8 s# |+ Q9 n 注:请在两分钟内进行图片对比!!8 s. t& M0 [6 J7 U5 x4 h0 { {/if}' g3 [: s0 V% v2 O
7 h! K8 Q( f: |
" s4 p- Q) W- z2 ^" m7 | 对比$ K4 T! g0 ^8 U8 n2 x/ X6 G3 e {#if uploaders.filter(f => !!f.filename).length == 2} - r" |! H8 H' n2 [: T+ A
, p8 t$ `7 H6 {; U/ J4 z9 { 清除图片 X5 I# o3 X$ V% c" F4 }; Y, m
6 H/ J; p9 P0 k* p {/if}5 J1 m2 T$ V. `# \* v7 F
( y5 J) l& i. S- l4 J- V5 l . O. I/ S, d+ X0 U. H# X4 } {#if flag == success} T7 R4 C4 r$ b2 m {successMsg} ' O; |( r& G9 [' o. k5 c' `! n {:else if flag == error} 8 j4 p/ {1 I* F# M& ~, c/ s2 ~ {errorMsg}7 m3 K% j3 O( N% C4 U3 m7 x {/if}$ p4 O) o& ~5 Q# s0 r; F* H2 R8 w

总结作为前端工程师,我们是距离用户侧最近的研发同学,不仅仅在于完成业务的代码实现,而且不同于其他研发同学,我们也承担着产品用户体验的重要职责,而这其中页面的还原程度是一项重要的指标,能够让设计同学认可我们完成的工作也是评价大家前端能力的一个维度,毕竟上下游通力合作,才能把产品体验做到最佳,共勉!!!

参考Svelte 中文文档一文详解 Svelte干货 | 携程机票前端Svelte生产实践Svelte 原理浅析与评测新兴前端框架 Svelte 从入门到原理被称为“三大框架”替代方案,Svelte如何简化Web开发工作

设计小姐姐都说好的视觉还原对比利器使用Svelte开发Chrome ExtensionChrome插件manifest.json文件详解30分钟开发一款抓取网站图片资源的浏览器插件一天学会Chrome插件开发

暂时无法加载帖子列表

回复

使用道具 举报

今夜有雨 发表于 2026-09-14 13:35:28 | 显示全部楼层
楼主太厉害了,整理得这么详细,必须支持
回复

使用道具 举报

过期域名抢注 发表于 2026-09-22 08:50:07 | 显示全部楼层
分析得很透彻,很多细节都说到点子上了~
回复

使用道具 举报

zx3968 发表于 2026-09-22 10:26:56 | 显示全部楼层
刚好遇到类似问题,看完这个帖子心里有底了
回复

使用道具 举报

jingvo2015 发表于 2026-10-02 14:53:58 | 显示全部楼层
完全赞同,我也是这么认为的,英雄所见略同~
回复

使用道具 举报

yy1g 发表于 2026-10-16 08:37:54 | 显示全部楼层
这个分享太实用了,刚好能用到,感谢楼主!
回复

使用道具 举报

刘chunhui 发表于 2026-10-19 18:06:27 | 显示全部楼层
蹲了这么久,终于看到有价值的讨论,支持一下!
回复

使用道具 举报

    您需要登录后才可以回帖 登录 | 加入怎通

    本版积分规则

    QQ|手机版|小黑屋|网站地图|真牛社区 ( 苏ICP备2023040716号-2 )

    GMT+8, 2026-10-31 09:25 , Processed in 0.043344 second(s), 25 queries , Gzip On.

    免责声明:本站信息来自互联网,本站不对其内容真实性负责,如有侵权等情况请联系420897364#qq.com(把#换成@)删除。

    Powered by Discuz! X3.5

    快速回复 返回顶部 返回列表