mirror of
https://github.com/WeBankFinTech/fes.js.git
synced 2025-04-05 19:41:57 +08:00
feat: 新增icon的能力
This commit is contained in:
parent
e92769aa7d
commit
d574db15b8
@ -13,6 +13,7 @@ const headPkgs = [
|
||||
"fes-plugin-access",
|
||||
"fes-plugin-model",
|
||||
"fes-plugin-layout",
|
||||
"fes-plugin-icon",
|
||||
];
|
||||
const tailPkgs = [];
|
||||
// const otherPkgs = readdirSync(join(__dirname, 'packages')).filter(
|
||||
|
@ -28,7 +28,7 @@ export default class PluginAPI {
|
||||
if (plugins[id]) {
|
||||
const name = plugins[id].isPreset ? 'preset' : 'plugin';
|
||||
throw new Error(
|
||||
`api.describe() failed, ${name} ${id} is already registered by ${plugins[id].path}.`,
|
||||
`api.describe() failed, ${name} ${id} is already registered by ${plugins[id].path}.`
|
||||
);
|
||||
}
|
||||
plugins[id] = plugins[this.id];
|
||||
@ -51,11 +51,11 @@ export default class PluginAPI {
|
||||
register(hook) {
|
||||
assert(
|
||||
hook.key && typeof hook.key === 'string',
|
||||
`api.register() failed, hook.key must supplied and should be string, but got ${hook.key}.`,
|
||||
`api.register() failed, hook.key must supplied and should be string, but got ${hook.key}.`
|
||||
);
|
||||
assert(
|
||||
hook.fn && typeof hook.fn === 'function',
|
||||
`api.register() failed, hook.fn must supplied and should be function, but got ${hook.fn}.`,
|
||||
`api.register() failed, hook.fn must supplied and should be function, but got ${hook.fn}.`
|
||||
);
|
||||
this.service.hooksByPluginId[this.id] = (
|
||||
this.service.hooksByPluginId[this.id] || []
|
||||
@ -66,7 +66,7 @@ export default class PluginAPI {
|
||||
const { name, alias } = command;
|
||||
assert(
|
||||
!this.service.commands[name],
|
||||
`api.registerCommand() failed, the command ${name} is exists.`,
|
||||
`api.registerCommand() failed, the command ${name} is exists.`
|
||||
);
|
||||
this.service.commands[name] = command;
|
||||
if (alias) {
|
||||
@ -79,11 +79,11 @@ export default class PluginAPI {
|
||||
assert(
|
||||
this.service.stage === ServiceStage.initPresets
|
||||
|| this.service.stage === ServiceStage.initPlugins,
|
||||
'api.registerPlugins() failed, it should only be used in registering stage.',
|
||||
'api.registerPlugins() failed, it should only be used in registering stage.'
|
||||
);
|
||||
assert(
|
||||
Array.isArray(plugins),
|
||||
'api.registerPlugins() failed, plugins must be Array.',
|
||||
'api.registerPlugins() failed, plugins must be Array.'
|
||||
);
|
||||
const extraPlugins = plugins.map(plugin => (isValidPlugin(plugin)
|
||||
? (plugin)
|
||||
@ -102,11 +102,11 @@ export default class PluginAPI {
|
||||
registerPresets(presets) {
|
||||
assert(
|
||||
this.service.stage === ServiceStage.initPresets,
|
||||
'api.registerPresets() failed, it should only used in presets.',
|
||||
'api.registerPresets() failed, it should only used in presets.'
|
||||
);
|
||||
assert(
|
||||
Array.isArray(presets),
|
||||
'api.registerPresets() failed, presets must be Array.',
|
||||
'api.registerPresets() failed, presets must be Array.'
|
||||
);
|
||||
const extraPresets = presets.map(preset => (isValidPlugin(preset)
|
||||
? (preset)
|
||||
@ -127,7 +127,7 @@ export default class PluginAPI {
|
||||
if (this.service.pluginMethods[name]) {
|
||||
if (exitsError) {
|
||||
throw new Error(
|
||||
`api.registerMethod() failed, method ${name} is already exist.`,
|
||||
`api.registerMethod() failed, method ${name} is already exist.`
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
|
3
packages/fes-plugin-icon/.fatherrc.js
Normal file
3
packages/fes-plugin-icon/.fatherrc.js
Normal file
@ -0,0 +1,3 @@
|
||||
export default {
|
||||
disableTypeCheck: false,
|
||||
};
|
22
packages/fes-plugin-icon/package.json
Normal file
22
packages/fes-plugin-icon/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@webank/fes-plugin-icon",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "lib/index.js",
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"module": "dist/index.esm.js",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"svgo": "1.3.2"
|
||||
}
|
||||
}
|
39
packages/fes-plugin-icon/src/Icon/Icon.jsx
Normal file
39
packages/fes-plugin-icon/src/Icon/Icon.jsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { computed } from 'vue';
|
||||
// eslint-disable-next-line
|
||||
import icons from './icons';
|
||||
|
||||
const noop = () => { };
|
||||
|
||||
export default {
|
||||
name: 'FesIcon',
|
||||
props: ['type', 'spin', 'rotate', 'tabIndex'],
|
||||
setup(props, { attrs }) {
|
||||
const CurrentIcon = computed(() => icons[props.type]);
|
||||
const iconTabIndex = computed(() => {
|
||||
let tabIndex = props.tabIndex;
|
||||
if (tabIndex == null && attrs.onClick) {
|
||||
tabIndex = -1;
|
||||
}
|
||||
return tabIndex;
|
||||
});
|
||||
const svgStyle = computed(() => (props.rotate
|
||||
? {
|
||||
msTransform: `rotate(${props.rotate}deg)`,
|
||||
transform: `rotate(${props.rotate}deg)`
|
||||
}
|
||||
: null));
|
||||
const svgCls = computed(() => ({
|
||||
'inner-icon--spin': !!props.spin || props.type === 'loading'
|
||||
}));
|
||||
return () => (
|
||||
<span
|
||||
tabIndex={iconTabIndex.value}
|
||||
role="img"
|
||||
class="inner-icon"
|
||||
onClick={attrs.onClick || noop}
|
||||
>
|
||||
<CurrentIcon.value class={svgCls.value} style={svgStyle.value} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
@ -1,8 +1,6 @@
|
||||
@import "~@/styles/theme.less";
|
||||
|
||||
.inner-icon {
|
||||
display: inline-block;
|
||||
color: @icon-color;
|
||||
color: inherit;
|
||||
font-style: normal;
|
||||
line-height: 0;
|
||||
text-align: center;
|
||||
@ -19,6 +17,9 @@
|
||||
|
||||
svg {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
&::before {
|
9
packages/fes-plugin-icon/src/icons.tpl
Normal file
9
packages/fes-plugin-icon/src/icons.tpl
Normal file
@ -0,0 +1,9 @@
|
||||
{{#ICON_NAMES}}
|
||||
import smile from './icons/{{.}}';
|
||||
{{/ICON_NAMES}}
|
||||
|
||||
export default {
|
||||
{{#ICON_NAMES}}
|
||||
{{.}}
|
||||
{{/ICON_NAMES}}
|
||||
};
|
76
packages/fes-plugin-icon/src/index.js
Normal file
76
packages/fes-plugin-icon/src/index.js
Normal file
@ -0,0 +1,76 @@
|
||||
import { readFileSync, copyFileSync, statSync } from 'fs';
|
||||
import { join, basename } from 'path';
|
||||
import optimizeSvg from './optimizeSvg';
|
||||
|
||||
export default (api) => {
|
||||
api.addRuntimePluginKey(() => '');
|
||||
// 配置
|
||||
api.describe({
|
||||
key: 'icon',
|
||||
config: {
|
||||
schema(joi) {
|
||||
return joi.object();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const namespace = 'plugin-icon';
|
||||
const absRuntimeFilePath = join(namespace, 'runtime.js');
|
||||
|
||||
// TODO 监听 icons 文件变更,重新生成文件
|
||||
api.onGenerateFiles(async () => {
|
||||
const base = join(api.paths.absSrcPath, 'icons');
|
||||
const iconFiles = api.utils.glob.sync('**/*', {
|
||||
cwd: join(api.paths.absSrcPath, 'icons')
|
||||
});
|
||||
const svgDatas = await optimizeSvg(iconFiles.map(item => join(base, item)));
|
||||
const iconNames = [];
|
||||
const SVG_COMPONENT_TMPLATE = 'export default () => (SVG)';
|
||||
for (const { fileName, data } of svgDatas) {
|
||||
iconNames.push(basename(fileName, '.svg'));
|
||||
api.writeTmpFile({
|
||||
path: `${namespace}/icons/${basename(fileName, '.svg')}.js`,
|
||||
content: SVG_COMPONENT_TMPLATE
|
||||
.replace('SVG', data)
|
||||
});
|
||||
}
|
||||
|
||||
api.writeTmpFile({
|
||||
path: `${namespace}/icons.js`,
|
||||
content: api.utils.Mustache.render(
|
||||
readFileSync(join(__dirname, './icons.tpl'), 'utf-8'),
|
||||
{
|
||||
ICON_NAMES: iconNames
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
api.writeTmpFile({
|
||||
path: absRuntimeFilePath,
|
||||
content: api.utils.Mustache.render(readFileSync(join(__dirname, 'runtime.tpl'), 'utf-8'), {
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
let generatedOnce = false;
|
||||
api.onGenerateFiles(() => {
|
||||
if (generatedOnce) return;
|
||||
generatedOnce = true;
|
||||
const cwd = join(__dirname, '../src/Icon');
|
||||
const files = api.utils.glob.sync('**/*', {
|
||||
cwd
|
||||
});
|
||||
const base = join(api.paths.absTmpPath, namespace);
|
||||
files.forEach((file) => {
|
||||
const source = join(cwd, file);
|
||||
const target = join(base, file);
|
||||
if (statSync(source).isDirectory()) {
|
||||
api.utils.mkdirp.sync(target);
|
||||
} else {
|
||||
copyFileSync(source, target);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
api.addRuntimePlugin(() => `@@/${absRuntimeFilePath}`);
|
||||
};
|
91
packages/fes-plugin-icon/src/optimizeSvg.js
Normal file
91
packages/fes-plugin-icon/src/optimizeSvg.js
Normal file
@ -0,0 +1,91 @@
|
||||
import { extname, basename } from 'path';
|
||||
import { statSync, readFileSync } from 'fs';
|
||||
|
||||
|
||||
import SVGO from 'svgo/lib/svgo';
|
||||
|
||||
const svgo = new SVGO({
|
||||
plugins: [{
|
||||
cleanupAttrs: true
|
||||
}, {
|
||||
removeDoctype: true
|
||||
}, {
|
||||
removeXMLProcInst: true
|
||||
}, {
|
||||
removeComments: true
|
||||
}, {
|
||||
removeMetadata: true
|
||||
}, {
|
||||
removeTitle: true
|
||||
}, {
|
||||
removeDesc: true
|
||||
}, {
|
||||
removeUselessDefs: true
|
||||
}, {
|
||||
removeEditorsNSData: true
|
||||
}, {
|
||||
removeEmptyAttrs: true
|
||||
}, {
|
||||
removeHiddenElems: true
|
||||
}, {
|
||||
removeEmptyText: true
|
||||
}, {
|
||||
removeEmptyContainers: true
|
||||
}, {
|
||||
removeViewBox: false
|
||||
}, {
|
||||
cleanupEnableBackground: true
|
||||
}, {
|
||||
convertStyleToAttrs: true
|
||||
}, {
|
||||
convertColors: true
|
||||
}, {
|
||||
convertPathData: true
|
||||
}, {
|
||||
convertTransform: true
|
||||
}, {
|
||||
removeUnknownsAndDefaults: true
|
||||
}, {
|
||||
removeNonInheritableGroupAttrs: true
|
||||
}, {
|
||||
removeUselessStrokeAndFill: true
|
||||
}, {
|
||||
removeUnusedNS: true
|
||||
}, {
|
||||
cleanupIDs: true
|
||||
}, {
|
||||
cleanupNumericValues: true
|
||||
}, {
|
||||
moveElemsAttrsToGroup: true
|
||||
}, {
|
||||
moveGroupAttrsToElems: true
|
||||
}, {
|
||||
collapseGroups: true
|
||||
}, {
|
||||
removeRasterImages: false
|
||||
}, {
|
||||
mergePaths: true
|
||||
}, {
|
||||
convertShapeToPath: true
|
||||
}, {
|
||||
sortAttrs: true
|
||||
}, {
|
||||
removeDimensions: true
|
||||
}, {
|
||||
removeAttrs: { attrs: '(stroke|fill)' }
|
||||
}]
|
||||
});
|
||||
|
||||
export default function optimizeSvg(files) {
|
||||
const optimizedSvgData = [];
|
||||
for (const filePath of files) {
|
||||
if (statSync(filePath).isFile() && extname(filePath) === '.svg') {
|
||||
const data = readFileSync(filePath, 'utf-8');
|
||||
optimizedSvgData.push(svgo.optimize(data, { path: filePath }).then(svgData => ({
|
||||
fileName: basename(filePath),
|
||||
...svgData
|
||||
})));
|
||||
}
|
||||
}
|
||||
return Promise.all(optimizedSvgData);
|
||||
}
|
5
packages/fes-plugin-icon/src/runtime.tpl
Normal file
5
packages/fes-plugin-icon/src/runtime.tpl
Normal file
@ -0,0 +1,5 @@
|
||||
import Icon from './index';
|
||||
|
||||
export function onAppCreated({ app }) {
|
||||
app.component('fes-icon', Icon);
|
||||
}
|
@ -46,7 +46,7 @@ export default (api) => {
|
||||
});
|
||||
|
||||
api.addHTMLHeadScripts(() => [{
|
||||
src: `${api.config.publicPath}@@/devScripts.js`
|
||||
src: '@@/devScripts.js'
|
||||
}]);
|
||||
|
||||
api.onGenerateFiles(() => {
|
||||
|
@ -112,7 +112,7 @@ export default (api) => {
|
||||
webpackConfig
|
||||
.plugin('html-tags')
|
||||
.use(HtmlWebpackTagsPlugin, [{
|
||||
append: true,
|
||||
append: false,
|
||||
scripts: headScripts.map(script => ({
|
||||
path: script.src
|
||||
}))
|
||||
|
@ -34,6 +34,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.0.4",
|
||||
"@webank/fes": "^2.0.0"
|
||||
"@webank/fes": "^2.0.0",
|
||||
"@webank/fes-plugin-icon": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<span
|
||||
:aria-label="type"
|
||||
v-bind="$attrs"
|
||||
:tabIndex="iconTabIndex"
|
||||
role="img"
|
||||
class="inner-icon"
|
||||
>
|
||||
<!-- <VueIcon :class="svgCls" :icon="icon" :style="svgStyle" /> -->
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import VueIcon from './IconBase';
|
||||
import icons from './icons';
|
||||
|
||||
export default {
|
||||
name: 'Icon',
|
||||
components: {
|
||||
// VueIcon
|
||||
},
|
||||
props: ['type', 'spin', 'rotate', 'tabIndex'],
|
||||
computed: {
|
||||
icon() {
|
||||
console.log(icons[this.type]);
|
||||
return icons[this.type];
|
||||
},
|
||||
svgCls() {
|
||||
return {
|
||||
'inner-icon--spin': !!this.spin || this.type === 'loading'
|
||||
};
|
||||
},
|
||||
svgStyle() {
|
||||
return this.rotate
|
||||
? {
|
||||
msTransform: `rotate(${this.rotate}deg)`,
|
||||
transform: `rotate(${this.rotate}deg)`
|
||||
}
|
||||
: null;
|
||||
},
|
||||
iconTabIndex() {
|
||||
let iconTabIndex = this.tabIndex;
|
||||
if (iconTabIndex == null && this.$listeners.click) {
|
||||
iconTabIndex = -1;
|
||||
}
|
||||
return iconTabIndex;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -1,36 +0,0 @@
|
||||
import { generate } from './util';
|
||||
|
||||
const IconBase = {
|
||||
functional: true,
|
||||
props: ['icon'],
|
||||
render(h, ctx) {
|
||||
const {
|
||||
data: { attrs, ...restData } = {},
|
||||
props = {},
|
||||
listeners
|
||||
} = ctx;
|
||||
const { icon, ...restProps } = {
|
||||
...attrs,
|
||||
...props
|
||||
};
|
||||
if (!icon) return null;
|
||||
const target = icon;
|
||||
|
||||
return generate(h, target.icon, `svg-${target.name}`, {
|
||||
...restData,
|
||||
attrs: {
|
||||
'data-icon': target.name,
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
fill: 'currentColor',
|
||||
'aria-hidden': 'true',
|
||||
...restProps
|
||||
},
|
||||
on: listeners
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
IconBase.name = 'IconBase';
|
||||
|
||||
export default IconBase;
|
@ -1,8 +0,0 @@
|
||||
/* Automatically generated by './build/bin/gen-icons.js' */
|
||||
|
||||
import smile from './icons/smile.svg';
|
||||
|
||||
|
||||
export default {
|
||||
smile
|
||||
};
|
@ -1,33 +0,0 @@
|
||||
export function normalizeAttrs(attrs = {}) {
|
||||
return Object.keys(attrs).reduce((acc, key) => {
|
||||
const val = attrs[key];
|
||||
switch (key) {
|
||||
case 'class':
|
||||
acc.className = val;
|
||||
delete acc.class;
|
||||
break;
|
||||
default:
|
||||
acc[key] = val;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function generate(h, node, key, rootProps) {
|
||||
if (!rootProps) {
|
||||
return h(
|
||||
node.tag,
|
||||
{ key, attrs: { ...normalizeAttrs(node.attrs) } },
|
||||
(node.children || []).map((child, index) => generate(h, child, `${key}-${node.tag}-${index}`))
|
||||
);
|
||||
}
|
||||
return h(
|
||||
node.tag,
|
||||
{
|
||||
key,
|
||||
...rootProps,
|
||||
attrs: { ...normalizeAttrs(node.attrs), ...rootProps.attrs }
|
||||
},
|
||||
(node.children || []).map((child, index) => generate(h, child, `${key}-${node.tag}-${index}`))
|
||||
);
|
||||
}
|
Before Width: | Height: | Size: 936 B After Width: | Height: | Size: 936 B |
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="onepiece">
|
||||
fes & 拉夫德鲁 <br />
|
||||
<!-- <Icon type="smile"></Icon> -->
|
||||
<fes-icon @click="clickIcon" :spin="true" class="one-icon" type="smile" />
|
||||
</div>
|
||||
</template>
|
||||
<config>
|
||||
@ -18,6 +18,7 @@ import { useRouter } from '@webank/fes';
|
||||
export default {
|
||||
setup() {
|
||||
const fes = ref('fes upgrade to vue3');
|
||||
const rotate = ref(90);
|
||||
const router = useRouter();
|
||||
onMounted(() => {
|
||||
console.log(router);
|
||||
@ -27,8 +28,13 @@ export default {
|
||||
onMounted(() => {
|
||||
console.log('mounted2!!');
|
||||
});
|
||||
const clickIcon = () => {
|
||||
console.log('click Icon');
|
||||
};
|
||||
return {
|
||||
fes
|
||||
fes,
|
||||
rotate,
|
||||
clickIcon
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -36,6 +42,7 @@ export default {
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import "~@/styles/mixins/hairline";
|
||||
@import "~@/styles/mixins/hover";
|
||||
|
||||
div {
|
||||
padding: 20px;
|
||||
@ -43,8 +50,13 @@ div {
|
||||
margin: 20px;
|
||||
}
|
||||
}
|
||||
.one-icon {
|
||||
color: yellow;
|
||||
font-size: 24px;
|
||||
.hover();
|
||||
}
|
||||
.onepiece {
|
||||
.hairline("top");
|
||||
background: url('../images/male.png');
|
||||
// background: url('../images/male.png');
|
||||
}
|
||||
</style>
|
||||
|
@ -18,6 +18,7 @@
|
||||
html:not([data-scale]) & {
|
||||
@media (min-resolution: 2dppx) {
|
||||
border-top: none;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
.scale-hairline-common(@color, 0, auto, auto, 0);
|
||||
@ -40,6 +41,7 @@
|
||||
html:not([data-scale]) & {
|
||||
@media (min-resolution: 2dppx) {
|
||||
border-right: none;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
.scale-hairline-common(@color, 0, 0, auto, auto);
|
||||
@ -63,6 +65,7 @@
|
||||
html:not([data-scale]) & {
|
||||
@media (min-resolution: 2dppx) {
|
||||
border-bottom: none;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
.scale-hairline-common(@color, auto, auto, 0, 0);
|
||||
@ -85,6 +88,7 @@
|
||||
html:not([data-scale]) & {
|
||||
@media (min-resolution: 2dppx) {
|
||||
border-left: none;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
.scale-hairline-common(@color, 0, auto, auto, 0);
|
||||
|
@ -13752,7 +13752,7 @@ svg-tags@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
|
||||
integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=
|
||||
|
||||
svgo@^1.0.0, svgo@^1.2.2:
|
||||
svgo@1.3.2, svgo@^1.0.0, svgo@^1.2.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"
|
||||
integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==
|
||||
|
Loading…
x
Reference in New Issue
Block a user