实现button样式、尺寸及禁用状态
This commit is contained in:
parent
2a63711c8a
commit
e729aa7adb
|
@ -1,2 +1,42 @@
|
|||
# Farris UI Vue 贡献指南
|
||||
|
||||
下面是参与贡献 Farris UI 的共享指南,请在反馈`issue`和`Pull Request`之前,花费几分钟阅读以下内容。
|
||||
你也可以根据自己的实践经验,自由的通过`Pull Reqeust`修改完善这个指南。
|
||||
|
||||
## 反馈Issue
|
||||
|
||||
如有你认为自己新发现了一个bug,或者提出一个新特性,请先确定之前没有人提出或者修复过这个bug或者特性。你可以在Issues列表和PR列表中搜索是否有人已经提出了类似bug或者特性。
|
||||
|
||||
接下来你可以在Issue列表界面创建一个新的Issue,我们为你提供了Issue模板,请在Issue模板中标记是要反馈「bug」还是「新特性」,并提供必须的上下文说明,以便于开发者可以清晰的理解你的意图。
|
||||
|
||||
## 领取Issue
|
||||
|
||||
如果你对修复某个bug,或者实现某个新特性敢兴趣,请在这个Issue下方通过评论告诉我们,我们可以进行充分沟通这个「bug」或者「新特性」的具体细节,然后会将Issue的负责人指定给你,将有你负责后续开发工作。
|
||||
请务必首先认领Issue然后在在开启你的贡献工作。
|
||||
|
||||
## 贡献代码
|
||||
|
||||
如果你成功领取了项目Issue,请通过Gitee推荐的「fork + pull request」的方式贡献代码。
|
||||
为了保证项目代码质量,我们指定了详细的编码风格指南。
|
||||
为了你的PR可以顺利通过代码审查,请在编码前认真阅读以下**编码指南**:
|
||||
|
||||
- [Farris UI TypeScript 编码指南](./style-guide/typescript_style_guide.md)
|
||||
|
||||
- [Farris UI Vue 组件编码指南](./style-guide/vue_component_style_guide.md)
|
||||
|
||||
## 提交Pull Request
|
||||
|
||||
我们欢迎你通过提交PR参与项目贡献,在你计划提交PR前,请先阅读以下注意事项:
|
||||
|
||||
- 在你提交PR之前请确保已经开启了一个Issue并认领了它,我们只接收与认领Issue关联的PR。如果你打算实现一个比较大的特性,在开启新的Issue前最好先与项目管理者进行充分讨论。
|
||||
|
||||
- 在没有十足把握时,尽量提交小规格的PR。不要在一个PR中修复多于一个bug或实现多于一个新特性,以便于更容易被接受。提交两个小规模的PR,会比提交一个大规模修改的PR要好。
|
||||
|
||||
- 当你提交新特性,或者修改已有特性时,请包含相应的测试代码,以便于确认组件新的交互特性。
|
||||
|
||||
- 在提交PR前端请先执行Rebase以便于保持干净的历史提交记录。
|
||||
|
||||
- 我们提供了PR模板,请在提交PR时安装模板要求提供「修改的内容」、「管理的PR」、「测试用例」、「界面预览」等相关内容。
|
||||
|
||||
|
||||
<a rel="license" href="https://creativecommons.org/licenses/by/3.0/"><img alt="Creative Commons License" style="border-width:0" src="https://i.creativecommons.org/l/by/3.0/88x31.png" /></a>
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
const types = ['config', 'feature', 'fix', 'docs', 'style', 'refactor', 'performance', 'test', 'build', 'release', 'chore', 'revert'];
|
||||
|
||||
module.exports = {
|
||||
parserPreset: { parserOpts: { headerPattern: /^(\w*)(?:\((.*)\))?!?: (.*)$/ } },
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
'type-empty': [2, 'never'],
|
||||
'type-enum': [2, 'always', types],
|
||||
'scope-case': [0, 'always'],
|
||||
'subject-empty': [2, 'never'],
|
||||
'subject-case': [0, 'never'],
|
||||
'header-max-length': [2, 'always', 88],
|
||||
},
|
||||
prompt: {
|
||||
questions: {
|
||||
type: {
|
||||
description: "Select the type of change that you're committing",
|
||||
enum: {
|
||||
config: {
|
||||
description: 'Changes that affect the tools, such as eslint, npm, vscode.',
|
||||
title: 'Config',
|
||||
emoji: '🛠',
|
||||
},
|
||||
feature: {
|
||||
description: 'A new feature',
|
||||
title: 'Features',
|
||||
emoji: '✨',
|
||||
},
|
||||
fix: {
|
||||
description: 'A bug fix',
|
||||
title: 'Bug Fixes',
|
||||
emoji: '🐛',
|
||||
},
|
||||
docs: {
|
||||
description: 'Documentation only changes',
|
||||
title: 'Documentation',
|
||||
emoji: '📚',
|
||||
},
|
||||
style: {
|
||||
description: 'Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)',
|
||||
title: 'Styles',
|
||||
emoji: '💎',
|
||||
},
|
||||
refactor: {
|
||||
description: 'A code change that neither fixes a bug nor adds a feature',
|
||||
title: 'Code Refactoring',
|
||||
emoji: '📦',
|
||||
},
|
||||
performance: {
|
||||
description: 'A code change that improves performance',
|
||||
title: 'Performance Improvements',
|
||||
emoji: '🚀',
|
||||
},
|
||||
test: {
|
||||
description: 'Adding missing tests or correcting existing tests',
|
||||
title: 'Tests',
|
||||
emoji: '🚨',
|
||||
},
|
||||
build: {
|
||||
description: 'Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)',
|
||||
title: 'Builds',
|
||||
emoji: '🛠',
|
||||
},
|
||||
ci: {
|
||||
description: 'Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)',
|
||||
title: 'Continuous Integrations',
|
||||
emoji: '⚙️',
|
||||
},
|
||||
chore: {
|
||||
description: "Other changes that don't modify src or test files",
|
||||
title: 'Chores',
|
||||
emoji: '♻️',
|
||||
},
|
||||
revert: {
|
||||
description: 'Reverts a previous commit',
|
||||
title: 'Reverts',
|
||||
emoji: '🗑',
|
||||
},
|
||||
},
|
||||
},
|
||||
scope: {
|
||||
description: 'What is the scope of this change (e.g. component or file name)',
|
||||
},
|
||||
subject: {
|
||||
description: 'Write a short, imperative tense description of the change',
|
||||
},
|
||||
body: {
|
||||
description: 'Provide a longer description of the change',
|
||||
},
|
||||
isBreaking: {
|
||||
description: 'Are there any breaking changes?',
|
||||
},
|
||||
breakingBody: {
|
||||
description: 'A BREAKING CHANGE commit requires a body. Please enter a longer description of the commit itself',
|
||||
},
|
||||
breaking: {
|
||||
description: 'Describe the breaking changes',
|
||||
},
|
||||
isIssueAffected: {
|
||||
description: 'Does this change affect any open issues?',
|
||||
},
|
||||
issuesBody: {
|
||||
description: 'If issues are closed, the commit requires a body. Please enter a longer description of the commit itself',
|
||||
},
|
||||
issues: {
|
||||
description: 'Add issue references (e.g. "fix #123", "re #123".)',
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
|
@ -4,5 +4,7 @@
|
|||
],
|
||||
"version": "0.0.0",
|
||||
"useWorkspaces": true,
|
||||
"npmClient": "yarn"
|
||||
"npmClient": "yarn",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"useNx": false
|
||||
}
|
35
package.json
35
package.json
|
@ -1,10 +1,41 @@
|
|||
{
|
||||
"name": "farris-vue",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"lerna": "^4.0.0"
|
||||
"@commitlint/cli": "^17.1.0",
|
||||
"@commitlint/config-conventional": "^17.1.0",
|
||||
"@farris/eslint-config": "^1.0.0",
|
||||
"@ls-lint/ls-lint": "^1.11.0",
|
||||
"@types/jest": "^29.0.1",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/node": "^18.7.16",
|
||||
"esbuild-register": "^3.3.0",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-vue": "^9.4.0",
|
||||
"husky": "^8.0.0",
|
||||
"intersection-observer": "^0.12.2",
|
||||
"lerna": "^5.5.4",
|
||||
"lint-staged": "^13.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"stylelint": "^14.11.0",
|
||||
"stylelint-config-recommended-scss": "^7.0.0",
|
||||
"stylelint-config-standard": "^28.0.0",
|
||||
"stylelint-scss": "^3.3.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"packages/docs-vue/{*.vue,*.js,*.ts,*.jsx,*.tsx}": "eslint --fix",
|
||||
"packages/docs-vue/{*.scss,*.css}": "stylelint --fix",
|
||||
"packages/ui-vue/{*.vue,*.js,*.ts,*.jsx,*.tsx}": "eslint --fix",
|
||||
"packages/ui-vue/{*.scss,*.css}": "stylelint --fix"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
]
|
||||
],
|
||||
"dependencies": {
|
||||
"jest-environment-jsdom": "^29.0.3"
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
// This starter template is using Vue 3 <script setup> SFCs
|
||||
// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
import HelloWorld from './components/HelloWorld.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref } from 'vue';
|
||||
|
||||
defineProps<{ msg: string }>()
|
||||
defineProps<{ msg: string }>();
|
||||
|
||||
const count = ref(0)
|
||||
const count = ref(0);
|
||||
</script>
|
||||
|
||||
<template>
|
|
@ -1,7 +1,8 @@
|
|||
/// <reference types="vite/client" />
|
||||
// / <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
import type { DefineComponent } from 'vue';
|
||||
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"skipLibCheck": true
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,169 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: [
|
||||
'plugin:vue/vue3-recommended',
|
||||
'airbnb-base',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'prettier',
|
||||
],
|
||||
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser',
|
||||
ecmaVersion: 2019,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
tsx: true,
|
||||
},
|
||||
extraFileExtensions: ['.vue']
|
||||
},
|
||||
|
||||
plugins: ['@typescript-eslint'],
|
||||
|
||||
env: {
|
||||
es6: true,
|
||||
node: true,
|
||||
jest: true,
|
||||
browser: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
'accessor-pairs': 'off',
|
||||
'array-callback-return': 'off',
|
||||
"arrow-body-style":'off',
|
||||
'curly': 'error',
|
||||
'class-methods-use-this': 'off',
|
||||
'complexity': [
|
||||
'error',
|
||||
{
|
||||
max: 40,
|
||||
},
|
||||
],
|
||||
'consistent-return': 'off',
|
||||
'default-case': 'off',
|
||||
'eol-last': 'error',
|
||||
'eqeqeq': ['error', 'smart'],
|
||||
'func-names': 'off',
|
||||
// eslint-plugin-import
|
||||
'import/order': 'off',
|
||||
'import/extensions': 'off',
|
||||
'import/no-unresolved': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'max-depth': 'off',
|
||||
'max-len': ['error', { code: 140 }],
|
||||
'max-nested-callbacks': ['error', 6],
|
||||
'max-params': 'off',
|
||||
'no-new': 'off',
|
||||
'no-bitwise': 'off',
|
||||
// 'no-console': 'off',
|
||||
'no-console': [
|
||||
'error',
|
||||
{
|
||||
allow: [
|
||||
'log',
|
||||
'warn',
|
||||
'dir',
|
||||
'timeLog',
|
||||
'assert',
|
||||
'clear',
|
||||
'count',
|
||||
'countReset',
|
||||
'group',
|
||||
'groupEnd',
|
||||
'table',
|
||||
'dirxml',
|
||||
'error',
|
||||
'groupCollapsed',
|
||||
'Console',
|
||||
'profile',
|
||||
'profileEnd',
|
||||
'timeStamp',
|
||||
'context',
|
||||
]
|
||||
}
|
||||
],
|
||||
'no-multiple-empty-lines': 'error',
|
||||
'no-restricted-globals': 'off',
|
||||
'no-shadow': 'off',
|
||||
'no-trailing-spaces': 'error',
|
||||
'no-param-reassign': 'off',
|
||||
'no-plusplus': 'off',
|
||||
'no-nested-ternary': 'off',
|
||||
'no-underscore-dangle': 'off',
|
||||
'no-unused-expressions': 'off',
|
||||
'no-unused-labels': 'error',
|
||||
'no-use-before-define': 'error',
|
||||
'no-useless-constructor': 'off',
|
||||
'no-useless-concat': 'off',
|
||||
'no-var': 'error',
|
||||
'prefer-const': 'error',
|
||||
'prefer-destructuring': ['error', { object: true, array: false }],
|
||||
'prefer-promise-reject-errors': 'off',
|
||||
'prefer-template': 'off',
|
||||
'semi': 'error',
|
||||
'space-in-parens': ['error', 'never'],
|
||||
'spaced-comment': ['error', 'always'],
|
||||
// typescript-eslint
|
||||
'@typescript-eslint/camelcase': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-useless-constructor': 'off',
|
||||
'@typescript-eslint/no-parameter-properties': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
'@typescript-eslint/dot-notation': 'off',
|
||||
'@typescript-eslint/indent': [
|
||||
'error',
|
||||
4,
|
||||
{ FunctionDeclaration: { parameters: 'first' }, FunctionExpression: { parameters: 'first' } },
|
||||
],
|
||||
'@typescript-eslint/member-delimiter-style': [
|
||||
'error',
|
||||
{
|
||||
multiline: {
|
||||
delimiter: 'semi',
|
||||
requireLast: true,
|
||||
},
|
||||
singleline: {
|
||||
delimiter: 'semi',
|
||||
requireLast: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-misused-new': 'error',
|
||||
'@typescript-eslint/prefer-function-type': 'error',
|
||||
'@typescript-eslint/semi': ['error', 'always'],
|
||||
'@typescript-eslint/type-annotation-spacing': 'error',
|
||||
'@typescript-eslint/unified-signatures': 'error',
|
||||
'@typescript-eslint/no-shadow': 'off',
|
||||
'@typescript-eslint/member-ordering': 'off',
|
||||
'@typescript-eslint/no-this-alias': 'off',
|
||||
// eslint-plugin-vue
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/attributes-order': 'off',
|
||||
'vue/require-v-for-key': 'off',
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/no-unused-components': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/return-in-computed-property': 'off'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.vue'],
|
||||
parser: require.resolve('vue-eslint-parser'),
|
||||
},
|
||||
{
|
||||
files: ['**/*.md/*.js', '**/*.md/*.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
},
|
||||
},
|
||||
]
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
/** @type {import('@jest/types').Config.InitialOptions} */
|
||||
module.exports = {
|
||||
resolver: '<rootDir>/jest.resolver.js',
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
// https://github.com/facebook/jest/issues/9771#issuecomment-871585234
|
||||
const resolver = require('enhanced-resolve').create.sync({
|
||||
conditionNames: ['require', 'node', 'default'],
|
||||
extensions: ['.js', '.json', '.node', '.ts', '.tsx'],
|
||||
});
|
||||
|
||||
module.exports = function (request, options) {
|
||||
// list global module that must be resolved by defaultResolver here
|
||||
if (['fs', 'http', 'path'].includes(request)) {
|
||||
return options.defaultResolver(request, options);
|
||||
}
|
||||
return resolver(options.basedir, request);
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "@farris/eslint-config",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.36.1",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "^5.36.1",
|
||||
"@typescript-eslint/parser": "^5.36.1",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-vue": "^9.4.0",
|
||||
"enhanced-resolve": "^5.10.0",
|
||||
"vue": "^3.2.37"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.23.0",
|
||||
"typescript": "^4.8.2",
|
||||
"@vitejs/plugin-vue": "^3.1.0",
|
||||
"@vitejs/plugin-vue-jsx": "^2.0.1",
|
||||
"@vue/babel-plugin-jsx": "^1.1.1",
|
||||
"vite": "^3.1.0",
|
||||
"vue-tsc": "^0.40.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^7.32.0 || ^8.2.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"root": true,
|
||||
"extends": ["../index"]
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
const { ESLint } = require('eslint');
|
||||
const path = require('path');
|
||||
|
||||
const eslint = new ESLint();
|
||||
|
||||
async function lintProject(name) {
|
||||
const projectPath = path.resolve(__dirname, name);
|
||||
const filesToLint = path.resolve(projectPath, '**');
|
||||
const rest = await eslint.lintFiles(filesToLint);
|
||||
const ruleId = [];
|
||||
rest.forEach((res) =>
|
||||
res.messages.forEach((msg) => {
|
||||
if (ruleId.indexOf(msg.ruleId) < 0) {
|
||||
ruleId.push(msg.ruleId);
|
||||
}
|
||||
})
|
||||
);
|
||||
return ruleId;
|
||||
}
|
||||
|
||||
test('a vue project should pass lint', async () => {
|
||||
const rest = await lintProject('vue');
|
||||
|
||||
expect([
|
||||
'no-const-assign',
|
||||
'@typescript-eslint/no-unused-vars',
|
||||
'vue/multi-word-component-names',
|
||||
'no-undef',
|
||||
]).toEqual(rest);
|
||||
});
|
||||
|
||||
test('a vue-tsx project should pass lint', async () => {
|
||||
const rest = await lintProject('vue-tsx');
|
||||
|
||||
expect([
|
||||
'@typescript-eslint/no-unused-vars',
|
||||
'vue/multi-word-component-names',
|
||||
'vue/no-ref-as-operand',
|
||||
'@typescript-eslint/no-empty-interface',
|
||||
]).toEqual(rest);
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
setup() {
|
||||
return () => (
|
||||
<>
|
||||
<h1>App</h1>
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
import { defineComponent, ref } from 'vue';
|
||||
import App from './app';
|
||||
|
||||
const h2 = 1;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Index',
|
||||
setup() {
|
||||
const count = ref(0);
|
||||
|
||||
count++;
|
||||
count + 1;
|
||||
1 + count;
|
||||
|
||||
return () => (
|
||||
<>
|
||||
<h1>About</h1>
|
||||
|
||||
<App />
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
interface Foo {}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"],
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", "**/*.vue"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<div>
|
||||
<span v-for="a in arr">{{ a }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const a = 1;
|
||||
a = 1;
|
||||
export default {
|
||||
name: 'Todo',
|
||||
data() {
|
||||
return {
|
||||
arr: [1, 2, 3],
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
|
@ -0,0 +1,2 @@
|
|||
const a = b + 1;
|
||||
export default a;
|
|
@ -11,6 +11,7 @@ node_modules
|
|||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
coverage
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { isContext } from 'vm';
|
||||
import { computed, defineComponent, SetupContext } from 'vue';
|
||||
import { AccordingProps, accordingProps } from './according.props';
|
||||
|
||||
import './according.css';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FAccording',
|
||||
props: accordingProps,
|
||||
emits: [],
|
||||
setup(props: AccordingProps, context: SetupContext) {
|
||||
const accordingStyle = computed(() => ({
|
||||
height: props.height ? `${props.height}px` : '',
|
||||
width: props.width ? `${props.width}px` : '',
|
||||
}));
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class={`farris-panel accoriding ${props.customClass.join(' ')}`} style={accordingStyle.value}>
|
||||
{context.slots.default && context.slots.default()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
/* 很重要 */
|
||||
.farris-panel {
|
||||
border: 1px solid rgba(0, 0, 0, .125)
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { ExtractPropTypes } from 'vue';
|
||||
|
||||
export const accordingProps = {
|
||||
customClass: { type: Array<string>, default: [] },
|
||||
height: { type: Number },
|
||||
width: { type: Number },
|
||||
enableFold: { type: Boolean, default: true },
|
||||
expanded: { type: Boolean, default: false },
|
||||
};
|
||||
export type AccordingProps = ExtractPropTypes<typeof accordingProps>;
|
|
@ -0,0 +1,58 @@
|
|||
import { computed, defineComponent, ref, SetupContext } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FAccordingItem',
|
||||
props: {},
|
||||
emits: [],
|
||||
setup(props, context: SetupContext) {
|
||||
const title = ref('');
|
||||
const isActive = ref(false);
|
||||
const isDisabled = ref(false);
|
||||
|
||||
function selectAccordingItem() {}
|
||||
|
||||
function onClick($event: Event) {
|
||||
selectAccordingItem();
|
||||
}
|
||||
|
||||
const accordingItemClass = computed(() => ({
|
||||
'f-state-disable': isDisabled.value,
|
||||
card: true,
|
||||
'farris-panel-item': true,
|
||||
'f-state-selected': isActive.value,
|
||||
}));
|
||||
|
||||
const shouldShowHeader = computed(() => {
|
||||
return true;
|
||||
});
|
||||
|
||||
const shouldShowCustomHeader = computed(() => {
|
||||
return false;
|
||||
});
|
||||
|
||||
const headIconClass = computed(() => ({
|
||||
'f-icon': true,
|
||||
'f-according-collapse': !isActive.value,
|
||||
'f-according-expand': isActive.value,
|
||||
}));
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class={accordingItemClass.value}>
|
||||
<div class="card-header" onClick={onClick}>
|
||||
<div class="panel-item-title">
|
||||
{shouldShowHeader.value && <span>{title.value}</span>}
|
||||
{shouldShowCustomHeader.value && context.slots.head && context.slots.head()}
|
||||
<span class={headIconClass.value}></span>
|
||||
</div>
|
||||
<div class="panel-item-tool">{context.slots.toolbar && context.slots.toolbar()}</div>
|
||||
<div class="panel-item-clear"></div>
|
||||
</div>
|
||||
<div dropAnimation="active?'active':'inactive'">
|
||||
<div class="card-body">{context.slots.content && context.slots.content()}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
import { ExtractPropTypes } from 'vue';
|
||||
|
||||
export const accordingItemProps = {
|
||||
width:{type:Number},
|
||||
height:{type:Number},
|
||||
title:{type:String,default:''},
|
||||
disable:{type:Boolean,default:false}
|
||||
};
|
||||
export type AccordingItemProps = ExtractPropTypes<typeof accordingItemProps>;
|
||||
|
|
@ -0,0 +1,308 @@
|
|||
import { Component, OnInit, ViewChildren, ElementRef, Input, Output, EventEmitter, HostListener, ViewChild, forwardRef } from '@angular/core';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { NotifyService } from '@farris/ui-notify';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { LocaleService } from '@farris/ui-locale';
|
||||
|
||||
export interface upImageFile {
|
||||
size: number;
|
||||
name: string;
|
||||
type: string;
|
||||
lastModified?: string;
|
||||
lastModifiedDate?: Date;
|
||||
state?: string;
|
||||
base64?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'farris-avatar',
|
||||
templateUrl: './avatar.component.html',
|
||||
styleUrls: ['./avatar.component.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => AvatarComponent),
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class AvatarComponent implements ControlValueAccessor, OnInit {
|
||||
private defaultImgSrc = 'data:image/jpeg;base64,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'
|
||||
private errorImgSrc = 'data:image/jpeg;base64,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';
|
||||
public imgSrc;
|
||||
// public fileBinary: string;
|
||||
@ViewChild('file') file: ElementRef;
|
||||
// @Input() cover = '';
|
||||
tReadOnly: boolean = false;
|
||||
@Input()
|
||||
set readonly(value: boolean) {
|
||||
if (value !== this.tReadOnly) {
|
||||
let localeTitle = this.localeService.getValue('avatar.imgtitle');
|
||||
this.imgtitle = value ? '' : (this.imgTitle ? this.imgTitle : localeTitle);
|
||||
this.tReadOnly = value;
|
||||
}
|
||||
|
||||
};
|
||||
get readonly(): boolean {
|
||||
return this.tReadOnly;
|
||||
}
|
||||
// @Input() type;
|
||||
@Input() size: number = 1;
|
||||
@Input() imgTitle: string;
|
||||
@Input() avatarWidth: number = 100;
|
||||
@Input() avatarHeight: number = 100;
|
||||
@Input() imgShape: string = 'circle';
|
||||
imgtitle: string = '点击修改';
|
||||
// @Input() isBase64:boolean = true;
|
||||
@Output('imgChange') imgChange = new EventEmitter();
|
||||
|
||||
_type;
|
||||
@Input()
|
||||
set type(val) {
|
||||
if (val && val.length) {
|
||||
let types = val;
|
||||
if (typeof val === 'string') {
|
||||
types = val.split(',');
|
||||
}
|
||||
if (types && types.length) {
|
||||
this.currentImgType = [];
|
||||
types.forEach(t => {
|
||||
if ((typeof t == 'string') && t.constructor == String) {
|
||||
let tImgtype = 'image/' + t;
|
||||
if (t === 'jpg') {
|
||||
let jpgType = 'image/jpeg';
|
||||
this.currentImgType.push(jpgType);
|
||||
}
|
||||
this.currentImgType.push(tImgtype);
|
||||
}
|
||||
});
|
||||
if (this.currentImgType.length > 0) {
|
||||
this.imgType = this.currentImgType.join(',')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
get type() {
|
||||
return this._type;
|
||||
}
|
||||
_cover;
|
||||
@Input()
|
||||
set cover(val) {
|
||||
if (val) {
|
||||
this._cover = val;
|
||||
this.imgsrcInit(this.cover);
|
||||
}
|
||||
else {
|
||||
this.imgSrc = this.defaultImgSrc;
|
||||
}
|
||||
}
|
||||
get cover() {
|
||||
return this._cover;
|
||||
}
|
||||
|
||||
private onChangeCallback: Function = () => { }
|
||||
private onTouchedCallback: Function = () => { }
|
||||
//是否加载中
|
||||
loadingImg: boolean;
|
||||
imgType = 'image/*';
|
||||
imgFileObj: upImageFile;
|
||||
currentImgType = ['image/image', 'image/webp', 'image/png', 'image/svg', 'image/gif', 'image/jpg', 'image/jpeg', 'image/bmp'];
|
||||
constructor(private notifyService: NotifyService, public localeService: LocaleService) {
|
||||
this.notifyService.config.position = 'top-center';
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// if(this.cover){
|
||||
// this.imgsrcInit(this.cover);
|
||||
// }
|
||||
// else{
|
||||
// this.imgSrc = this.defaultImgSrc;
|
||||
// }
|
||||
if (this.readonly) {
|
||||
this.imgtitle = '';
|
||||
}
|
||||
else if (this.imgTitle) {
|
||||
this.imgtitle = this.imgTitle;
|
||||
}
|
||||
else {
|
||||
this.imgtitle = this.localeService.getValue('avatar.imgtitle');
|
||||
}
|
||||
}
|
||||
/*判断cover数据格式,做出修改赋值给imgSrc
|
||||
*/
|
||||
imgsrcInit(val) {
|
||||
let isImg = this.isSrc(val);
|
||||
if (isImg) {
|
||||
this.imgSrc = val;
|
||||
}
|
||||
else {
|
||||
let isFullBase64 = this.isBaseSrc(val);
|
||||
if (isFullBase64) {
|
||||
this.imgSrc = val
|
||||
}
|
||||
else {
|
||||
this.imgSrc = this.addBase64(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('click')
|
||||
onClick(): void {
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
(this.file.nativeElement as HTMLInputElement).click();
|
||||
}
|
||||
|
||||
getfiledata(event) {
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
const filetarget = event.target as HTMLInputElement;
|
||||
let getfile = filetarget.files;
|
||||
if (!getfile[0]) {
|
||||
return;
|
||||
}
|
||||
let fileType = getfile[0].type;
|
||||
const isLtSize = getfile[0].size / 1024 / 1024 < this.size;
|
||||
if (this.currentImgType.indexOf(fileType) < 0) {
|
||||
let typeerrorText = this.localeService.getValue('avatar.typeError');
|
||||
this.notifyService.error({
|
||||
type: 'error',
|
||||
title: '',
|
||||
msg: typeerrorText
|
||||
});
|
||||
filetarget.value = '';
|
||||
// this.notifyService.error('上传图片类型不正确');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLtSize) {
|
||||
let sizeerrorText = this.localeService.getValue('avatar.sizeError');
|
||||
let errormes: string = sizeerrorText + this.size + "M!";
|
||||
this.notifyService.error({
|
||||
type: 'error',
|
||||
title: '',
|
||||
msg: errormes
|
||||
});
|
||||
filetarget.value = '';
|
||||
// this.notifyService.error(`上传图片不能大于${this.size}M!`);
|
||||
return;
|
||||
}
|
||||
this.transformFile(getfile[0]);
|
||||
filetarget.value = '';
|
||||
}
|
||||
|
||||
public getImgFileObj() {
|
||||
return this.imgFileObj;
|
||||
}
|
||||
|
||||
transformFile(getfile: any) {
|
||||
// const subject = new Subject();
|
||||
this.imgFileObj = {
|
||||
size: getfile.size,
|
||||
name: getfile.name,
|
||||
type: getfile.type,
|
||||
lastModified: getfile.lastModified,
|
||||
lastModifiedDate: getfile.lastModifiedDate
|
||||
}
|
||||
this.do(getfile).subscribe(res => {
|
||||
this.loadingImg = false;
|
||||
if (res['state'] === 'done') {
|
||||
this.imgSrc = res['result'];
|
||||
|
||||
//this.onChangeCallback(this.imgSrc);
|
||||
this.onChangeCallback(this.removeBase64(this.imgSrc));
|
||||
this.onTouchedCallback();
|
||||
}
|
||||
else if (res['state'] === 'error') {
|
||||
let uploaderrorText = this.localeService.getValue('avatar.uploadError');
|
||||
this.notifyService.error({
|
||||
type: 'error',
|
||||
title: '',
|
||||
msg: uploaderrorText
|
||||
})
|
||||
// this.notifyService.error('图片上传失败,请重试!');
|
||||
}
|
||||
this.imgFileObj.state = res['state'];
|
||||
this.imgFileObj.base64 = res['result'];
|
||||
this.imgChange.emit(this.imgFileObj);
|
||||
});
|
||||
}
|
||||
|
||||
read(file: File): Observable<string> {
|
||||
return Observable.create(observer => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
//this.loadingImg = true;
|
||||
// reader.onloadstart=function(){}
|
||||
reader.onload = () => {
|
||||
//console.log(reader.result);
|
||||
observer.next({ state: 'done', 'result': reader.result });
|
||||
observer.complete();
|
||||
};
|
||||
reader.onerror = function () {
|
||||
observer.next({ state: 'error', 'result': reader.result });
|
||||
observer.complete();
|
||||
}
|
||||
});
|
||||
}
|
||||
do(file: any): Observable<string> {
|
||||
return this.read(file as File);
|
||||
}
|
||||
|
||||
writeValue(val: any): void {
|
||||
if (val && val.length) {
|
||||
// if(this.isBase64){
|
||||
// this.imgSrc = this.addBase64(val);
|
||||
// }
|
||||
// else{
|
||||
// this.imgSrc = val;
|
||||
// }
|
||||
this.imgsrcInit(val);
|
||||
}
|
||||
else if (this.cover) {
|
||||
this.imgsrcInit(this.cover);
|
||||
}
|
||||
else {
|
||||
this.imgSrc = this.defaultImgSrc;
|
||||
}
|
||||
}
|
||||
registerOnChange(fn: any): void {
|
||||
this.onChangeCallback = fn;
|
||||
}
|
||||
registerOnTouched(fn: any): void {
|
||||
this.onTouchedCallback = fn;
|
||||
}
|
||||
// setDisabledState?(isDisabled: boolean): void {
|
||||
// this.disabled = isDisabled;
|
||||
// }
|
||||
|
||||
addBase64(val) {
|
||||
if (!val) return;
|
||||
return 'data:image/jpeg;base64,' + val;
|
||||
}
|
||||
|
||||
removeBase64(val) {
|
||||
if (!val) return;
|
||||
let img_arr = val.split(',');
|
||||
if (img_arr.length) {
|
||||
return img_arr[1];
|
||||
}
|
||||
|
||||
}
|
||||
//判断是否是图片路径
|
||||
isSrc(url) {
|
||||
return (url.match(/\.(jpeg|jpg|gif|png|svg|bmp|webp)$/) != null)
|
||||
}
|
||||
//判断是否是完成base64
|
||||
isBaseSrc(url) {
|
||||
return (url.indexOf('data:image/') > -1 ? true : false)
|
||||
}
|
||||
|
||||
errorSrc() {
|
||||
this.imgSrc = this.errorImgSrc;
|
||||
this.imgtitle = this.localeService.getValue('avatar.loadError');
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
import { defineComponent, computed, ref, SetupContext } from 'vue';
|
||||
import { avatarProps, AvatarProps } from './avatar.props';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Avatar',
|
||||
props: avatarProps,
|
||||
emits: ['change'],
|
||||
setup(props: AvatarProps, context: SetupContext) {
|
||||
const avatarClass = computed(() => ({
|
||||
'f-avatar': true,
|
||||
'f-avatar-readonly': props.readonly,
|
||||
'f-avatar-circle': props.shape === 'circle',
|
||||
'f-avatar-square': props.shape === 'square',
|
||||
}));
|
||||
|
||||
const avatarStyle = computed(() => ({
|
||||
width: props.avatarWidth + 'px',
|
||||
height: props.avatarHeight + 'px',
|
||||
}));
|
||||
|
||||
let showLoading = false;
|
||||
let imgSrc = '';
|
||||
|
||||
const currentImgType = ['image/image', 'image/webp', 'image/png', 'image/svg', 'image/gif', 'image/jpg', 'image/jpeg', 'image/bmp'];
|
||||
|
||||
function errorSrc() {
|
||||
return '';
|
||||
}
|
||||
|
||||
function getfiledata() {}
|
||||
|
||||
const defaultImgSrc =
|
||||
'data:image/jpeg;base64,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';
|
||||
const errorImgSrc =
|
||||
'data:image/jpeg;base64,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';
|
||||
|
||||
const imageType = computed(() => props.type.join());
|
||||
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class={avatarClass.value} style={avatarStyle.value}>
|
||||
{showLoading && (
|
||||
<div class="f-avatar-upload-loading">
|
||||
<div class="loading-inner">加载中</div>
|
||||
</div>
|
||||
)}
|
||||
<img title={props.tile} class="f-avatar-image" src={imgSrc} onError={errorSrc()} />
|
||||
{!props.readonly && (
|
||||
<div class="f-avatar-icon">
|
||||
<span class="f-icon f-icon-camera"></span>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
name="file-input"
|
||||
type="file"
|
||||
class="f-avatar-upload"
|
||||
accept={imageType.value}
|
||||
onChange={getfiledata}
|
||||
style="display: none;"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
import { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
type AvatarShap = 'square' | 'circle';
|
||||
|
||||
export const avatarProps = {
|
||||
/**
|
||||
* 头像宽度
|
||||
*/
|
||||
avatarWidth: { type: Number, default: 100 },
|
||||
/**
|
||||
* 头像高度
|
||||
*/
|
||||
avatarHeight: { type: Number, default: 100 },
|
||||
/**
|
||||
* 组件标识
|
||||
*/
|
||||
cover: { type: String },
|
||||
/**
|
||||
* 只读
|
||||
*/
|
||||
readonly: { type: Boolean, default: false },
|
||||
/**
|
||||
* 头像形状
|
||||
*/
|
||||
shape: { type: String as PropType<AvatarShap>, default: 'circle' },
|
||||
/**
|
||||
* 头像最大尺寸, 单位MB
|
||||
*/
|
||||
maxSize: { type: Number, default: 1 },
|
||||
/**
|
||||
* 头像标题
|
||||
*/
|
||||
tile: { type: String, default: '' },
|
||||
/**
|
||||
* 支持的头像类型
|
||||
*/
|
||||
type: { type: Array<string>, default: [] },
|
||||
};
|
||||
|
||||
export type AvatarProps = ExtractPropTypes<typeof avatarProps>;
|
|
@ -0,0 +1,19 @@
|
|||
import { ComputedRef } from 'vue';
|
||||
|
||||
export interface UseImage {
|
||||
acceptTypes: ComputedRef<string>;
|
||||
|
||||
imageSrc: ComputedRef<string>;
|
||||
|
||||
imageTitle: ComputedRef<string>;
|
||||
}
|
||||
|
||||
export interface ImageFile {
|
||||
size: number;
|
||||
name: string;
|
||||
type: string;
|
||||
lastModified?: string;
|
||||
lastModifiedDate?: Date;
|
||||
state?: string;
|
||||
base64?: string;
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
import { stringLiteral } from '@babel/types';
|
||||
import { SetupContext } from 'vue';
|
||||
import { resourceLimits } from 'worker_threads';
|
||||
import { AvatarProps } from '../avatar.props';
|
||||
import { ImageFile } from './types';
|
||||
|
||||
export function useFile(props: AvatarProps, context: SetupContext, allowTypes: string[]) {
|
||||
function getFileData($event: Event) {
|
||||
if (props.readonly) {
|
||||
return;
|
||||
}
|
||||
const fileInput = $event.target as HTMLInputElement;
|
||||
const selectedFiles = fileInput.files;
|
||||
if (!selectedFiles || !selectedFiles[0]) {
|
||||
return;
|
||||
}
|
||||
const fileType = selectedFiles[0].type;
|
||||
const isLtSize = selectedFiles[0].size / 1024 / 1024 < props.maxSize;
|
||||
if (allowTypes.indexOf(fileType) < 0) {
|
||||
const typeErrorMessage = this.localeService.getValue('avatar.typeError');
|
||||
this.notifyService.error({
|
||||
type: 'error',
|
||||
title: '',
|
||||
msg: typeErrorMessage,
|
||||
});
|
||||
fileInput.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLtSize) {
|
||||
const sizeErrorMessageTemplate = this.localeService.getValue('avatar.sizeError');
|
||||
const sizeErrorMessage: string = sizeErrorMessageTemplate + props.maxSize + 'MB!';
|
||||
this.notifyService.error({
|
||||
type: 'error',
|
||||
title: '',
|
||||
msg: sizeErrorMessage,
|
||||
});
|
||||
fileInput.value = '';
|
||||
return;
|
||||
}
|
||||
this.transformFile(selectedFiles[0]);
|
||||
fileInput.value = '';
|
||||
}
|
||||
|
||||
function readSourceFile(sourceFile: File): Promise<{ state: string; content: string | ArrayBuffer | null }> {
|
||||
const result = new Promise<{ state: string; content: string | ArrayBuffer | null }>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(sourceFile);
|
||||
reader.onload = () => {
|
||||
resolve({ state: 'done', content: reader.result });
|
||||
};
|
||||
reader.onerror = function () {
|
||||
reject({ state: 'error', content: reader.result });
|
||||
};
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function updateImageSrc(imageSrc: string) {}
|
||||
|
||||
function removeBase64(imageSrc: string) {}
|
||||
|
||||
function transformFile(sourceFile: File) {
|
||||
// const subject = new Subject();
|
||||
const imageFile: ImageFile = {
|
||||
size: sourceFile.size,
|
||||
name: sourceFile.name,
|
||||
type: sourceFile.type,
|
||||
lastModified: String(sourceFile.lastModified),
|
||||
};
|
||||
readSourceFile(sourceFile)
|
||||
.then((result) => {
|
||||
if (result.state === 'done') {
|
||||
updateImageSrc(result.content as string);
|
||||
const imageFileContent = removeBase64(imageSrc);
|
||||
context.emit('change', imageFileContent);
|
||||
context.emit('touched');
|
||||
imageFile.state = result.state;
|
||||
imageFile.base64 = result.content as string;
|
||||
context.emit('imageChange', imageFile);
|
||||
}
|
||||
})
|
||||
.catch((reason) => {
|
||||
const uploadErrorText = this.localeService.getValue('avatar.uploadError');
|
||||
this.notifyService.error({
|
||||
type: 'error',
|
||||
title: '',
|
||||
msg: uploadErrorText,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
import { computed, SetupContext } from 'vue';
|
||||
import { AvatarProps } from '../avatar.props';
|
||||
import { UseImage } from './types';
|
||||
|
||||
export function useImage(props: AvatarProps, context: SetupContext, fileInput: HTMLInputElement): UseImage {
|
||||
const defaultImage = '';
|
||||
const errorImage = '';
|
||||
|
||||
// 判断是否是图片路径
|
||||
function isUrl(url: string) {
|
||||
return url.match(/\.(jpeg|jpg|gif|png|svg|bmp|webp)$/) != null;
|
||||
}
|
||||
|
||||
// 判断是否是完成base64
|
||||
function isBase64Image(url: string) {
|
||||
return url.indexOf('data:image/') > -1;
|
||||
}
|
||||
|
||||
function appendBase64ImageHeader(val) {
|
||||
if (!val) {
|
||||
return '';
|
||||
}
|
||||
return 'data:image/jpeg;base64,' + val;
|
||||
}
|
||||
|
||||
const acceptTypes = computed(() => {
|
||||
if (!props.type || !props.type.length) {
|
||||
return '';
|
||||
}
|
||||
const imageTypesArray = props.type.map((fileType: string) => {
|
||||
if (fileType === 'jpg') {
|
||||
fileType = 'jpeg';
|
||||
}
|
||||
return `image/${fileType}`;
|
||||
});
|
||||
if (!imageTypesArray || !imageTypesArray.length) {
|
||||
return 'image/*';
|
||||
}
|
||||
return imageTypesArray.join(',');
|
||||
});
|
||||
|
||||
const imageSrc = computed(() => {
|
||||
if (!props.cover) {
|
||||
return defaultImage;
|
||||
}
|
||||
if (isUrl(props.cover)) {
|
||||
return props.cover;
|
||||
}
|
||||
if (isBase64Image(props.cover)) {
|
||||
return props.cover;
|
||||
}
|
||||
return appendBase64ImageHeader(props.cover);
|
||||
});
|
||||
|
||||
const imageTitle = computed(() => {
|
||||
return props.readonly ? '' : props.tile;
|
||||
});
|
||||
|
||||
function onClickImage() {
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
fileInput.click();
|
||||
}
|
||||
|
||||
function getImageFile() {
|
||||
return this.imgFileObj;
|
||||
}
|
||||
|
||||
return { acceptTypes, imageSrc, imageTitle };
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import type { App } from 'vue';
|
||||
import ButtonEdit from './src/button-edit.component';
|
||||
|
||||
export * from './src/button-edit.props';
|
||||
|
||||
export { ButtonEdit };
|
||||
|
||||
export default {
|
||||
install(app: App): void {
|
||||
app.component(ButtonEdit.name, ButtonEdit);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,112 @@
|
|||
import { defineComponent, computed, ref } from 'vue';
|
||||
import type { SetupContext } from 'vue';
|
||||
import { buttonEditProps, ButtonEditProps } from './button-edit.props';
|
||||
import { useButton } from './composition/use-button';
|
||||
import { useClear } from './composition/use-clear';
|
||||
import { useTextBox } from './composition/use-text-box';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FButtonEdit',
|
||||
props: buttonEditProps,
|
||||
emits: [
|
||||
'updateExtendInfo',
|
||||
'clear',
|
||||
'change',
|
||||
'click',
|
||||
'clickButton',
|
||||
'blur',
|
||||
'focus',
|
||||
'mouseEnterIcon',
|
||||
'mouseLeaveIcon',
|
||||
'keyup',
|
||||
'keydown',
|
||||
'inputClick',
|
||||
'input',
|
||||
'update:modelValue',
|
||||
],
|
||||
setup(props: ButtonEditProps, context: SetupContext) {
|
||||
const modelValue = ref(props.modelValue);
|
||||
const { buttonClass, onClickButton, onMouseEnterButton, onMouseLeaveButton } = useButton(props, context);
|
||||
const displayText = ref('');
|
||||
const {
|
||||
hasFocusedTextBox,
|
||||
isTextBoxReadonly,
|
||||
textBoxClass,
|
||||
textBoxPlaceholder,
|
||||
textBoxTitle,
|
||||
onBlurTextBox,
|
||||
onClickTextBox,
|
||||
onFocusTextBox,
|
||||
onInput,
|
||||
onKeyDownTextBox,
|
||||
onKeyUpTextBox,
|
||||
onMouseDownTextBox,
|
||||
onTextBoxValueChange,
|
||||
} = useTextBox(props, context, modelValue, displayText);
|
||||
|
||||
const { enableClearButton, showClearButton, onClearValue, onMouseEnterTextBox, onMouseLeaveTextBox } = useClear(
|
||||
props,
|
||||
context,
|
||||
modelValue,
|
||||
hasFocusedTextBox,
|
||||
displayText
|
||||
);
|
||||
|
||||
const inputGroupClass = computed(() => ({
|
||||
'input-group': true,
|
||||
'f-state-disable': props.disable,
|
||||
'f-state-editable': props.editable && !props.disable && !props.readonly,
|
||||
'f-state-readonly': props.readonly && !props.disable,
|
||||
'f-state-focus': hasFocusedTextBox,
|
||||
}));
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class="f-cmp-inputgroup" id={props.id}>
|
||||
<div
|
||||
class={[props.customClass, inputGroupClass.value]}
|
||||
onMouseenter={onMouseEnterTextBox}
|
||||
onMouseleave={onMouseLeaveTextBox}>
|
||||
<input
|
||||
name="input-group-value"
|
||||
autocomplete={'' + props.autoComplete}
|
||||
class={textBoxClass.value}
|
||||
disabled={props.disable}
|
||||
maxlength={props.maxLength}
|
||||
minlength={props.minLength}
|
||||
placeholder={textBoxPlaceholder.value}
|
||||
readonly={isTextBoxReadonly.value}
|
||||
tabindex={props.tabIndex}
|
||||
title={textBoxTitle.value}
|
||||
type={props.inputType}
|
||||
value={modelValue.value}
|
||||
onBlur={onBlurTextBox}
|
||||
onChange={onTextBoxValueChange}
|
||||
onClick={onClickTextBox}
|
||||
onFocus={onFocusTextBox}
|
||||
onInput={onInput}
|
||||
onKeydown={onKeyDownTextBox}
|
||||
onKeyup={onKeyUpTextBox}
|
||||
onMousedown={onMouseDownTextBox}
|
||||
/>
|
||||
<div class={buttonClass.value}>
|
||||
{enableClearButton.value && (
|
||||
<span class="input-group-text input-group-clear" v-show={showClearButton.value} onClick={onClearValue}>
|
||||
<i class="f-icon modal_close"></i>
|
||||
</span>
|
||||
)}
|
||||
{props.buttonContent && (
|
||||
<span
|
||||
class="input-group-text input-group-append-button"
|
||||
onClick={onClickButton}
|
||||
onMouseenter={onMouseEnterButton}
|
||||
onMouseleave={onMouseLeaveButton}
|
||||
v-html={props.buttonContent}></span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,81 @@
|
|||
import { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
type TextAlignment = 'left' | 'center' | 'right';
|
||||
|
||||
export const buttonEditProps = {
|
||||
/**
|
||||
* 组件标识
|
||||
*/
|
||||
id: String,
|
||||
/**
|
||||
* 扩展按钮显示内容,这是一段现在扩展按钮中的html标签
|
||||
*/
|
||||
buttonContent: { type: String, default: '<i class="f-icon f-icon-lookup"></i>' },
|
||||
/**
|
||||
* 启用输入框自动完成功能
|
||||
*/
|
||||
autoComplete: { type: Boolean, default: false },
|
||||
/**
|
||||
* 组件自定义样式
|
||||
*/
|
||||
customClass: { type: String, default: '' },
|
||||
/**
|
||||
* 禁用组件,既不允许在输入框中录入,也不允许点击扩展按钮。
|
||||
*/
|
||||
disable: { type: Boolean, default: false },
|
||||
/**
|
||||
* 允许在输入框中录入文本。
|
||||
*/
|
||||
editable: { type: Boolean, default: true },
|
||||
/**
|
||||
* 显示清空文本按钮
|
||||
*/
|
||||
enableClear: { type: Boolean, default: false },
|
||||
/**
|
||||
* 组件值
|
||||
*/
|
||||
modelValue: { type: String, default: '' },
|
||||
/**
|
||||
* 将组件设置为只读,既不允许在输入框中录入,也不允许点击扩展按钮,但是允许复制输入框中的内容。
|
||||
*/
|
||||
readonly: { type: Boolean, default: false },
|
||||
/**
|
||||
* 文本对齐方式
|
||||
*/
|
||||
textAlign: { type: String as PropType<TextAlignment>, default: 'left' },
|
||||
/**
|
||||
* 禁用组件时,是否显示扩展按钮
|
||||
*/
|
||||
showButtonWhenDisabled: { type: Boolean, default: false },
|
||||
|
||||
/**
|
||||
* 显示输入框的标签
|
||||
*/
|
||||
enableTitle: { type: Boolean, default: false },
|
||||
/**
|
||||
* 输入框类型
|
||||
*/
|
||||
inputType: { type: String, default: 'text' },
|
||||
/**
|
||||
* 显示输入框提示信息
|
||||
*/
|
||||
forcePlaceholder: { type: Boolean, default: false },
|
||||
/**
|
||||
* 输入框提示文本
|
||||
*/
|
||||
placeholder: { type: String, default: '' },
|
||||
/**
|
||||
* 输入框最小长度
|
||||
*/
|
||||
minLength: Number,
|
||||
/**
|
||||
* 输入框最大长度
|
||||
*/
|
||||
maxLength: Number,
|
||||
/**
|
||||
* 输入框Tab键索引
|
||||
*/
|
||||
tabIndex: Number,
|
||||
};
|
||||
|
||||
export type ButtonEditProps = ExtractPropTypes<typeof buttonEditProps>;
|
|
@ -0,0 +1,106 @@
|
|||
import { ComputedRef, Ref } from 'vue';
|
||||
|
||||
export interface UseButton {
|
||||
/**
|
||||
* 附加按钮的Class
|
||||
*/
|
||||
buttonClass: ComputedRef<Record<string, boolean | undefined>>;
|
||||
/**
|
||||
* 点击附加按钮事件响应函数
|
||||
*/
|
||||
onClickButton: ($event: Event) => void;
|
||||
/**
|
||||
* 鼠标移入附加按钮事件响应函数
|
||||
*/
|
||||
onMouseEnterButton: ($event: MouseEvent) => void;
|
||||
/**
|
||||
* 鼠标移出附加按钮事件响应函数
|
||||
*/
|
||||
onMouseLeaveButton: ($event: MouseEvent) => void;
|
||||
/**
|
||||
* 鼠标滑过附加按钮事件响应函数
|
||||
*/
|
||||
onMouseOverButton: () => void;
|
||||
}
|
||||
|
||||
export interface UseClear {
|
||||
/**
|
||||
* 启用清空按钮
|
||||
*/
|
||||
enableClearButton: ComputedRef<boolean>;
|
||||
/**
|
||||
* 显示清空按钮
|
||||
*/
|
||||
showClearButton: Ref<boolean>;
|
||||
/**
|
||||
* 清空输入框值事件响应函数
|
||||
*/
|
||||
onClearValue: ($event: Event) => void;
|
||||
/**
|
||||
* 鼠标进入输入框事件响应函数
|
||||
*/
|
||||
onMouseEnterTextBox: ($event: MouseEvent) => void;
|
||||
/**
|
||||
* 鼠标移出输入框事件响应函数
|
||||
*/
|
||||
onMouseLeaveTextBox: ($event: MouseEvent) => void;
|
||||
}
|
||||
|
||||
export interface UseTextBox {
|
||||
/**
|
||||
* 输入框是否获得焦点
|
||||
*/
|
||||
hasFocusedTextBox: ComputedRef<boolean>;
|
||||
/**
|
||||
* 输入框是否处于只读状态
|
||||
*/
|
||||
isTextBoxReadonly: ComputedRef<boolean>;
|
||||
/**
|
||||
* 输入框Class
|
||||
*/
|
||||
textBoxClass: ComputedRef<Record<string, boolean | undefined>>;
|
||||
/**
|
||||
* 输入框提示语
|
||||
*/
|
||||
textBoxPlaceholder: ComputedRef<string>;
|
||||
/**
|
||||
* 输入框提示标签
|
||||
*/
|
||||
textBoxTitle: ComputedRef<string>;
|
||||
/**
|
||||
* 更新输入框的值并触发change事件
|
||||
*/
|
||||
changeTextBoxValue: (newValue: string, showEmitChangeEmit: boolean) => void;
|
||||
/**
|
||||
* 输入框失去焦点事件响应函数
|
||||
*/
|
||||
onBlurTextBox: ($event: Event) => void;
|
||||
/**
|
||||
* 鼠标点击输入框事件响应函数
|
||||
*/
|
||||
onClickTextBox: ($event: Event) => void;
|
||||
/**
|
||||
* 输入框获得焦点事件响应函数
|
||||
*/
|
||||
onFocusTextBox: ($event: Event) => void;
|
||||
/**
|
||||
* 输入框值变化事件
|
||||
*/
|
||||
onInput: ($event: Event) => void;
|
||||
/**
|
||||
* 鼠标点击输入框事件响应函数
|
||||
*/
|
||||
onMouseDownTextBox: ($event: MouseEvent) => void;
|
||||
/**
|
||||
* 键盘在输入框按下事件
|
||||
*/
|
||||
onKeyDownTextBox: ($event: Event) => void;
|
||||
/**
|
||||
* 键盘在输入框抬起事件
|
||||
*/
|
||||
onKeyUpTextBox: ($event: Event) => void;
|
||||
/**
|
||||
* 输入框值变化事件响应函数
|
||||
*/
|
||||
onTextBoxValueChange: ($event: Event) => void;
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import { UseButton } from './types';
|
||||
import { computed, SetupContext } from 'vue';
|
||||
import { ButtonEditProps } from '../button-edit.props';
|
||||
|
||||
export function useButton(props: ButtonEditProps, context: SetupContext): UseButton {
|
||||
const buttonClass = computed(() => ({
|
||||
'input-group-append': true,
|
||||
'append-force-show': props.showButtonWhenDisabled && (props.readonly || props.disable),
|
||||
}));
|
||||
|
||||
const canClickAppendButton = computed(() => props.showButtonWhenDisabled || ((!props.editable || !props.readonly) && !props.disable));
|
||||
|
||||
function onClickButton($event: Event) {
|
||||
if (canClickAppendButton.value) {
|
||||
context.emit('clickButton', { origin: $event, value: props.modelValue });
|
||||
}
|
||||
$event.stopPropagation();
|
||||
}
|
||||
|
||||
function onMouseEnterButton($event: MouseEvent) {
|
||||
context.emit('mouseEnterIcon', $event);
|
||||
}
|
||||
|
||||
function onMouseLeaveButton($event: MouseEvent) {
|
||||
context.emit('mouseLeaveIcon', $event);
|
||||
}
|
||||
|
||||
function onMouseOverButton() {
|
||||
context.emit('mouseOverButton');
|
||||
}
|
||||
|
||||
return {
|
||||
buttonClass,
|
||||
onClickButton,
|
||||
onMouseEnterButton,
|
||||
onMouseLeaveButton,
|
||||
onMouseOverButton,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import { computed, ComputedRef, Ref, ref, SetupContext, watch } from 'vue';
|
||||
import { ButtonEditProps } from '../button-edit.props';
|
||||
import { UseClear } from './types';
|
||||
import { useTextBox } from './use-text-box';
|
||||
|
||||
export function useClear(
|
||||
props: ButtonEditProps,
|
||||
context: SetupContext,
|
||||
modelValue: Ref<string>,
|
||||
hasFocusedTextBox: ComputedRef<boolean>,
|
||||
displayText: Ref<string>
|
||||
): UseClear {
|
||||
const showClearButton = ref(false);
|
||||
const enableClearButton = computed(() => props.enableClear && !props.readonly && !props.disable);
|
||||
const { changeTextBoxValue } = useTextBox(props, context, modelValue, displayText);
|
||||
|
||||
function toggleClearIcon(isShow: boolean) {
|
||||
showClearButton.value = isShow;
|
||||
}
|
||||
|
||||
watch(displayText, () => {
|
||||
if (hasFocusedTextBox.value) {
|
||||
toggleClearIcon(!!displayText.value);
|
||||
} else {
|
||||
toggleClearIcon(false);
|
||||
}
|
||||
});
|
||||
|
||||
function onClearValue($event: Event) {
|
||||
const flag1 = !props.readonly && !props.disable && props.editable;
|
||||
const flag2 = !props.editable;
|
||||
$event.stopPropagation();
|
||||
if (flag1 || flag2) {
|
||||
changeTextBoxValue('', false);
|
||||
toggleClearIcon(!showClearButton.value);
|
||||
context.emit('clear');
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseEnterTextBox($event: Event) {
|
||||
if (!enableClearButton.value) {
|
||||
return;
|
||||
}
|
||||
if (!modelValue.value) {
|
||||
toggleClearIcon(false);
|
||||
return;
|
||||
}
|
||||
if ((!props.editable || !props.readonly) && !props.disable) {
|
||||
toggleClearIcon(true);
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseLeaveTextBox($event: Event) {
|
||||
if (!enableClearButton.value) {
|
||||
return;
|
||||
}
|
||||
toggleClearIcon(false);
|
||||
}
|
||||
|
||||
return {
|
||||
enableClearButton,
|
||||
showClearButton,
|
||||
onClearValue,
|
||||
onMouseEnterTextBox,
|
||||
onMouseLeaveTextBox,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
import { computed, ref, Ref, SetupContext, watch } from 'vue';
|
||||
import { ButtonEditProps } from '../button-edit.props';
|
||||
import { UseTextBox } from './types';
|
||||
|
||||
export function useTextBox(props: ButtonEditProps, context: SetupContext, modelValue: Ref<string>, displayText: Ref<string>): UseTextBox {
|
||||
const textBoxTitle = computed(() => (props.enableTitle ? modelValue.value : ''));
|
||||
|
||||
const textBoxPlaceholder = computed(() => ((props.disable || props.readonly) && !props.forcePlaceholder ? '' : props.placeholder));
|
||||
|
||||
const isTextBoxReadonly = computed(() => props.readonly || !props.editable);
|
||||
|
||||
let focusState = false;
|
||||
|
||||
const hasFocusedTextBox = computed(() => focusState);
|
||||
|
||||
const textBoxClass = computed(() => ({
|
||||
'text-left': props.textAlign === 'left',
|
||||
'text-center': props.textAlign === 'center',
|
||||
'text-right': props.textAlign === 'right',
|
||||
'form-control': true,
|
||||
'f-utils-fill': true,
|
||||
}));
|
||||
|
||||
function changeTextBoxValue(newValue: string, showEmitChangeEmit = true) {
|
||||
if (modelValue.value !== newValue) {
|
||||
modelValue.value = newValue;
|
||||
if (showEmitChangeEmit) {
|
||||
context.emit('change', newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value: string) => context.emit('change', value)
|
||||
);
|
||||
|
||||
function onBlurTextBox($event: Event) {
|
||||
focusState = false;
|
||||
context.emit('blur', $event);
|
||||
$event.stopPropagation();
|
||||
}
|
||||
|
||||
function onClickTextBox($event: Event) {
|
||||
context.emit('click', $event);
|
||||
}
|
||||
|
||||
function onFocusTextBox($event: Event) {
|
||||
if (props.disable) {
|
||||
return;
|
||||
}
|
||||
focusState = true;
|
||||
if (!isTextBoxReadonly.value) {
|
||||
context.emit('focus', $event);
|
||||
}
|
||||
}
|
||||
|
||||
function onInput($event: Event) {
|
||||
context.emit('input', ($event.target as HTMLInputElement).value);
|
||||
const newValue = ($event.target as HTMLInputElement).value;
|
||||
displayText.value = newValue;
|
||||
if (modelValue.value !== newValue) {
|
||||
changeTextBoxValue(newValue, false);
|
||||
context.emit('update:modelValue', ($event.target as HTMLInputElement).value);
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseDownTextBox($event: MouseEvent) {
|
||||
const target = $event.target as HTMLElement;
|
||||
if (target.tagName !== 'INPUT') {
|
||||
$event.preventDefault();
|
||||
}
|
||||
$event.stopPropagation();
|
||||
}
|
||||
|
||||
function onKeyDownTextBox($event: Event) {
|
||||
context.emit('keydown', $event);
|
||||
}
|
||||
|
||||
function onKeyUpTextBox($event: Event) {
|
||||
context.emit('keyup', $event);
|
||||
}
|
||||
|
||||
function onTextBoxValueChange($event: Event) {
|
||||
const newValue = ($event.target as HTMLInputElement).value;
|
||||
changeTextBoxValue(newValue);
|
||||
}
|
||||
|
||||
return {
|
||||
hasFocusedTextBox,
|
||||
isTextBoxReadonly,
|
||||
textBoxClass,
|
||||
textBoxPlaceholder,
|
||||
textBoxTitle,
|
||||
changeTextBoxValue,
|
||||
onBlurTextBox,
|
||||
onClickTextBox,
|
||||
onFocusTextBox,
|
||||
onInput,
|
||||
onKeyDownTextBox,
|
||||
onKeyUpTextBox,
|
||||
onMouseDownTextBox,
|
||||
onTextBoxValueChange,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { ref } from 'vue';
|
||||
import { ButtonEdit } from '..';
|
||||
|
||||
describe('f-button-edit', () => {
|
||||
const mocks = {};
|
||||
|
||||
beforeAll(() => {});
|
||||
|
||||
describe('properties', () => {
|
||||
it('should has default props', () => {});
|
||||
it('should has auto complete', () => {});
|
||||
it('should net has auto complete', () => {});
|
||||
it('should be disabled', () => {});
|
||||
it('should net be disabled', () => {});
|
||||
it('should be editable', () => {
|
||||
const wrapper = mount({
|
||||
setup(props, ctx) {
|
||||
return () => {
|
||||
return <ButtonEdit editable={true}></ButtonEdit>;
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(wrapper.find('div').find('div').find('input').attributes.readonly).toBeFalsy();
|
||||
});
|
||||
it('should not be editable', () => {
|
||||
const wrapper = mount({
|
||||
setup(props, ctx) {
|
||||
return () => {
|
||||
return <ButtonEdit editable={false}></ButtonEdit>;
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(wrapper.find('.f-cmp-inputgroup').exists()).toBeTruthy();
|
||||
expect(wrapper.find('div').find('div').find('input').find('[readonly]').exists).toBeTruthy();
|
||||
});
|
||||
it('should show clear button', () => {});
|
||||
it('should not show clear button', () => {});
|
||||
it('should be readonly', () => {});
|
||||
it('should not be readonly', () => {});
|
||||
it('should enable text alignment', () => {});
|
||||
it('should show append button even be disabled', () => {});
|
||||
it('should has title', () => {});
|
||||
it('should has type', () => {});
|
||||
it('should has placeholder', () => {});
|
||||
it('should has min length', () => {});
|
||||
it('should has max length', () => {});
|
||||
it('should has tab index', () => {});
|
||||
});
|
||||
|
||||
describe('render', () => {});
|
||||
|
||||
describe('methods', () => {});
|
||||
|
||||
describe('events', () => {
|
||||
it('should emit event named clear when click clear button', async () => {
|
||||
const handleClick = jest.fn();
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
return () => {
|
||||
return <ButtonEdit enableClear onClear={handleClick}></ButtonEdit>;
|
||||
};
|
||||
},
|
||||
});
|
||||
await wrapper.find('.input-group-clear').trigger('click');
|
||||
expect(handleClick).toBeCalled();
|
||||
});
|
||||
|
||||
it('should emit event named change when changed text box value', async () => {
|
||||
const handleClick = jest.fn();
|
||||
const num = ref('0');
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
return () => {
|
||||
return <ButtonEdit v-model={num.value} onChange={handleClick}></ButtonEdit>;
|
||||
};
|
||||
},
|
||||
});
|
||||
await wrapper.find('div').find('div').find('input').setValue('test');
|
||||
expect(handleClick).toBeCalled();
|
||||
});
|
||||
|
||||
it('should emit event named click whend click text box', async () => {
|
||||
const handleClick = jest.fn();
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
return () => {
|
||||
return <ButtonEdit onClick={handleClick}></ButtonEdit>;
|
||||
};
|
||||
},
|
||||
});
|
||||
await wrapper.find('div').find('div').find('input').trigger('click');
|
||||
expect(handleClick).toBeCalled();
|
||||
});
|
||||
|
||||
it('should emit event named clickButton when click append button', async () => {
|
||||
const handleClick = jest.fn();
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
return () => {
|
||||
return <ButtonEdit onClickButton={handleClick}></ButtonEdit>;
|
||||
};
|
||||
},
|
||||
});
|
||||
await wrapper.find('.input-group-append-button').trigger('click');
|
||||
expect(handleClick).toBeCalled();
|
||||
});
|
||||
|
||||
it('should emit event named blur when text box lost focus', async () => {
|
||||
const handleClick = jest.fn();
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
return () => {
|
||||
return <ButtonEdit onBlur={handleClick}></ButtonEdit>;
|
||||
};
|
||||
},
|
||||
});
|
||||
await wrapper.find('div').find('div').find('input').trigger('blur');
|
||||
expect(handleClick).toBeCalled();
|
||||
});
|
||||
|
||||
it('should emit event named focus when text box get focus', async () => {
|
||||
const handleClick = jest.fn();
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
return () => {
|
||||
return <ButtonEdit onFocus={handleClick}></ButtonEdit>;
|
||||
};
|
||||
},
|
||||
});
|
||||
await wrapper.find('div').find('div').find('input').trigger('focus');
|
||||
expect(handleClick).toBeCalled();
|
||||
});
|
||||
|
||||
it('should emit event named mouseEnterIcon when mouse move in append button', async () => {
|
||||
const handleClick = jest.fn();
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
return () => {
|
||||
return <ButtonEdit onMouseEnterIcon={handleClick}></ButtonEdit>;
|
||||
};
|
||||
},
|
||||
});
|
||||
await wrapper.find('.input-group-append-button').trigger('mouseenter');
|
||||
expect(handleClick).toBeCalled();
|
||||
});
|
||||
|
||||
it('should emit event named mouseLeaveIcon when mouse leave append button', async () => {
|
||||
const handleClick = jest.fn();
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
return () => {
|
||||
return <ButtonEdit onMouseLeaveIcon={handleClick}></ButtonEdit>;
|
||||
};
|
||||
},
|
||||
});
|
||||
await wrapper.find('.input-group-append-button').trigger('mouseleave');
|
||||
expect(handleClick).toBeCalled();
|
||||
});
|
||||
|
||||
it('should emit event named keyup when input text in text box', async () => {
|
||||
const handleClick = jest.fn();
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
return () => {
|
||||
return <ButtonEdit onKeyup={handleClick}></ButtonEdit>;
|
||||
};
|
||||
},
|
||||
});
|
||||
await wrapper.find('div').find('div').find('input').trigger('keyup');
|
||||
expect(handleClick).toBeCalled();
|
||||
});
|
||||
|
||||
it('should emit event named keydown when input text in text box', async () => {
|
||||
const handleClick = jest.fn();
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
return () => {
|
||||
return <ButtonEdit onKeydown={handleClick}></ButtonEdit>;
|
||||
};
|
||||
},
|
||||
});
|
||||
await wrapper.find('div').find('div').find('input').trigger('keydown');
|
||||
expect(handleClick).toBeCalled();
|
||||
});
|
||||
|
||||
it('should emit event named input when input text in text box', async () => {
|
||||
const handleClick = jest.fn();
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
return () => {
|
||||
return <ButtonEdit onInput={handleClick}></ButtonEdit>;
|
||||
};
|
||||
},
|
||||
});
|
||||
await wrapper.find('div').find('div').find('input').trigger('input');
|
||||
expect(handleClick).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('behaviors', () => {
|
||||
it('should hightlight text box when mouse in', () => {});
|
||||
it('should show clear button when mouse in text box', () => {});
|
||||
it('should show clear button when fouse text box and it not empty', () => {});
|
||||
it('should hide clear button when text box is emtyp', () => {});
|
||||
it('should show clear button when text any word from empty', () => {});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
import type { App } from 'vue';
|
||||
import FButton from './src/button.component';
|
||||
|
||||
export * from './src/button.props';
|
||||
|
||||
export { FButton };
|
||||
|
||||
export default {
|
||||
install(app: App): void {
|
||||
app.component(FButton.name, FButton);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
// import { defineComponent, computed } from 'vue';
|
||||
// import type { SetupContext } from 'vue';
|
||||
// import { buttonGroupProps, ButtonGroupProps } from './button.props';
|
||||
// import { useButtonGroup } from './composition/use-button-group';
|
||||
|
||||
// export default defineComponent({
|
||||
// name: 'FButtonGroup',
|
||||
// props: buttonGroupProps,
|
||||
// emits: ['click'],
|
||||
// setup(props: ButtonGroupProps, context: SetupContext) {
|
||||
// // const { onClickButton } = useButtonGroup(props, context);
|
||||
// // const fButtonSize = computed(() => ({
|
||||
// // 'btn-lg': props.size === 'large',
|
||||
// // 'btn-sm': props.size === 'small',
|
||||
// // }));
|
||||
// // const fButtonType = computed(() => ({
|
||||
// // 'btn-primary': props.buttonType === 'primary',
|
||||
// // 'btn-warning': props.buttonType === 'warning',
|
||||
// // 'btn-danger': props.buttonType === 'danger',
|
||||
// // 'btn-success': props.buttonType === 'success',
|
||||
// // 'btn-link': props.buttonType === 'link',
|
||||
// // 'btn-secondary': props.buttonType === 'secondary',
|
||||
// // }));
|
||||
|
||||
// return () => (
|
||||
// );
|
||||
// },
|
||||
// });
|
|
@ -0,0 +1,26 @@
|
|||
// import { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
// type ButtonType = 'primary' | 'warning' | 'danger' | 'success' | 'link' | 'secondary';
|
||||
// type SizeType = 'small' | 'large';
|
||||
|
||||
// export const buttonProps = {
|
||||
// /**
|
||||
// * 组件标识
|
||||
// */
|
||||
// id: String,
|
||||
// /**
|
||||
// * 设置按钮类型
|
||||
// */
|
||||
// buttonType: { type: String as PropType<ButtonType>, default: 'primary' },
|
||||
// /**
|
||||
// * 是否禁用
|
||||
// */
|
||||
// disable: { type: Boolean, default: false },
|
||||
// /**
|
||||
// * 按钮尺寸
|
||||
// */
|
||||
// size: { type: String as PropType<SizeType>, default: 'small' },
|
||||
// // 待确定:text参数
|
||||
// };
|
||||
|
||||
// export type ButtonProps = ExtractPropTypes<typeof buttonProps>;
|
|
@ -0,0 +1,102 @@
|
|||
import { defineComponent, computed } from 'vue';
|
||||
import type { SetupContext } from 'vue';
|
||||
import { buttonProps, ButtonProps } from './button.props';
|
||||
import { useButton } from './composition/use-button';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FButton',
|
||||
props: buttonProps,
|
||||
emits: ['click'],
|
||||
setup(props: ButtonProps, context: SetupContext) {
|
||||
const { onClickButton } = useButton(props, context);
|
||||
const fButtonSize = computed(() => ({
|
||||
'btn-lg': props.size === 'large',
|
||||
'btn-sm': props.size === 'small',
|
||||
}));
|
||||
const fButtonType = computed(() => ({
|
||||
'btn-primary': props.buttonType === 'primary',
|
||||
'btn-warning': props.buttonType === 'warning',
|
||||
'btn-danger': props.buttonType === 'danger',
|
||||
'btn-success': props.buttonType === 'success',
|
||||
'btn-link': props.buttonType === 'link',
|
||||
'btn-secondary': props.buttonType === 'secondary',
|
||||
}));
|
||||
|
||||
return () => (
|
||||
// btn-lg btn btn-primary
|
||||
// btn-sm btn btn-warning
|
||||
<div>
|
||||
<div style={'text-align:left;margin-top:10px;'}>
|
||||
<div style={'margin-top:10px;'}>primary</div>
|
||||
<button
|
||||
class={[fButtonSize.value, ' btn btn-primary']}
|
||||
style={'margin:5px'}
|
||||
id={props.id}
|
||||
disabled={props.disable}
|
||||
onClick={onClickButton}>
|
||||
主要按钮
|
||||
</button>
|
||||
<button
|
||||
class={[fButtonSize.value, ' btn btn-danger']}
|
||||
style={'margin:5px'}
|
||||
id={props.id}
|
||||
disabled={props.disable}
|
||||
onClick={onClickButton}>
|
||||
危险按钮
|
||||
</button>
|
||||
<button
|
||||
class={[fButtonSize.value, ' btn btn-success']}
|
||||
style={'margin:5px'}
|
||||
id={props.id}
|
||||
disabled={props.disable}
|
||||
onClick={onClickButton}>
|
||||
成功按钮
|
||||
</button>
|
||||
<button
|
||||
class={[fButtonSize.value, ' btn btn-warning']}
|
||||
style={'margin:5px'}
|
||||
id={props.id}
|
||||
disabled={props.disable}
|
||||
onClick={onClickButton}>
|
||||
警告按钮
|
||||
</button>
|
||||
<button
|
||||
class={[fButtonSize.value, ' btn btn-secondary']}
|
||||
style={'margin:5px'}
|
||||
id={props.id}
|
||||
disabled={props.disable}
|
||||
onClick={onClickButton}>
|
||||
信息按钮
|
||||
</button>
|
||||
<button
|
||||
class={[fButtonSize.value, ' btn btn-link']}
|
||||
style={'margin:5px'}
|
||||
id={props.id}
|
||||
disabled={props.disable}
|
||||
onClick={onClickButton}>
|
||||
文本按钮
|
||||
</button>
|
||||
</div>
|
||||
<div style={'text-align:left'}>
|
||||
<div style={'margin-top:10px;'}>size</div>
|
||||
<button
|
||||
class={['btn-sm btn', fButtonType.value]}
|
||||
style={'margin:5px'}
|
||||
id={props.id}
|
||||
disabled={props.disable}
|
||||
onClick={onClickButton}>
|
||||
小尺寸
|
||||
</button>
|
||||
<button
|
||||
class={['btn-lg btn', fButtonType.value]}
|
||||
style={'margin:5px'}
|
||||
id={props.id}
|
||||
disabled={props.disable}
|
||||
onClick={onClickButton}>
|
||||
大尺寸
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
import { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
type ButtonType = 'primary' | 'warning' | 'danger' | 'success' | 'link' | 'secondary';
|
||||
type SizeType = 'small' | 'large';
|
||||
|
||||
export const buttonProps = {
|
||||
/**
|
||||
* 组件标识
|
||||
*/
|
||||
id: String,
|
||||
/**
|
||||
* 设置按钮类型
|
||||
*/
|
||||
buttonType: { type: String as PropType<ButtonType>, default: 'primary' },
|
||||
/**
|
||||
* 是否禁用
|
||||
*/
|
||||
disable: { type: Boolean, default: false },
|
||||
/**
|
||||
* 按钮尺寸
|
||||
*/
|
||||
size: { type: String as PropType<SizeType>, default: 'small' },
|
||||
// 待确定:text参数
|
||||
};
|
||||
|
||||
export type ButtonProps = ExtractPropTypes<typeof buttonProps>;
|
|
@ -0,0 +1,13 @@
|
|||
// import { ComputedRef } from 'vue';
|
||||
|
||||
// export interface UseButton {
|
||||
// /**
|
||||
// * 附加按钮的Class
|
||||
// */
|
||||
// // buttonClass: ComputedRef<Record<string, boolean | undefined>>;
|
||||
// /**
|
||||
// * 点击附加按钮事件响应函数
|
||||
// */
|
||||
// onClickButton: ($event: Event) => void;
|
||||
|
||||
// }
|
|
@ -0,0 +1,13 @@
|
|||
import { ComputedRef } from 'vue';
|
||||
|
||||
export interface UseButton {
|
||||
/**
|
||||
* 附加按钮的Class
|
||||
*/
|
||||
// buttonClass: ComputedRef<Record<string, boolean | undefined>>;
|
||||
/**
|
||||
* 点击附加按钮事件响应函数
|
||||
*/
|
||||
onClickButton: ($event: Event) => void;
|
||||
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// import { UseButton } from './types';
|
||||
// import { ButtonProps } from '../button.props';
|
||||
// import { computed, SetupContext } from 'vue';
|
||||
|
||||
// export function useButton(props: ButtonProps, context: SetupContext): UseButton {
|
||||
|
||||
// // const buttonClass = computed(() => ({
|
||||
// // // 'input-group-append': true,
|
||||
// // // 'append-force-show': props.showButtonWhenDisabled && (props.readonly || props.disable),
|
||||
// // }));
|
||||
|
||||
// function onClickButton($event: Event) {
|
||||
// $event.stopPropagation();
|
||||
// // this.disabled
|
||||
// if (props.disable) {
|
||||
// context.emit('clickButton', $event);
|
||||
// }
|
||||
// }
|
||||
|
||||
// return {
|
||||
// // buttonClass,
|
||||
// onClickButton
|
||||
// };
|
||||
// }
|
|
@ -0,0 +1,24 @@
|
|||
import { UseButton } from './types';
|
||||
import { ButtonProps } from '../button.props';
|
||||
import { computed, SetupContext } from 'vue';
|
||||
|
||||
export function useButton(props: ButtonProps, context: SetupContext): UseButton {
|
||||
|
||||
// const buttonClass = computed(() => ({
|
||||
// // 'input-group-append': true,
|
||||
// // 'append-force-show': props.showButtonWhenDisabled && (props.readonly || props.disable),
|
||||
// }));
|
||||
|
||||
function onClickButton($event: Event) {
|
||||
$event.stopPropagation();
|
||||
// this.disabled
|
||||
if (props.disable) {
|
||||
context.emit('clickButton', $event);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// buttonClass,
|
||||
onClickButton
|
||||
};
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import type { App } from 'vue';
|
||||
import InputGroup from './src/input-group.component';
|
||||
|
||||
export default {
|
||||
install(app: App): void {
|
||||
app.component("inputGroup2", InputGroup);
|
||||
}
|
||||
};
|
|
@ -1,101 +0,0 @@
|
|||
|
||||
import { defineComponent, computed } from "vue";
|
||||
import { InputGroupProps, props } from './input-group.props';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'input-group3',
|
||||
props,
|
||||
setup(props: InputGroupProps) {
|
||||
const textBoxTitle = computed(() => (props.enableTitle ? props.value : ''));
|
||||
|
||||
const textBoxPlaceholder = computed(() => ((props.disable || props.readonly) && !props.forcePlaceholder ? '' : props.placeholder));
|
||||
|
||||
const isTextBoxReadonly = computed(() => {
|
||||
return props.readonly || !props.editable;
|
||||
});
|
||||
|
||||
const inputGroupClass = computed(() => ({
|
||||
'input-group': true,
|
||||
'f-state-disable': props.disable,
|
||||
'f-state-editable': props.editable && !props.disable && !props.readonly,
|
||||
'f-state-readonly': props.readonly && !props.disable
|
||||
}));
|
||||
|
||||
const textBoxClass = computed(() => ({
|
||||
'text-left': props.textAlign === 'left',
|
||||
'text-center': props.textAlign === 'center',
|
||||
'text-right': props.textAlign === 'right',
|
||||
'form-control': true,
|
||||
'f-utils-fill': true
|
||||
}));
|
||||
|
||||
const inputGroupAppendClass = computed(() => ({
|
||||
'input-group-append': true,
|
||||
'append-force-show': props.showButtonWhenDisabled && (props.readonly || props.disable)
|
||||
|
||||
}));
|
||||
|
||||
const showClearButton = computed(() => props.enableClear && !props.readonly && !props.disable);
|
||||
|
||||
function onBlur($event: Event) {
|
||||
console.log('on blur');
|
||||
}
|
||||
function onClearValue($event: Event) {
|
||||
console.log('on onClearValue');
|
||||
}
|
||||
function onEnter($event: KeyboardEvent) {
|
||||
console.log('on onEnter');
|
||||
}
|
||||
function onClickHandle(event: Event) {
|
||||
console.log('on onClickHandle');
|
||||
}
|
||||
function onIconMouseEnter(e: MouseEvent) {
|
||||
console.log('on onIconMouseEnter');
|
||||
}
|
||||
function onIconMouseLeave(e: MouseEvent) {
|
||||
console.log('on onIconMouseLeave');
|
||||
}
|
||||
function onInputClick($event: Event) {
|
||||
console.log('on onInputClick');
|
||||
}
|
||||
function onInputFocus($event: Event) {
|
||||
console.log('on onInputFocus');
|
||||
}
|
||||
function onMouseDown($event: MouseEvent) {
|
||||
console.log('on onMousedown');
|
||||
}
|
||||
function onMouseOverInExtentInfo($event: Event) {
|
||||
console.log('on onMouseOverInExtentInfo');
|
||||
}
|
||||
function onValueChange(val: string, emit = true) {
|
||||
console.log('on onValueChange');
|
||||
}
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class="f-cmp-inputgroup" id={props.id}>
|
||||
<div class={[props.customClass, inputGroupClass.value]}>
|
||||
<input name="input-group-value" autocomplete={'' + props.autoComplete} class={textBoxClass.value} disabled={props.disable}
|
||||
maxlength={props.maxLength} minlength={props.minLength} placeholder={textBoxPlaceholder.value}
|
||||
readonly={isTextBoxReadonly.value} tabindex={props.tabIndex} title={textBoxTitle.value} type={props.inputType}
|
||||
onBlur={onBlur} onClick={onInputClick} onFocus={onInputFocus} onKeydown={onEnter}
|
||||
onMousedown={onMouseDown} />
|
||||
<div class={inputGroupAppendClass.value}>
|
||||
{
|
||||
showClearButton.value &&
|
||||
<span class="input-group-text input-group-clear">
|
||||
<i class="f-icon modal_close"></i>
|
||||
</span>
|
||||
}
|
||||
{
|
||||
props.groupText &&
|
||||
<span class="input-group-text" v-html={props.groupText}>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
|
@ -1,22 +0,0 @@
|
|||
import { ExtractPropTypes } from 'vue';
|
||||
export const props = {
|
||||
id: String,
|
||||
customClass: { type: String, default: '' },
|
||||
disable: { type: Boolean, default: false },
|
||||
editable: { type: Boolean, default: true },
|
||||
readonly: { type: Boolean, default: false },
|
||||
textAlign: { type: String, default: 'left' },
|
||||
showButtonWhenDisabled: { type: Boolean, default: false },
|
||||
enableClear: { type: Boolean, default: false },
|
||||
groupText: { type: String, default: '<i class="f-icon f-icon-lookup"></i>' },
|
||||
enableTitle: { type: Boolean, default: false },
|
||||
inputType: { type: String, default: 'text' },
|
||||
forcePlaceholder: { type: Boolean, default: false },
|
||||
placeholder: { type: String, default: '' },
|
||||
autoComplete: { type: Boolean, default: false },
|
||||
value: { type: String, default: '' },
|
||||
minLength: Number,
|
||||
maxLength: Number,
|
||||
tabIndex: Number
|
||||
};
|
||||
export type InputGroupProps = ExtractPropTypes<typeof props>;
|
|
@ -0,0 +1,131 @@
|
|||
import { computed, defineComponent, ref, SetupContext, watch } from 'vue';
|
||||
import { NotifyButton, NotifyData } from '../notify.props';
|
||||
import { ToastProps, toastProps } from './toast.props';
|
||||
|
||||
import './toast.css';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Toast',
|
||||
props: toastProps,
|
||||
emits: ['close', 'click'],
|
||||
setup: (props: ToastProps, context: SetupContext) => {
|
||||
const animateIn = ref(props.animate);
|
||||
const animateEnd = 'fadeOut';
|
||||
const toast = computed(() => {
|
||||
return props.options as NotifyData;
|
||||
});
|
||||
const showingToast = ref(false);
|
||||
|
||||
const toastClass = computed(() => {
|
||||
const classObject = {
|
||||
animated: showingToast.value,
|
||||
toast: true,
|
||||
};
|
||||
classObject[props.animate] = false;
|
||||
classObject[animateEnd] = showingToast.value;
|
||||
classObject[toast.value.type] = true;
|
||||
if (toast.value.theme) {
|
||||
classObject[toast.value.theme] = true;
|
||||
}
|
||||
return classObject;
|
||||
});
|
||||
|
||||
const toastIconClass = computed(() => {
|
||||
const hasSpecialToastType = toast.value && toast.value.type;
|
||||
const iconType = hasSpecialToastType ? toast.value.type.replace('toasty-type-', '') : 'default';
|
||||
const iconTypeName = `f-icon-${iconType}`;
|
||||
const classObject = { 'f-icon': true };
|
||||
classObject[iconTypeName] = true;
|
||||
return classObject;
|
||||
});
|
||||
|
||||
const shouldShowTips = computed(() => toast.value.title || toast.value.msg);
|
||||
|
||||
const shouldShowTitle = computed(() => toast.value.title && toast.value.msg);
|
||||
|
||||
const shouldShowMessageOnly = computed(() => !toast.value.title && toast.value.msg);
|
||||
|
||||
const shouldShowCloseButton = computed(() => {
|
||||
return true;
|
||||
});
|
||||
|
||||
const shouldShowButtonsInTitle = computed(() => !!toast.value.buttons || !!context.slots.default);
|
||||
|
||||
function onCloseToast($event: Event) {
|
||||
$event.stopPropagation();
|
||||
$event.preventDefault();
|
||||
showingToast.value = false;
|
||||
setTimeout(() => {
|
||||
context.emit('close', toast.value);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function onClickButton($event: Event, notifyButton: NotifyButton) {}
|
||||
|
||||
function getNotifyButtonClass(notifyButton: NotifyButton) {
|
||||
return `f-preten-link ${notifyButton.customClass ? notifyButton.customClass : ''}`;
|
||||
}
|
||||
|
||||
watch(animateIn, () => {
|
||||
const animateInClass = animateIn.value || 'bounceInRight';
|
||||
const animateOutClass = 'fadeOut';
|
||||
});
|
||||
|
||||
const renderNotifyButtons = () => {
|
||||
return (
|
||||
<>
|
||||
<div class="after-toast-msg text-right">
|
||||
{!context.slots.default &&
|
||||
toast.value.buttons?.map((notifyButton: NotifyButton) => {
|
||||
return (
|
||||
<span
|
||||
class={getNotifyButtonClass(notifyButton)}
|
||||
onClick={($event) => onClickButton($event, notifyButton)}>
|
||||
{notifyButton.text}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{context.slots.default && context.slots.default()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class={toastClass}>
|
||||
{shouldShowCloseButton.value && (
|
||||
<button class="toast-close f-btn-icon f-bare" onClick={onCloseToast}>
|
||||
<span class="f-icon modal_close"></span>
|
||||
</button>
|
||||
)}
|
||||
{shouldShowTips.value && (
|
||||
<section class="modal-tips">
|
||||
<div class="float-left modal-tips-iconwrap">
|
||||
<span class={toastIconClass}></span>
|
||||
</div>
|
||||
<div class="modal-tips-content">
|
||||
{shouldShowTitle.value && (
|
||||
<>
|
||||
<h5 class="toast-title modal-tips-title" v-html={toast.value.title}></h5>
|
||||
<p class="toast-msg" v-html={toast.value.msg}></p>
|
||||
{shouldShowButtonsInTitle.value && renderNotifyButtons()}
|
||||
</>
|
||||
)}
|
||||
{shouldShowMessageOnly.value &&
|
||||
(toast.value.buttons ? (
|
||||
<div class="toast-title-btns-wrapper d-flex">
|
||||
<h5 class="toast-title modal-tips-title only-toast-msg" v-html={toast.value.msg}></h5>
|
||||
<div class="after-toast-title text-right ml-auto">{renderNotifyButtons()}</div>
|
||||
</div>
|
||||
) : (
|
||||
<h5 class="toast-title modal-tips-title only-toast-msg" v-html={toast.value.msg}></h5>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
.toast-title-beforeshow {
|
||||
opacity: 0;
|
||||
}
|
||||
@-webkit-keyframes farrisMoveUpIn {
|
||||
0% {
|
||||
transform: translateY(-100%);
|
||||
transform-origin: 0 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
transform-origin: 0 0;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes farrisMoveUpIn {
|
||||
0% {
|
||||
transform: translateY(-100%);
|
||||
transform-origin: 0 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
transform-origin: 0 0;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes farrisMoveUpOut {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
transform-origin: 0 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(-100%);
|
||||
transform-origin: 0 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes farrisMoveUpOut {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
transform-origin: 0 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(-100%);
|
||||
transform-origin: 0 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.toast.fadeIn {
|
||||
-webkit-animation: farrisMoveUpIn 0.2s linear;
|
||||
animation: farrisMoveUpIn 0.2s linear;
|
||||
}
|
||||
.toast.fadeOut {
|
||||
-webkit-animation: farrisMoveUpOut 0.2s linear;
|
||||
animation: farrisMoveUpOut 0.2s linear;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { ExtractPropTypes, PropType } from 'vue';
|
||||
import { NotifyData, ToastyAnimate } from '../notify.props';
|
||||
|
||||
export const toastProps = {
|
||||
animate: { type: String as PropType<ToastyAnimate>, default: 'fadeIn' },
|
||||
options: { type: Object as PropType<NotifyData> },
|
||||
};
|
||||
|
||||
export type ToastProps = ExtractPropTypes<typeof toastProps>;
|
|
@ -0,0 +1,86 @@
|
|||
import { isFunction } from 'lodash';
|
||||
import { computed, defineComponent, ref, SetupContext } from 'vue';
|
||||
import { NotifyData, NotifyProps, notifyProps } from './notify.props';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Notify',
|
||||
props: notifyProps,
|
||||
emits: ['empty'],
|
||||
setup(props: NotifyProps, context: SetupContext) {
|
||||
const notifyClass = computed(() => ({
|
||||
'farris-notify': true,
|
||||
}));
|
||||
|
||||
const defaultNotifyDistance = {
|
||||
left: 12,
|
||||
right: 12,
|
||||
top: 136,
|
||||
bottom: 12,
|
||||
};
|
||||
|
||||
const toasts = ref(props.toasts || []);
|
||||
|
||||
const notifyStyle = computed(() => ({
|
||||
left: props.position.indexOf('left') > -1 ? `${props.left ? props.left : defaultNotifyDistance.left}px` : '',
|
||||
right: props.position.indexOf('right') > -1 ? `${props.right ? props.right : defaultNotifyDistance.right}px` : '',
|
||||
top: props.position.indexOf('top') > -1 ? `${props.top ? props.top : defaultNotifyDistance.top}px` : '',
|
||||
bottom: props.position.indexOf('bottom') > -1 ? `${props.bottom ? props.bottom : defaultNotifyDistance.bottom}px` : '',
|
||||
}));
|
||||
|
||||
function closeToast(toast: NotifyData) {}
|
||||
|
||||
function addToast(toast: NotifyData) {
|
||||
if (toasts.value.length >= props.limit) {
|
||||
toasts.value.shift();
|
||||
}
|
||||
toasts.value.push(toast);
|
||||
// if (props.timeout) {
|
||||
// this._setTimeout(notify);
|
||||
// }
|
||||
}
|
||||
|
||||
function invokeToastOnRemoveCallback(toast: NotifyData) {
|
||||
if (toast && toast.onRemove && isFunction(toast.onRemove)) {
|
||||
toast.onRemove.call(this, toast);
|
||||
}
|
||||
}
|
||||
|
||||
function clear(id: number | string) {
|
||||
const targetToastIndex = toasts.value.findIndex((toast: NotifyData) => toast.id === id);
|
||||
if (targetToastIndex > -1) {
|
||||
const targetToast = toasts.value[targetToastIndex];
|
||||
invokeToastOnRemoveCallback(targetToast);
|
||||
toasts.value.splice(targetToastIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
toasts.value.forEach((toast: NotifyData) => invokeToastOnRemoveCallback(toast));
|
||||
toasts.value.length = 0;
|
||||
context.emit('empty');
|
||||
}
|
||||
|
||||
context.expose({ addToast, clear, clearAll, closeToast });
|
||||
|
||||
function onClose($event: Event, toast: NotifyData) {
|
||||
closeToast(toast);
|
||||
}
|
||||
function onClick($event: Event) {}
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div id={props.id} class={notifyClass.value} style={notifyStyle.value}>
|
||||
{toasts.value.map((toastData: NotifyData) => {
|
||||
return (
|
||||
<f-toast
|
||||
v-model={toastData}
|
||||
animate={props.animate}
|
||||
onClose={($event: Event) => onClose($event, toastData)}
|
||||
onClick={onClick}></f-toast>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
import { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
export type NotifyPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'top-center' | 'bottom-center' | 'center-center';
|
||||
|
||||
export type ToastyAnimate =
|
||||
| 'bounceInRight'
|
||||
| 'bounceInLeft'
|
||||
| 'bounceInRight'
|
||||
| 'bounceInLeft'
|
||||
| 'bounceInDown'
|
||||
| 'bounceInUp'
|
||||
| 'bounceIn'
|
||||
| 'fadeIn';
|
||||
|
||||
export type NotifyTheme = 'default' | 'material' | 'bootstrap';
|
||||
|
||||
export interface NotifyButton {
|
||||
customClass?: string;
|
||||
text: string;
|
||||
disable?: boolean;
|
||||
onClick?: ($event: Event, component: any) => any;
|
||||
}
|
||||
|
||||
export interface NotifyData {
|
||||
type: string;
|
||||
title?: string;
|
||||
msg?: string;
|
||||
/** 按钮列表模板 */
|
||||
buttons?: Array<NotifyButton>;
|
||||
showClose?: boolean;
|
||||
theme?: string;
|
||||
timeout?: number;
|
||||
onAdd?: () => void;
|
||||
onRemove?: () => void;
|
||||
id?: number | string;
|
||||
}
|
||||
|
||||
// export interface NotifyData extends NotifyOptions {}
|
||||
|
||||
export const notifyProps = {
|
||||
limit: { type: Number, default: 5 },
|
||||
showCloseButton: { type: Boolean, default: true },
|
||||
position: { type: String as PropType<NotifyPosition>, default: 'top-center' },
|
||||
timeout: { type: Number, default: 3000 },
|
||||
theme: { type: String as PropType<NotifyTheme>, default: 'bootstrap' },
|
||||
left: { type: Number },
|
||||
right: { type: Number },
|
||||
top: { type: Number },
|
||||
bottom: { type: Number },
|
||||
id: { type: String },
|
||||
animate: { type: String as PropType<ToastyAnimate>, default: 'fadeIn' },
|
||||
toasts: { type: Array<NotifyData> },
|
||||
options: { type: Object as PropType<NotifyData> },
|
||||
};
|
||||
export type NotifyProps = ExtractPropTypes<typeof notifyProps>;
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import { computed, defineComponent, SetupContext } from 'vue';
|
||||
import { PopoverProps, popoverProps } from './popover.props';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FPopover',
|
||||
props: popoverProps,
|
||||
emits: [],
|
||||
setup(props: PopoverProps, context: SetupContext) {
|
||||
const shouldShowTitle = computed(() => !!props.title);
|
||||
|
||||
const popoverContainerClass = computed(() => ({
|
||||
'popover-content': true,
|
||||
'popover-body': true,
|
||||
}));
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<>
|
||||
<div class="popover-arrow arrow"></div>
|
||||
{shouldShowTitle.value && <h3 class="popover-title popover-header">{props.title}</h3>}
|
||||
<div class={popoverContainerClass.value}>{context.slots.default && context.slots?.default()}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
import { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
export type PopoverPosition = 'top' | 'bottom' | 'left' | 'right' | 'auto';
|
||||
|
||||
export const popoverProps = {
|
||||
title: { type: String },
|
||||
position: { type: String as PropType<PopoverPosition>, default: 'top' },
|
||||
};
|
||||
|
||||
export type PopoverProps = ExtractPropTypes<typeof popoverProps>;
|
|
@ -0,0 +1,94 @@
|
|||
import { computed, defineComponent, ref, SetupContext } from 'vue';
|
||||
import { switchProps, SwitchProps, SwitchType } from './switch.props';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FSwitch',
|
||||
props: switchProps,
|
||||
emits: [],
|
||||
setup(props: SwitchProps, context: SetupContext) {
|
||||
const checked = ref(false);
|
||||
|
||||
const disable = ref(false);
|
||||
|
||||
const editable = ref(false);
|
||||
|
||||
const squire = ref(false);
|
||||
|
||||
const size = ref(props.size);
|
||||
|
||||
const checkedLabel = ref('');
|
||||
|
||||
const uncheckedLabel = ref('');
|
||||
|
||||
function getColor(flag = '') {
|
||||
if (flag === 'borderColor') {
|
||||
return this.defaultBoColor;
|
||||
}
|
||||
if (flag === 'switchColor') {
|
||||
if (this.reverse) {
|
||||
return !this.checked ? this.switchColor : this.switchOffColor || this.switchColor;
|
||||
}
|
||||
return this.checked ? this.switchColor : this.switchOffColor || this.switchColor;
|
||||
}
|
||||
if (this.reverse) {
|
||||
return !this.checked ? this.color : this.defaultBgColor;
|
||||
}
|
||||
return this.checked ? this.color : this.defaultBgColor;
|
||||
}
|
||||
|
||||
function getBackgroundColor() {
|
||||
return '';
|
||||
}
|
||||
|
||||
function getBorderColor() {
|
||||
return '';
|
||||
}
|
||||
|
||||
function getSwitchColor() {
|
||||
return '';
|
||||
}
|
||||
|
||||
const switchContainerClass = computed(() => ({
|
||||
switch: true,
|
||||
'f-cmp-switch': true,
|
||||
checked: checked.value,
|
||||
disabled: disable.value || !editable.value,
|
||||
squire: squire.value,
|
||||
'switch-large': size.value === 'large',
|
||||
'switch-medium': size.value === 'medium',
|
||||
'switch-small': size.value === 'small',
|
||||
}));
|
||||
|
||||
const switchContainerStyle = computed(() => ({
|
||||
outline: 'none',
|
||||
'backgroud-color': getBackgroundColor(),
|
||||
'border-color': getBorderColor(),
|
||||
}));
|
||||
|
||||
const smallStyle = computed(() => ({
|
||||
background: getSwitchColor(),
|
||||
}));
|
||||
|
||||
const shouldShowSwitch = computed(() => {
|
||||
// checkedLabel || uncheckedLabel
|
||||
return checkedLabel.value || uncheckedLabel.value;
|
||||
});
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<>
|
||||
<span tabindex="0" role="button" class={switchContainerClass.value} style={switchContainerStyle.value}>
|
||||
{shouldShowSwitch.value && (
|
||||
<span class="switch-pane">
|
||||
<span class="switch-label-checked">{checkedLabel.value}</span>
|
||||
<span class="switch-label-unchecked">{uncheckedLabel.value}</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
<small style={smallStyle.value}>{context.slots.default && context.slots.default()}</small>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
import { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
export type SwitchType = 'small' | 'medium' | 'large';
|
||||
|
||||
export const switchProps = {
|
||||
square: { type: Boolean, default: false },
|
||||
size: { type: String as PropType<SwitchType>, default: 'medium' },
|
||||
switchOffColor: { type: String },
|
||||
switchColor: { type: String },
|
||||
defaultBackgroundColor: { type: String },
|
||||
defaultBorderColor: { type: String },
|
||||
checkedLabel: { type: String },
|
||||
uncheckedLabel: { type: String },
|
||||
checked: { type: Boolean },
|
||||
readonly: { type: Boolean },
|
||||
disable: { type: Boolean },
|
||||
editable: { type: Boolean, default: true },
|
||||
reverse: { type: Boolean },
|
||||
trueValue: { type: Object, default: true },
|
||||
falseValue: { type: Object, default: false },
|
||||
};
|
||||
export type SwitchProps = ExtractPropTypes<typeof switchProps>;
|
|
@ -0,0 +1,41 @@
|
|||
import { computed, defineComponent, ref, SetupContext } from 'vue';
|
||||
import { TextProps, textProps } from './text.props';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FText',
|
||||
props: textProps,
|
||||
emits: [],
|
||||
setup(props: TextProps, context: SetupContext) {
|
||||
const isTextArea = ref(true);
|
||||
const autoSize = ref(true);
|
||||
const textAlginment = ref('');
|
||||
const height = ref(0);
|
||||
const maxHeight = ref(0);
|
||||
|
||||
const textClass = computed(() => ({
|
||||
'f-form-control-text': !isTextArea.value,
|
||||
'f-form-context-textarea': isTextArea,
|
||||
'f-component-text-auto-size': autoSize.value,
|
||||
}));
|
||||
|
||||
const textStyle = computed(() => ({
|
||||
textalign: textAlginment.value,
|
||||
height: !autoSize.value && height.value > 0 ? `${height.value}px` : '',
|
||||
'min-height': !autoSize.value && height.value > 0 ? `${height.value}px` : '',
|
||||
'max-height': !autoSize.value && maxHeight.value > 0 ? `${maxHeight.value}px` : '',
|
||||
}));
|
||||
|
||||
const text = computed(() => {
|
||||
// text && text.length > 0 ? text : control
|
||||
return '';
|
||||
});
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<span class={textClass.value} style={textStyle.value}>
|
||||
{text.value}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
import { ExtractPropTypes } from 'vue';
|
||||
|
||||
export const textProps = {};
|
||||
export type TextProps = ExtractPropTypes<typeof textProps>;
|
|
@ -0,0 +1,35 @@
|
|||
import { computed, defineComponent, ref, SetupContext } from 'vue';
|
||||
import { TooltipProps, tooltipProps } from './tooltip.props';
|
||||
|
||||
import './tooltip.css';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FTooltip',
|
||||
props: tooltipProps,
|
||||
emits: [],
|
||||
setup(props: TooltipProps, context: SetupContext) {
|
||||
const isTextContext = ref(true);
|
||||
|
||||
const tooltipClass = computed(() => ({
|
||||
tooltip: true,
|
||||
show: true,
|
||||
}));
|
||||
|
||||
const shouldShowTooltipText = computed(() => isTextContext.value);
|
||||
|
||||
const tooltipText = computed(() => props.content);
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class={tooltipClass}>
|
||||
<div class="arrow"></div>
|
||||
<div class="tooltip-inner">
|
||||
<div class="tooltip-tmpl">
|
||||
{shouldShowTooltipText.value && <div class="tooltip-text" v-html={tooltipText.value}></div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
.f-tooltip-bottom .arrow,
|
||||
.f-tooltip-top .arrow {
|
||||
left: 50%;
|
||||
}
|
||||
.f-tooltip-left .arrow,
|
||||
.f-tooltip-right .arrow {
|
||||
top: 25%;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
export type TooltipPosition =
|
||||
| 'top'
|
||||
| 'top-left'
|
||||
| 'top-right'
|
||||
| 'bottom'
|
||||
| 'bottom-left'
|
||||
| 'bottom-right'
|
||||
| 'right'
|
||||
| 'right-top'
|
||||
| 'right-bottom'
|
||||
| 'left'
|
||||
| 'left-top'
|
||||
| 'left-bottom';
|
||||
|
||||
export const tooltipProps = {
|
||||
content: { type: String },
|
||||
width: { type: Number },
|
||||
customClass: { type: String },
|
||||
position: { type: String as PropType<TooltipPosition>, default: 'top' },
|
||||
};
|
||||
export type TooltipProps = ExtractPropTypes<typeof tooltipProps>;
|
|
@ -0,0 +1,38 @@
|
|||
module.exports = {
|
||||
// Automatically clear mock calls and instances between every test
|
||||
clearMocks: true,
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx|js|jsx)$': [
|
||||
'babel-jest',
|
||||
{
|
||||
presets: [['@babel/preset-env', { targets: { node: 'current' } }], ['@babel/preset-typescript']],
|
||||
plugins: ['@vue/babel-plugin-jsx'],
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/?!(lodash-es)'],
|
||||
// The glob patterns Jest uses to detect test files
|
||||
testMatch: ['**/**/*.spec.(ts|tsx)'],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
moduleFileExtensions: ['js', 'json', 'ts', 'tsx'],
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
moduleNameMapper: {
|
||||
'\\.(css|less|scss|sass)$': '<rootDir>/__mocks__/style-mock.ts',
|
||||
'\\.(gif|ttf|eot|svg|jpg|png)$': '<rootDir>/__mocks__/file-mock.ts',
|
||||
},
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
testPathIgnorePatterns: ['/node_modules/'],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
setupFiles: ['<rootDir>/jest.setup.js'],
|
||||
|
||||
testEnvironmentOptions: {
|
||||
customExportConditions: ['node', 'node-addons'],
|
||||
},
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import { config } from '@vue/test-utils';
|
||||
|
||||
import ButtonEdit from './components/button-edit/src/button-edit.component';
|
||||
|
||||
config.global.components = {
|
||||
'f-button-edit': ButtonEdit,
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -2,14 +2,21 @@
|
|||
"name": "@farris/ui-vue",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test": "jest --config jest.config.js",
|
||||
"coverage": "jest --config jest.config.js --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.2.37"
|
||||
"vue": "^3.2.37",
|
||||
"@types/lodash-es": "^4.17.4",
|
||||
"@vue/shared": "^3.2.0",
|
||||
"@vueuse/core": "9.2.0",
|
||||
"async-validator": "^4.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^3.1.0",
|
||||
|
@ -17,6 +24,36 @@
|
|||
"@vue/babel-plugin-jsx": "^1.1.1",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^3.1.0",
|
||||
"vue-tsc": "^0.40.4"
|
||||
"vue-tsc": "^0.40.4",
|
||||
"@vue/test-utils": "^2.0.0",
|
||||
"@babel/parser": "^7.19.0",
|
||||
"@babel/preset-env": "^7.19.0",
|
||||
"@babel/preset-typescript": "^7.18.0",
|
||||
"@babel/traverse": "^7.19.0",
|
||||
"@commitlint/cli": "^17.1.0",
|
||||
"@commitlint/config-conventional": "^17.1.0",
|
||||
"@types/chalk": "^2.2.0",
|
||||
"@types/commander": "^2.12.2",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/ora": "^3.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.37.0",
|
||||
"@typescript-eslint/parser": "^5.37.0",
|
||||
"@vue/compiler-sfc": "^3.2.0",
|
||||
"@vuedx/typecheck": "^0.7.5",
|
||||
"@vuedx/typescript-plugin-vue": "^0.7.5",
|
||||
"babel-jest": "^29.0.3",
|
||||
"chalk": "^5.0.0",
|
||||
"commander": "^9.4.0",
|
||||
"conventional-changelog-cli": "^2.2.2",
|
||||
"inquirer": "^9.1.1",
|
||||
"jest": "^29.0.0",
|
||||
"ora": "^6.1.2",
|
||||
"patch-vue-directive-ssr": "^0.0.1",
|
||||
"sass": "^1.32.2",
|
||||
"shelljs": "^0.8.4",
|
||||
"vite-plugin-md": "^0.20.0",
|
||||
"vite-svg-loader": "^3.6.0",
|
||||
"vitepress": "0.20.1",
|
||||
"vitepress-theme-demoblock": "1.3.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,44 +1,49 @@
|
|||
<script setup lang="ts">
|
||||
// This starter template is using Vue 3 <script setup> SFCs
|
||||
// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
|
||||
import { ref } from "vue";
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
import InputGroup from "../components/input-group/src/input-group.component";
|
||||
import { ref } from 'vue';
|
||||
import HelloWorld from './components/hello-world.vue';
|
||||
import ButtonEdit from '../components/button-edit/src/button-edit.component';
|
||||
import FButton from '../components/button/src/button.component';
|
||||
|
||||
let canEdit = ref(true);
|
||||
let canAutoComplete = ref(false);
|
||||
const canEdit = ref(true);
|
||||
const disable = ref(false);
|
||||
|
||||
const canAutoComplete = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a href="https://vitejs.dev" target="_blank">
|
||||
<img src="/vite.svg" class="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://vuejs.org/" target="_blank">
|
||||
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
|
||||
</a>
|
||||
</div>
|
||||
<input type="checkbox" id="checkbox" v-model="canEdit" />
|
||||
<label for="checkbox">editable:{{ canEdit }}</label>
|
||||
<input type="checkbox" id="checkbox" v-model="canAutoComplete" />
|
||||
<label for="checkbox">auto complete:{{ canAutoComplete }}</label>
|
||||
<HelloWorld msg="Vite + Vue" />
|
||||
<InputGroup :editable="canEdit" :auto-complete="canAutoComplete" :enable-clear="true"></InputGroup>
|
||||
<div>
|
||||
<a href="https://vitejs.dev" target="_blank">
|
||||
<img src="/vite.svg" class="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://vuejs.org/" target="_blank">
|
||||
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
|
||||
</a>
|
||||
</div>
|
||||
<input type="checkbox" id="checkbox" v-model="canEdit" />
|
||||
<label for="checkbox">editable:{{ canEdit }}</label>
|
||||
<input type="checkbox" id="checkbox" v-model="canAutoComplete" />
|
||||
<label for="checkbox">auto complete:{{ canAutoComplete }}</label>
|
||||
<HelloWorld msg="Vite + Vue" />
|
||||
<ButtonEdit :editable="canEdit" :auto-complete="canAutoComplete" :enable-clear="true"></ButtonEdit>
|
||||
<input type="checkbox" id="checkbox" v-model="disable" />
|
||||
<label for="checkbox">disable:{{ disable }}</label>
|
||||
<FButton :disable="disable"></FButton>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
|
||||
.logo.vue:hover {
|
||||
filter: drop-shadow(0 0 2em #42b883aa);
|
||||
filter: drop-shadow(0 0 2em #42b883aa);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import App from './app.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
|
|
|
@ -10,8 +10,12 @@
|
|||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"skipLibCheck": true
|
||||
"lib": ["ESNext", "DOM","dom.iterable", "scripthost"],
|
||||
"skipLibCheck": true,
|
||||
"importHelpers": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(),vueJsx()]
|
||||
})
|
||||
plugins: [vue(), vueJsx()]
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,165 @@
|
|||
# Farris UI Vue 组件开发规范
|
||||
|
||||
为了保证 Farris UI Vue 的源代码风格一致,确保组件研发质量,方便社区开发者阅读代码参与贡献,需要所有参与Farris UI Vue 组件开发的贡献,都要遵循此组件开发规范。
|
||||
|
||||
|
||||
## 组件目录和文件规范
|
||||
|
||||
Farris UI Vue 的组件,都包含在`ui-vue`包的`components`目录下,每一个子组件为独立目录,目录名为全小写的组件名,当组件名包含多个英文单词时,使用`-`分隔。
|
||||
|
||||
目录规范分成以下部分:
|
||||
|
||||
### 组件目录结构
|
||||
|
||||
以下是单个组件目录的结构
|
||||
|
||||
```
|
||||
input-group
|
||||
├── test // 单元测试
|
||||
| └── input-group.spec.tsx
|
||||
├── src // 组件源码
|
||||
| ├── components // 子组件
|
||||
| | └── input-group-sub.component.tsx
|
||||
| ├── composition // 组件的可复用逻辑
|
||||
| | ├── types.ts // 组合式Api返利值接口类型
|
||||
| | ├── use-append-button.ts // 实现组件特性1的组合式Api
|
||||
| | └── use-clear.ts // 实现组件特性2的组合式Api
|
||||
| ├── input-group.component.tsx // 组件代码
|
||||
| └── input-group.props.ts // 定义组件Api
|
||||
└── index.ts // 组件入口文件
|
||||
```
|
||||
|
||||
### 入口文件 `index.ts`
|
||||
|
||||
```TypeScript
|
||||
import type { App } from 'vue';
|
||||
import InputGroup from './src/input-group.component';
|
||||
|
||||
export * from './src/input-group.props';
|
||||
|
||||
export { InputGroup };
|
||||
|
||||
export default {
|
||||
install(app: App): void {
|
||||
app.component(InputGroup.name, InputGroup);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 组件文件 `input-group.component.tsx`
|
||||
|
||||
```TypeScript
|
||||
import { defineComponent, toRefs } from 'vue';
|
||||
import type { SetupContext } from 'vue';
|
||||
|
||||
import { InputGroupProps, inputGroupProps } from './input-group.props';
|
||||
import { useClear } from './composition/use-clear';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FInputGroup',
|
||||
props: inputGroupProps,
|
||||
emits: ['update:modelValue'],
|
||||
setup(props: InputGroupProps, setupContext: SetupContext) {
|
||||
const { someProp } = toRefs(props);
|
||||
const { showClearButton, onClearValue } = useClear();
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<div class="f-input-group">{ someProp.value }</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 类型文件 `input-group.props.ts`
|
||||
|
||||
```TypeScript
|
||||
import { PropType, ExtractPropTypes } from 'vue';
|
||||
|
||||
export const inputGroupProps = {
|
||||
/**
|
||||
* 显示输入框的标签
|
||||
*/
|
||||
enableTitle: { type: Boolean, default: false },
|
||||
/**
|
||||
* 输入框提示文本
|
||||
*/
|
||||
placeholder: { type: String, default: '' },
|
||||
/**
|
||||
* 输入框Tab键索引
|
||||
*/
|
||||
tabIndex: Number
|
||||
};
|
||||
|
||||
export type InputGroupProps = ExtractPropTypes<typeof inputGroupProps>;
|
||||
```
|
||||
|
||||
### 组合式Api `use-clear.ts`
|
||||
|
||||
```TypeScript
|
||||
import { computed, SetupContext } from 'vue';
|
||||
import { InputGroupProps } from '../input-group.props';
|
||||
import { UseClear } from './types';
|
||||
|
||||
export function useClear(props: InputGroupProps, setupContext: SetupContext): UseClear {
|
||||
const showClearButton = computed(() => props.enableClear && !props.readonly && !props.disable);
|
||||
|
||||
function onClearValue($event: Event) {
|
||||
console.log('on onClearValue');
|
||||
}
|
||||
|
||||
return {
|
||||
showClearButton,
|
||||
onClearValue
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 单元测试 `input-group.spec.tsx`
|
||||
|
||||
```ts
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { InputGroup } from '..';
|
||||
|
||||
describe('f-input-group', () => {
|
||||
it('variant', () => {
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
return () => {
|
||||
return <InputGroup editable={false}></InputGroup>;
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find('.f-cmp-inputgroup').exists()).toBeTruthy();
|
||||
expect(wrapper.find('div').find('div').find('input').find('[readlony]').exists).toBeTruthy();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 组件编码规范
|
||||
|
||||
- 组件采用中划线风格命名,组件的参数名和事件名统一使用中划线格式。
|
||||
- 所有组件统一在名称前使用`F`前缀,组件选择器前使用`f-`前缀。
|
||||
- 直接用v-model传递双向绑定的参数。
|
||||
- 使用属性透传传递原生属性,不允许在单独定义API声明原生属性。
|
||||
- 正确定义和使用TypeScript类型,代码中无TypeScript类型报错。
|
||||
- 变量采用语义化命名,原则上不需要通过注释说明变量或函数功能,详细命名规则参考[Farris UI TypeScript 编码指南](./style-guid/typescript_style_guid.md)。
|
||||
- 需要将组件的props定义在独立的文件some-component.props.ts文件中,并在此文件中同时导出props和PropsType。
|
||||
- 应该在setup函数返回的render函数中编写组件的Html模板。
|
||||
- 必须在组件的index.ts文件中导出组件参数的类型,以便于在引用组件时,方便TypeScript进行类型提示。
|
||||
- defineComponent函数接收的参数顺序为name、props、emits、inheritAttrs、setup。
|
||||
- 不要在组件内显式声明components和directives。
|
||||
- 需要安装字典顺序排列组件的变量。
|
||||
|
||||
|
||||
## 组件文档规范
|
||||
|
||||
待上线Doc站点后补充
|
||||
|
||||
|
||||
## 关于
|
||||
本项目的 Farris UI Vue 组件开发规范遵循[CC-By 3.0协议](https://creativecommons.org/licenses/by/3.0/)。
|
||||
|
||||
<a rel="license" href="https://creativecommons.org/licenses/by/3.0/"><img alt="Creative Commons License" style="border-width:0" src="https://i.creativecommons.org/l/by/3.0/88x31.png" /></a>
|
Loading…
Reference in New Issue