实现button样式、尺寸及禁用状态

This commit is contained in:
Cassiel 2022-10-01 10:44:47 +08:00
parent 2a63711c8a
commit e729aa7adb
95 changed files with 22426 additions and 1804 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -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>

110
commitlint.config.js Normal file
View File

@ -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".)',
},
},
}
};

View File

@ -4,5 +4,7 @@
],
"version": "0.0.0",
"useWorkspaces": true,
"npmClient": "yarn"
"npmClient": "yarn",
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"useNx": false
}

View File

@ -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"
}
}

BIN
packages/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -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>

View File

@ -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>

View File

@ -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;
}

View File

@ -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" }]

BIN
packages/eslint-config/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -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',
},
},
]
};

View File

@ -0,0 +1,4 @@
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
resolver: '<rootDir>/jest.resolver.js',
};

View File

@ -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);
};

View File

@ -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"
}
}

View File

@ -0,0 +1,4 @@
{
"root": true,
"extends": ["../index"]
}

View File

@ -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);
});

View File

@ -0,0 +1,12 @@
import { defineComponent } from 'vue';
export default defineComponent({
name: 'App',
setup() {
return () => (
<>
<h1>App</h1>
</>
);
},
});

View File

@ -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 />
</>
);
},
});

View File

@ -0,0 +1 @@
interface Foo {}

View File

@ -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"]
}

View File

@ -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>

View File

@ -0,0 +1,2 @@
const a = b + 1;
export default a;

View File

@ -11,6 +11,7 @@ node_modules
dist
dist-ssr
*.local
coverage
# Editor directories and files
.vscode/*

View File

@ -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>
);
};
},
});

View File

@ -0,0 +1,4 @@
/* 很重要 */
.farris-panel {
border: 1px solid rgba(0, 0, 0, .125)
}

View File

@ -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>;

View File

@ -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>
);
};
},
});

View File

@ -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>;

View File

@ -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');
}
}
/*coverimgSrc
*/
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');
}
}

View File

@ -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>
);
};
},
});

View File

@ -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>;

View File

@ -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;
}

View File

@ -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,
});
});
}
}

View File

@ -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 };
}

View File

@ -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);
},
};

View File

@ -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>
);
};
},
});

View File

@ -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>;

View File

@ -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;
}

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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', () => {});
});
});

View File

@ -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);
},
};

View File

@ -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 () => (
// );
// },
// });

View File

@ -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>;

View File

@ -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>
);
},
});

View File

@ -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>;

View File

@ -0,0 +1,13 @@
// import { ComputedRef } from 'vue';
// export interface UseButton {
// /**
// * 附加按钮的Class
// */
// // buttonClass: ComputedRef<Record<string, boolean | undefined>>;
// /**
// * 点击附加按钮事件响应函数
// */
// onClickButton: ($event: Event) => void;
// }

View File

@ -0,0 +1,13 @@
import { ComputedRef } from 'vue';
export interface UseButton {
/**
* Class
*/
// buttonClass: ComputedRef<Record<string, boolean | undefined>>;
/**
*
*/
onClickButton: ($event: Event) => void;
}

View File

@ -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
// };
// }

View File

@ -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
};
}

View File

@ -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);
}
};

View File

@ -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>
);
}
}
})

View File

@ -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>;

View File

@ -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>
);
};
},
});

View File

@ -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;
}

View File

@ -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>;

View File

@ -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>
);
};
},
});

View File

@ -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>;

View File

@ -0,0 +1 @@

View File

@ -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>
</>
);
};
},
});

View File

@ -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>;

View File

@ -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>
</>
);
};
},
});

View File

@ -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>;

View File

View File

@ -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>
);
};
},
});

View File

@ -0,0 +1,4 @@
import { ExtractPropTypes } from 'vue';
export const textProps = {};
export type TextProps = ExtractPropTypes<typeof textProps>;

View File

@ -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>
);
};
},
});

View File

@ -0,0 +1,8 @@
.f-tooltip-bottom .arrow,
.f-tooltip-top .arrow {
left: 50%;
}
.f-tooltip-left .arrow,
.f-tooltip-right .arrow {
top: 25%;
}

View File

@ -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>;

View File

@ -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'],
},
};

View File

@ -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,
};

10789
packages/ui-vue/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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>

View File

@ -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')

View File

@ -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" }]

View File

@ -1,9 +1,11 @@
{
"compilerOptions": {
"strict": true,
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true
},
"include": ["vite.config.ts"]
}

View File

@ -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

View File

@ -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>

8455
yarn.lock

File diff suppressed because it is too large Load Diff