release: 第一版工程提交

This commit is contained in:
wangjx 2025-02-21 09:14:45 +08:00
commit 8fd3a287a8
183 changed files with 29013 additions and 0 deletions

103
.cz-config.js Normal file
View File

@ -0,0 +1,103 @@
module.exports = {
// type 类型(定义之后,可通过上下键选择)
types: [
{ value: 'improvement', name: 'improvement: 功能优化' },
{ value: 'fix', name: 'fix: 修复 bug' },
{ value: 'feat', name: 'feat: 新增功能' },
{ value: 'style', name: 'style: 代码格式(不影响功能,例如空格、分号等格式修正)' },
{ value: 'docs', name: 'docs: 文档变更' },
{ value: 'refactor', name: 'refactor: 代码重构(不包括 bug 修复、功能新增)' },
{ value: 'perf', name: 'perf: 性能优化' },
{ value: 'test', name: 'test: 添加、修改测试用例' },
{
value: 'ci',
name: 'ci: 修改 CI 配置、脚本如打包部署脚本、dockerfile等'
},
{ value: 'revert', name: 'revert: 回滚 commit' },
{
value: 'build',
name: 'build: 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)'
},
{
value: 'chore',
name: 'chore: 版本发布或对构建过程或辅助工具和库的更改(不影响源文件、测试用例)'
}
// { value: ':bug: fix', name: '🐛 fix: 修复 bug' },
// { value: ':sparkles: feat', name: '✨ feat: 新增功能' },
// {
// value: ':lipstick: style',
// name: '💄 style: 代码格式(不影响功能,例如空格、分号等格式修正)'
// },
// { value: ':memo: docs', name: '📝 docs: 文档变更' },
// { value: ':recycle: refactor', name: '♻️ refactor: 代码重构(不包括 bug 修复、功能新增)' },
// { value: ':zap: perf', name: '⚡️ perf: 性能优化' },
// { value: ':white_check_mark: test', name: '✅ test: 添加、修改测试用例' },
// {
// value: ':hammer: chore',
// name: '🔨 chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)'
// },
// { value: ':wrench: ci', name: '🔧 ci: 修改 CI 配置、脚本' },
// { value: ':rocket: deps', name: '🚀 deps: 升级依赖' }
// {
// value: ':bookmark: release',
// name: '🔖 release: 版本发布'
// }
],
// scope 类型(定义之后,可通过上下键选择)
scopes: [
['custom', '自定义范围例如sys/user'],
['utils', 'utils 相关'],
['router', 'router 相关']
].map(([value, description]) => {
return {
value,
name: `${value.padEnd(30)} (${description})`
}
}),
// 是否允许自定义填写 scope在 scope 选择的时候,会有 empty 和 custom 可以选择。
// allowCustomScopes: true,
// allowTicketNumber: false,
// isTicketNumberRequired: false,
// ticketNumberPrefix: 'TICKET-',
// ticketNumberRegExp: '\\d{1,5}',
// 针对每一个 type 去定义对应的 scopes例如 fix
scopeOverrides: {
chore: [
{ value: 'release', name: 'release 版本发布' },
{ value: 'custom', name: 'custom 自定义scope' }
],
ci: [
{ value: 'deploy', name: 'deploy 部署脚本' },
{ value: 'docker', name: 'docker docker相关' },
{ value: 'custom', name: 'custom 自定义scope' }
],
build: [
{ value: 'deps', name: 'deps 依赖变更' },
{ value: 'custom', name: 'custom 自定义scope' }
]
},
// 交互提示信息
messages: {
type: '确保本次提交遵循 Angular 规范!\n选择你要提交的类型',
scope: '\n选择一个 scope可选',
customScope: '请输入自定义的 scope',
subject: '填写简短精炼的变更描述:\n',
body: '填写更加详细的变更描述(可选)。使用 "|" 换行:\n',
breaking: '列举非兼容性重大的变更(可选):\n',
footer: '列举出所有变更的 ISSUES CLOSED可选。 例如: #31, #34\n',
confirmCommit: '确认提交?'
},
// ['feat', 'fix']设置只有 type 选择了 feat 或 fix才询问 breaking message
allowBreakingChanges: [],
// 跳过要询问的步骤
skipQuestions: ['body'],
// subject 限制长度
subjectLimit: 100,
breaklineChar: '|' // 支持 body 和 footer
// footerPrefix : 'ISSUES CLOSED:'
// askForBreakingChangeFirst : true,
}

16
.editorconfig Normal file
View File

@ -0,0 +1,16 @@
# Editor configuration, see http://editorconfig.org
# 表示是最顶层的 EditorConfig 配置文件
root = true
[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格tab | space
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行
[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false

281
.eslintrc-auto-import.json Normal file
View File

@ -0,0 +1,281 @@
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"EffectScope": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"PropType": true,
"Ref": true,
"VNode": true,
"WritableComputedRef": true,
"asyncComputed": true,
"autoResetRef": true,
"computed": true,
"computedAsync": true,
"computedEager": true,
"computedInject": true,
"computedWithControl": true,
"controlledComputed": true,
"controlledRef": true,
"createApp": true,
"createEventHook": true,
"createGlobalState": true,
"createInjectionState": true,
"createReactiveFn": true,
"createSharedComposable": true,
"createUnrefFn": true,
"customRef": true,
"debouncedRef": true,
"debouncedWatch": true,
"defineAsyncComponent": true,
"defineComponent": true,
"eagerComputed": true,
"effectScope": true,
"extendRef": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"ignorableWatch": true,
"inject": true,
"isDefined": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"makeDestructurable": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeRouteLeave": true,
"onBeforeRouteUpdate": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onClickOutside": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onKeyStroke": true,
"onLongPress": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onStartTyping": true,
"onUnmounted": true,
"onUpdated": true,
"pausableWatch": true,
"provide": true,
"reactify": true,
"reactifyObject": true,
"reactive": true,
"reactiveComputed": true,
"reactiveOmit": true,
"reactivePick": true,
"readonly": true,
"ref": true,
"refAutoReset": true,
"refDebounced": true,
"refDefault": true,
"refThrottled": true,
"refWithControl": true,
"resolveComponent": true,
"resolveRef": true,
"resolveUnref": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"syncRef": true,
"syncRefs": true,
"templateRef": true,
"throttledRef": true,
"throttledWatch": true,
"toRaw": true,
"toReactive": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"tryOnBeforeMount": true,
"tryOnBeforeUnmount": true,
"tryOnMounted": true,
"tryOnScopeDispose": true,
"tryOnUnmounted": true,
"unref": true,
"unrefElement": true,
"until": true,
"useActiveElement": true,
"useArrayEvery": true,
"useArrayFilter": true,
"useArrayFind": true,
"useArrayFindIndex": true,
"useArrayFindLast": true,
"useArrayJoin": true,
"useArrayMap": true,
"useArrayReduce": true,
"useArraySome": true,
"useArrayUnique": true,
"useAsyncQueue": true,
"useAsyncState": true,
"useAttrs": true,
"useBase64": true,
"useBattery": true,
"useBluetooth": true,
"useBreakpoints": true,
"useBroadcastChannel": true,
"useBrowserLocation": true,
"useCached": true,
"useClipboard": true,
"useCloned": true,
"useColorMode": true,
"useConfirmDialog": true,
"useCounter": true,
"useCssModule": true,
"useCssVar": true,
"useCssVars": true,
"useCurrentElement": true,
"useCycleList": true,
"useDark": true,
"useDateFormat": true,
"useDebounce": true,
"useDebounceFn": true,
"useDebouncedRefHistory": true,
"useDeviceMotion": true,
"useDeviceOrientation": true,
"useDevicePixelRatio": true,
"useDevicesList": true,
"useDisplayMedia": true,
"useDocumentVisibility": true,
"useDraggable": true,
"useDropZone": true,
"useElementBounding": true,
"useElementByPoint": true,
"useElementHover": true,
"useElementSize": true,
"useElementVisibility": true,
"useEventBus": true,
"useEventListener": true,
"useEventSource": true,
"useEyeDropper": true,
"useFavicon": true,
"useFetch": true,
"useFileDialog": true,
"useFileSystemAccess": true,
"useFocus": true,
"useFocusWithin": true,
"useFps": true,
"useFullscreen": true,
"useGamepad": true,
"useGeolocation": true,
"useIdle": true,
"useImage": true,
"useInfiniteScroll": true,
"useIntersectionObserver": true,
"useInterval": true,
"useIntervalFn": true,
"useKeyModifier": true,
"useLastChanged": true,
"useLink": true,
"useLocalStorage": true,
"useMagicKeys": true,
"useManualRefHistory": true,
"useMediaControls": true,
"useMediaQuery": true,
"useMemoize": true,
"useMemory": true,
"useMounted": true,
"useMouse": true,
"useMouseInElement": true,
"useMousePressed": true,
"useMutationObserver": true,
"useNavigatorLanguage": true,
"useNetwork": true,
"useNow": true,
"useObjectUrl": true,
"useOffsetPagination": true,
"useOnline": true,
"usePageLeave": true,
"useParallax": true,
"usePermission": true,
"usePointer": true,
"usePointerLock": true,
"usePointerSwipe": true,
"usePreferredColorScheme": true,
"usePreferredContrast": true,
"usePreferredDark": true,
"usePreferredLanguages": true,
"usePreferredReducedMotion": true,
"usePrevious": true,
"useRafFn": true,
"useRefHistory": true,
"useResizeObserver": true,
"useRoute": true,
"useRouter": true,
"useScreenOrientation": true,
"useScreenSafeArea": true,
"useScriptTag": true,
"useScroll": true,
"useScrollLock": true,
"useSessionStorage": true,
"useShare": true,
"useSlots": true,
"useSorted": true,
"useSpeechRecognition": true,
"useSpeechSynthesis": true,
"useStepper": true,
"useStorage": true,
"useStorageAsync": true,
"useStyleTag": true,
"useSupported": true,
"useSwipe": true,
"useTemplateRefsList": true,
"useTextDirection": true,
"useTextSelection": true,
"useTextareaAutosize": true,
"useThrottle": true,
"useThrottleFn": true,
"useThrottledRefHistory": true,
"useTimeAgo": true,
"useTimeout": true,
"useTimeoutFn": true,
"useTimeoutPoll": true,
"useTimestamp": true,
"useTitle": true,
"useToNumber": true,
"useToString": true,
"useToggle": true,
"useTransition": true,
"useUrlSearchParams": true,
"useUserMedia": true,
"useVModel": true,
"useVModels": true,
"useVibrate": true,
"useVirtualList": true,
"useWakeLock": true,
"useWebNotification": true,
"useWebSocket": true,
"useWebWorker": true,
"useWebWorkerFn": true,
"useWindowFocus": true,
"useWindowScroll": true,
"useWindowSize": true,
"watch": true,
"watchArray": true,
"watchAtMost": true,
"watchDebounced": true,
"watchEffect": true,
"watchIgnorable": true,
"watchOnce": true,
"watchPausable": true,
"watchPostEffect": true,
"watchSyncEffect": true,
"watchThrottled": true,
"watchTriggerable": true,
"watchWithFilter": true,
"whenever": true
}
}

56
.eslintrc.js Normal file
View File

@ -0,0 +1,56 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true
},
extends: [
'plugin:vue/vue3-essential',
'airbnb-base',
'plugin:prettier/recommended', // 添加 prettier 插件
'./.eslintrc-auto-import.json'
],
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser',
sourceType: 'module'
},
globals: {
defineProps: true,
defineEmits: true,
defineExpose: true
},
settings: {},
plugins: ['vue', '@typescript-eslint'],
rules: {
'import/no-unresolved': 'off',
'import/extensions': 'off',
// 开放入参修改值
'no-param-reassign': [
'error',
{
props: true,
ignorePropertyModificationsFor: [
'e', // for e.returnvalue
'ctx', // for Koa routing
'req', // for Express requests
'request', // for Express requests
'res', // for Express responses
'response', // for Express responses
'state' // for vuex state
]
}
],
'import/prefer-default-export': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
vars: 'all',
args: 'after-used',
ignoreRestSiblings: true
}
],
'import/no-extraneous-dependencies': ['error', { devDependencies: true }]
}
}

33
.gitattributes vendored Normal file
View File

@ -0,0 +1,33 @@
* text=auto
# Force the following filetypes to have unix eols, so Windows does not break them
*.* text eol=lf
# Separate configuration for files without suffix
LICENSE text eol=lf
Dockerfile text eol=lf
pre-commit text eol=lf
commit-msg text eol=lf
# These files are binary and should be left untouched
# (binary is a macro for -text -diff)
*.ico binary
*.jpg binary
*.jpeg binary
*.png binary
*.pdf binary
*.doc binary
*.docx binary
*.ppt binary
*.pptx binary
*.xls binary
*.xlsx binary
*.exe binary
*.ttf binary
*.woff binary
*.woff2 binary
*.eot binary
*.otf binary
# Add more binary...

View File

@ -0,0 +1,56 @@
name: Build
# 打标签时触发构建另外标签需v开头例如v1.0.0需要配置DOCKER_PASSWORD的secrets
# 构建后镜像为 ${docker_registry}/${docker_username}/${repo_name}:1.0.0
on:
push:
tags:
- v*
env:
DOCKER_REGISTRY: registry.cn-hangzhou.aliyuncs.com
DOCKER_USERNAME: rsjst
jobs:
build-image:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 1
- name: Setup Pnpm and Install
uses: seepine/action-setup-pnpm@v1
- name: Project Build
run: pnpm run build
- name: Get Meta
id: meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=$(echo ${{ github.ref }} | awk -F"/" '{print $3}' | awk -F"v" '{print $2}') >> $GITHUB_OUTPUT
- name: Docker build push
uses: seepine/action-docker-build-push@v1
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ env.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
context: .
file: ./docker/Dockerfile
platforms: linux/amd64,linux/arm64
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_USERNAME }}/${{ steps.meta.outputs.REPO_NAME }}:latest
${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_USERNAME }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
- name: WeChat Work notification
uses: seepine/action-wechat-work@master
env:
WECHAT_WORK_BOT_WEBHOOK: ${{ secrets.WECHAT_WORK_BOT_WEBHOOK }}
if: ${{ env.WECHAT_WORK_BOT_WEBHOOK != '' }}
with:
msgtype: markdown
content: "${{ steps.meta.outputs.REPO_NAME }} build docker image success.\n
> Tag: ${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_USERNAME }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}"

View File

@ -0,0 +1,76 @@
name: Checks
on:
- pull_request
jobs:
setup:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 1
- name: Setup Pnpm and Install
uses: seepine/action-setup-pnpm@v1
eslint:
needs: setup
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 1
- name: Setup Pnpm and Install
uses: seepine/action-setup-pnpm@v1
- name: Eslint Test
run: npx eslint --ext ".vue,.js,.jsx,.ts,.tsx" src/ --max-warnings=0
tslint:
needs: setup
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 1
- name: Setup Pnpm and Install
uses: seepine/action-setup-pnpm@v1
- name: Tslint Test
run: pnpm type-check
commit-lint:
needs: setup
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 10
- name: Setup Pnpm and Install
uses: seepine/action-setup-pnpm@v1
- name: Commit lint
run: npx commitlint --to HEAD --verbose
build:
needs: setup
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 1
- name: Setup Pnpm and Install
uses: seepine/action-setup-pnpm@v1
- name: Build
run: pnpm build

29
.gitignore vendored Normal file
View File

@ -0,0 +1,29 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
auto-imports.d.ts
components.d.ts
yarn.lock
package-lock.lock

8
.hintrc Normal file
View File

@ -0,0 +1,8 @@
{
"extends": [
"development"
],
"hints": {
"meta-viewport": "off"
}
}

4
.husky/commit-msg Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install commitlint --edit

4
.husky/pre-commit Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

1
.npmrc Normal file
View File

@ -0,0 +1 @@
registry=https://registry.npmmirror.com

11
.prettierrc Normal file
View File

@ -0,0 +1,11 @@
{
"useTabs": false,
"tabWidth": 2,
"printWidth": 100,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"semi": false,
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindConfig": "./tailwind.config.js"
}

22
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,22 @@
{
"recommendations": [
// volar
"Vue.volar",
// editor
"EditorConfig.EditorConfig",
// prettier
"esbenp.prettier-vscode",
// eslint
"dbaeumer.vscode-eslint",
// package
"ravenq.vscode-goto-node-modules",
// html
"formulahendry.auto-rename-tag",
// git
"mhutchie.git-graph",
// tailwindcss
"bradlc.vscode-tailwindcss",
//
"oderwat.indent-rainbow"
]
}

44
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,44 @@
{
// eslint
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
//
"emmet.showExpandedAbbreviation": "never",
//
"editor.minimap.enabled": false,
//
"editor.wordWrap": "on",
//
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
// package.json
"package.json": ".*, index.html, yarn.lock, *.js, *.ts, *.json, *.sh"
},
//
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/Thumbs.db": true,
".husky": true
},
//
"indentRainbow.colors": [
"rgba(245, 63, 63, 0.07)",
"rgba(255, 125, 0, 0.07)",
"rgba(247, 186, 30, 0.07)",
"rgba(250, 220, 25, 0.07)",
"rgba(159, 219, 29, 0.07)",
"rgba(0, 180, 42, 0.07)",
"rgba(20, 201, 201, 0.07)",
"rgba(52, 145, 250, 0.07)",
"rgba(22, 93, 255, 0.07)",
"rgba(114, 46, 209, 0.07)",
"rgba(217, 26, 217, 0.07)",
"rgba(245, 49, 157, 0.07)"
]
}

172
README.md Normal file
View File

@ -0,0 +1,172 @@
# FxBootUi
## 一、开发准备
### 1.开发工具VsCode
### 2.安装插件
打开项目后右下角会有建议安装,选择安装即可,也可自己一个个搜索安装
- Vue.volar
- EditorConfig.EditorConfig
- esbenp.prettier-vscode
- dbaeumer.vscode-eslint
- ravenq.vscode-goto-node-modules
- formulahendry.auto-rename-tag
- mhutchie.git-graph
## 二、快速入门
### 1.修改项目信息
配置根目录的`package.json`,修改项目名和版本号
```json
{
"name": "fxboot-ui",
"version": "0.1.0"
}
```
### 2.安装依赖
```shell
# 若未安装pnpm请先执行npm i -g pnpm
pnpm i
```
### 3.运行
```shell
pnpm dev
```
## 三、代码管理
### 1.配置用户名和邮箱
将以下命令中 `yourName``your@email.com` 替换为你的用户名和邮箱并执行
```shell
git config user.name yourName
git config user.email your@email.com
```
### 2.非 windows 系统可能需要先执行命令
```shell
chmod 755 .husky/pre-commit
chmod 755 .husky/commit-msg
```
### 3.保存变更文件
使用 `git add xxx` 或 VsCode 左侧的源代码管理面板提交变更文件
### 4.提交代码
执行命令后,按提示输入内容即可
```shell
pnpm cz
```
### 5.推送到仓库
```shell
git push
```
### 6.查看提交记录
通过`菜单栏->查看->命令面板`(或快捷键打开也可),输入`git log`,选择建议项`Git Graph: View Git Graph (git log)`即可
## 四、部署
### 1.配置镜像
配置根目录的 `deploy.sh` 文件,修改 `HUB` 为你的镜像地址
```shell
HUB=registry.cn-hangzhou.aliyuncs.com/rsjst
```
### 2.打包
```shell
sh deploy.sh
```
### 3.部署
配置 dockerSwarm 的 stack 中的 image 为打包的镜像即可,例如
```yml
version: '3.7'
services:
fx-boot-ui:
image: registry.cn-hangzhou.aliyuncs.com/rsjst/fx-boot-ui:0.1.0
environment:
# 配置为服务器的ip地址:后端端口
PROXY_PASS: http://192.168.1.1:8020/
ports:
# 映射为4000端口
- 4000:80
# 下述配置实现热更新,不需要可去除
deploy:
mode: replicated
replicas: 1
update_config:
delay: 5s
order: start-first
```
### 4.配置 nginx
配置服务器上的nginx代理转发到前端端口
```nginx
location / {
proxy_pass http://127.0.0.1:4000/;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
```
## 五、技术要点
Vue 3 + Typescript + Vite
- 编程语言TypeScript 4.x + JavaScript
- 构建工具Vite 3.x
- 前端框架Vue 3.x
- 路由工具Vue Router 4.x
- 状态管理Pinia
- UI 组件库Arco
- UI 组件库Crco
- CSS 预编译Sass
- HTTP 工具Axios
- Git Hook 工具husky + lint-staged
- 代码规范EditorConfig + Prettier + ESLint + Airbnb JavaScript Style Guide
- 提交规范Commitizen + Commitlint
## 六、相关文档
### 1.Vue3.X
[快速上手Vue.js](https://cn.vuejs.org/guide/quick-start.html)
### 2.Pinia
[Pinia](https://pinia.vuejs.org/)
[Pinia Demo](./src/store/README.md)
### 3.Arco
[Arco 组件库](https://arco.design/vue/component/button)
### 4.Crco
[Crco 组件库](https://crco.seepine.com)
或从 `/src/views` 中任意子目录中,皆可查看 `crco` 的应用案例,例如 `/src/views/recruit/resume`

26
commitlint.config.js Normal file
View File

@ -0,0 +1,26 @@
module.exports = {
extends: [
'@commitlint/config-conventional'
// 'gitmoji'
],
rules: {
'type-enum': [
2,
'always',
[
'build',
'chore',
'ci',
'docs',
'feat',
'fix',
'perf',
'refactor',
'revert',
'style',
'test',
'improvement'
]
]
}
}

18
deploy.sh Normal file
View File

@ -0,0 +1,18 @@
run() {
echo "[RUN] " $*
$*
if [ $? -ne 0 ]; then
echo "[ERROR] fail"
exit 1
fi
}
run npm run pre
run npm run build
HUB=registry.cn-hangzhou.aliyuncs.com/rsjst
NAME=$(cat package.json | grep "name" | sed 's/:/\n/g' | sed '1d' | sed 's/}//g' | sed 's/ //g' | sed 's/,//g' | sed 's/"//g')
VERSION=$(cat package.json | grep "version" | sed 's/:/\n/g' | sed '1d' | sed 's/}//g' | sed 's/ //g' | sed 's/,//g' | sed 's/"//g')
run docker buildx build -t $HUB/$NAME:$VERSION --platform=linux/amd64,linux/arm64 -f ./docker/Dockerfile . --push
run echo build and push $HUB/$NAME:$VERSION success

16
docker/Dockerfile Normal file
View File

@ -0,0 +1,16 @@
FROM git.zgfxrc.cn/registry/nginx:1.25-alpine-slim
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories \
&& apk add --no-cache tzdata \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apk del tzdata \
&& echo "*/30 * * * * ntpd -d -q -n -p ntp.aliyun.com" >> /etc/crontabs/root
ENV LISTEN_PORT=80\
SERVER_NAME=localhost\
PROXY_PASS=http://127.0.0.1
ADD ./docker/ui.conf /etc/nginx/templates/default.conf.template
COPY ./dist/ /html
RUN chmod -R 755 /html

31
docker/ui.conf Normal file
View File

@ -0,0 +1,31 @@
server {
listen ${LISTEN_PORT};
server_name ${SERVER_NAME};
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_comp_level 2;
gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml;
gzip_vary on;
gzip_proxied expired no-cache no-store private auth;
gzip_disable "MSIE [1-6]\.";
client_max_body_size 4000m;
client_header_buffer_size 32k;
large_client_header_buffers 4 32k;
location /webapi/ {
proxy_pass ${PROXY_PASS};
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto http;
}
location / {
root /html;
index index.html;
try_files $uri $uri/ /index.html;
}
}

277
index.html Normal file
View File

@ -0,0 +1,277 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
/>
<title>FxBoot</title>
<link rel="stylesheet" href="/public/font-awesome-4.7.0/css/font-awesome.min.css" />
<link rel="stylesheet" href="/public/iconfont/iconfont.css">
<!-- <script src="https://cdn.bootcss.com/vConsole/3.2.0/vconsole.min.js"></script> -->
<script>
// let vConsole = new VConsole()
</script>
<style>
#loader-wrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999999;
}
#loader {
display: block;
position: relative;
left: 50%;
top: 42%;
width: 50px;
height: 50px;
margin: -25px 0 0 -25px;
border-radius: 50%;
border: 3px solid transparent;
/* COLOR 1 */
border-top-color: #165dff;
-webkit-animation: spin 2s linear infinite;
/* Chrome, Opera 15+, Safari 5+ */
-ms-animation: spin 2s linear infinite;
/* Chrome, Opera 15+, Safari 5+ */
-moz-animation: spin 2s linear infinite;
/* Chrome, Opera 15+, Safari 5+ */
-o-animation: spin 2s linear infinite;
/* Chrome, Opera 15+, Safari 5+ */
animation: spin 2s linear infinite;
/* Chrome, Firefox 16+, IE 10+, Opera */
z-index: 1001;
}
#loader:before {
content: '';
position: absolute;
top: 5px;
left: 5px;
right: 5px;
bottom: 5px;
border-radius: 50%;
border: 3px solid transparent;
/* COLOR 2 */
border-top-color: #165dff;
-webkit-animation: spin 3s linear infinite;
/* Chrome, Opera 15+, Safari 5+ */
-moz-animation: spin 3s linear infinite;
/* Chrome, Opera 15+, Safari 5+ */
-o-animation: spin 3s linear infinite;
/* Chrome, Opera 15+, Safari 5+ */
-ms-animation: spin 3s linear infinite;
/* Chrome, Opera 15+, Safari 5+ */
animation: spin 3s linear infinite;
/* Chrome, Firefox 16+, IE 10+, Opera */
}
#loader:after {
content: '';
position: absolute;
top: 15px;
left: 15px;
right: 15px;
bottom: 15px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #165dff;
/* COLOR 3 */
-moz-animation: spin 1.5s linear infinite;
/* Chrome, Opera 15+, Safari 5+ */
-o-animation: spin 1.5s linear infinite;
/* Chrome, Opera 15+, Safari 5+ */
-ms-animation: spin 1.5s linear infinite;
/* Chrome, Opera 15+, Safari 5+ */
-webkit-animation: spin 1.5s linear infinite;
/* Chrome, Opera 15+, Safari 5+ */
animation: spin 1.5s linear infinite;
/* Chrome, Firefox 16+, IE 10+, Opera */
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
/* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: rotate(0deg);
/* IE 9 */
transform: rotate(0deg);
/* Firefox 16+, IE 10+, Opera */
}
100% {
-webkit-transform: rotate(360deg);
/* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: rotate(360deg);
/* IE 9 */
transform: rotate(360deg);
/* Firefox 16+, IE 10+, Opera */
}
}
@keyframes spin {
0% {
-webkit-transform: rotate(0deg);
/* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: rotate(0deg);
/* IE 9 */
transform: rotate(0deg);
/* Firefox 16+, IE 10+, Opera */
}
100% {
-webkit-transform: rotate(360deg);
/* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: rotate(360deg);
/* IE 9 */
transform: rotate(360deg);
/* Firefox 16+, IE 10+, Opera */
}
}
#loader-wrapper .loader-section {
position: fixed;
top: 0;
width: 51%;
height: 100%;
background: #fff;
/* Old browsers */
z-index: 1000;
-webkit-transform: translateX(0);
/* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: translateX(0);
/* IE 9 */
transform: translateX(0);
/* Firefox 16+, IE 10+, Opera */
}
#loader-wrapper .loader-section.section-left {
left: 0;
}
#loader-wrapper .loader-section.section-right {
right: 0;
}
/* Loaded */
.loaded #loader-wrapper .loader-section.section-left {
-webkit-transform: translateX(-100%);
/* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: translateX(-100%);
/* IE 9 */
transform: translateX(-100%);
/* Firefox 16+, IE 10+, Opera */
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.loaded #loader-wrapper .loader-section.section-right {
-webkit-transform: translateX(100%);
/* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: translateX(100%);
/* IE 9 */
transform: translateX(100%);
/* Firefox 16+, IE 10+, Opera */
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.loaded #loader {
opacity: 0;
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
.loaded #loader-wrapper {
visibility: hidden;
-webkit-transform: translateY(-100%);
/* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: translateY(-100%);
/* IE 9 */
transform: translateY(-100%);
/* Firefox 16+, IE 10+, Opera */
-webkit-transition: all 0.3s 1s ease-out;
transition: all 0.3s 1s ease-out;
}
/* JavaScript Turned Off */
.no-js #loader-wrapper {
display: none;
}
.no-js h1 {
color: #222222;
}
#loader-wrapper .load_title {
font-family: 'Open Sans', serif;
color: #165dff;
font-size: 19px;
width: 100%;
text-align: center;
z-index: 9999999999999;
position: absolute;
top: 48%;
opacity: 1;
line-height: 30px;
}
#loader-wrapper .load_title span {
font-weight: normal;
font-style: italic;
font-size: 13px;
color: #165dff;
opacity: 0.5;
}
</style>
</head>
<body>
<noscript>
<strong>您的浏览器未开启或不支持JavaScript.</strong>
</noscript>
<div id="app">
<div id="loader-wrapper">
<div id="loader"></div>
<div class="loader-section section-left"></div>
<div class="loader-section section-right"></div>
<div class="load_title">FxBoot</div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
<script>
window.onload = function () {
var lastTouchEnd = 0
document.addEventListener('touchstart', function (event) {
if (event.touches.length > 1) {
event.preventDefault()
}
})
document.addEventListener(
'touchend',
function (event) {
var now = new Date().getTime()
if (now - lastTouchEnd <= 300) {
event.preventDefault()
}
lastTouchEnd = now
},
false
)
document.addEventListener('gesturestart', function (event) {
event.preventDefault()
})
document.addEventListener('dblclick', function (event) {
event.preventDefault()
})
}
</script>
</html>

71
package.json Normal file
View File

@ -0,0 +1,71 @@
{
"name": "fxboot-ui",
"version": "0.1.0",
"private": true,
"license": "ISC",
"packageManager": "pnpm@9.7.0",
"scripts": {
"dev": "vite",
"build": "vite optimize && vite build",
"type-check": "vite optimize && vue-tsc --noEmit --project tsconfig.json --strict",
"serve": "vite preview",
"prepare": "husky install",
"commit": "npx cz-customizable",
"cz": "npx cz-customizable"
},
"dependencies": {
"@icon-park/vue-next": "^1.4.2",
"@vueuse/core": "^9.13.0",
"axios": "^1.6.8",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.3",
"decimal.js": "^10.4.3",
"encryptlong": "^3.1.4",
"jsencrypt": "^3.2.1",
"lodash": "^4.17.21",
"pinia": "~2.0.14",
"qs": "~6.10.1",
"vue": "~3.2.37",
"vue-router": "~4.0.16",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@arco-design/web-vue": "~2.55.1",
"@commitlint/cli": "~17.3.0",
"@commitlint/config-conventional": "~17.3.0",
"@iconify/json": "^2.2.144",
"@types/lodash": "^4.14.180",
"@types/node": "~16.11.11",
"@typescript-eslint/eslint-plugin": "~4.33.0",
"@typescript-eslint/parser": "~4.33.0",
"@vitejs/plugin-vue": "^3.0.0",
"@vitejs/plugin-vue-jsx": "^2.0.0",
"autoprefixer": "^10.4.13",
"crco": "2.9.14",
"cz-customizable": "~7.0.0",
"eslint": "~7.32.0",
"eslint-config-airbnb-base": "~14.2.1",
"eslint-config-prettier": "~8.3.0",
"eslint-plugin-import": "~2.25.3",
"eslint-plugin-prettier": "~4.0.0",
"eslint-plugin-vue": "~7.20.0",
"husky": "~8.0.2",
"less": "^4.1.2",
"lint-staged": "~12.3.7",
"postcss": "^8.4.21",
"prettier": "~2.6.0",
"prettier-plugin-tailwindcss": "^0.2.4",
"sass": "~1.49.9",
"tailwindcss": "^3.2.7",
"terser": "^5.14.2",
"typescript": "~4.4.4",
"unplugin-auto-import": "^0.16.7",
"unplugin-vue-components": "^0.25.2",
"vite": "^3.0.0",
"vite-plugin-compression": "^0.5.1",
"vue-tsc": "~0.3.0"
},
"lint-staged": {
"*.{vue,js,jsx,ts,tsx}": "eslint --fix"
}
}

6477
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}

BIN
public/bg.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

BIN
public/bg_small.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,7 @@
I hope you love Font Awesome. If you've found it useful, please do me a favor and check out my latest project,
Fort Awesome (https://fortawesome.com). It makes it easy to put the perfect icons on your website. Choose from our awesome,
comprehensive icon sets or copy and paste your own.
Please. Check it out.
-Dave Gandy

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 434 KiB

View File

@ -0,0 +1,34 @@
// Animated Icons
// --------------------------
.@{fa-css-prefix}-spin {
-webkit-animation: fa-spin 2s infinite linear;
animation: fa-spin 2s infinite linear;
}
.@{fa-css-prefix}-pulse {
-webkit-animation: fa-spin 1s infinite steps(8);
animation: fa-spin 1s infinite steps(8);
}
@-webkit-keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}

View File

@ -0,0 +1,41 @@
// Bordered & Pulled
// -------------------------
.@{fa-css-prefix}-border {
padding: 0.2em 0.25em 0.15em;
border: solid 0.08em @fa-border-color;
border-radius: 0.1em;
}
.@{fa-css-prefix}-pull-left {
float: left;
}
.@{fa-css-prefix}-pull-right {
float: right;
}
.@{fa-css-prefix} {
&.@{fa-css-prefix}-pull-left {
margin-right: 0.3em;
}
&.@{fa-css-prefix}-pull-right {
margin-left: 0.3em;
}
}
/* Deprecated as of 4.4.0 */
.pull-right {
float: right;
}
.pull-left {
float: left;
}
.@{fa-css-prefix} {
&.pull-left {
margin-right: 0.3em;
}
&.pull-right {
margin-left: 0.3em;
}
}

View File

@ -0,0 +1,11 @@
// Base Class Definition
// -------------------------
.@{fa-css-prefix} {
display: inline-block;
font: normal normal normal @fa-font-size-base / @fa-line-height-base FontAwesome; // shortening font declaration
font-size: inherit; // can't have font-size inherit on line above, so need to override
text-rendering: auto; // optimizelegibility throws things off #1094
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -0,0 +1,6 @@
// Fixed Width Icons
// -------------------------
.@{fa-css-prefix}-fw {
width: (18em / 14);
text-align: center;
}

View File

@ -0,0 +1,18 @@
/*!
* Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
* License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
*/
@import 'variables.less';
@import 'mixins.less';
@import 'path.less';
@import 'core.less';
@import 'larger.less';
@import 'fixed-width.less';
@import 'list.less';
@import 'bordered-pulled.less';
@import 'animated.less';
@import 'rotated-flipped.less';
@import 'stacked.less';
@import 'icons.less';
@import 'screen-reader.less';

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
// Icon Sizes
// -------------------------
/* makes the font 33% larger relative to the icon container */
.@{fa-css-prefix}-lg {
font-size: (4em / 3);
line-height: (3em / 4);
vertical-align: -15%;
}
.@{fa-css-prefix}-2x {
font-size: 2em;
}
.@{fa-css-prefix}-3x {
font-size: 3em;
}
.@{fa-css-prefix}-4x {
font-size: 4em;
}
.@{fa-css-prefix}-5x {
font-size: 5em;
}

View File

@ -0,0 +1,21 @@
// List Icons
// -------------------------
.@{fa-css-prefix}-ul {
padding-left: 0;
margin-left: @fa-li-width;
list-style-type: none;
> li {
position: relative;
}
}
.@{fa-css-prefix}-li {
position: absolute;
left: -@fa-li-width;
width: @fa-li-width;
top: (2em / 14);
text-align: center;
&.@{fa-css-prefix}-lg {
left: (-@fa-li-width + (4em / 14));
}
}

View File

@ -0,0 +1,58 @@
// Mixins
// --------------------------
.fa-icon() {
display: inline-block;
font: normal normal normal @fa-font-size-base / @fa-line-height-base FontAwesome; // shortening font declaration
font-size: inherit; // can't have font-size inherit on line above, so need to override
text-rendering: auto; // optimizelegibility throws things off #1094
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.fa-icon-rotate(@degrees, @rotation) {
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})';
-webkit-transform: rotate(@degrees);
-ms-transform: rotate(@degrees);
transform: rotate(@degrees);
}
.fa-icon-flip(@horiz, @vert, @rotation) {
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)';
-webkit-transform: scale(@horiz, @vert);
-ms-transform: scale(@horiz, @vert);
transform: scale(@horiz, @vert);
}
// Only display content to screen readers. A la Bootstrap 4.
//
// See: http://a11yproject.com/posts/how-to-hide-content/
.sr-only() {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
// Use in conjunction with .sr-only to only display content when it's focused.
//
// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
//
// Credit: HTML5 Boilerplate
.sr-only-focusable() {
&:active,
&:focus {
position: static;
width: auto;
height: auto;
margin: 0;
overflow: visible;
clip: auto;
}
}

View File

@ -0,0 +1,16 @@
/* FONT PATH
* -------------------------- */
@font-face {
font-family: 'FontAwesome';
src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}');
src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}')
format('embedded-opentype'),
url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'),
url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'),
url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'),
url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg');
// src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts
font-weight: normal;
font-style: normal;
}

View File

@ -0,0 +1,30 @@
// Rotated & Flipped Icons
// -------------------------
.@{fa-css-prefix}-rotate-90 {
.fa-icon-rotate(90deg, 1);
}
.@{fa-css-prefix}-rotate-180 {
.fa-icon-rotate(180deg, 2);
}
.@{fa-css-prefix}-rotate-270 {
.fa-icon-rotate(270deg, 3);
}
.@{fa-css-prefix}-flip-horizontal {
.fa-icon-flip(-1, 1, 0);
}
.@{fa-css-prefix}-flip-vertical {
.fa-icon-flip(1, -1, 2);
}
// Hook for IE8-9
// -------------------------
:root .@{fa-css-prefix}-rotate-90,
:root .@{fa-css-prefix}-rotate-180,
:root .@{fa-css-prefix}-rotate-270,
:root .@{fa-css-prefix}-flip-horizontal,
:root .@{fa-css-prefix}-flip-vertical {
filter: none;
}

View File

@ -0,0 +1,9 @@
// Screen Readers
// -------------------------
.sr-only {
.sr-only();
}
.sr-only-focusable {
.sr-only-focusable();
}

View File

@ -0,0 +1,27 @@
// Stacked Icons
// -------------------------
.@{fa-css-prefix}-stack {
position: relative;
display: inline-block;
width: 2em;
height: 2em;
line-height: 2em;
vertical-align: middle;
}
.@{fa-css-prefix}-stack-1x,
.@{fa-css-prefix}-stack-2x {
position: absolute;
left: 0;
width: 100%;
text-align: center;
}
.@{fa-css-prefix}-stack-1x {
line-height: inherit;
}
.@{fa-css-prefix}-stack-2x {
font-size: 2em;
}
.@{fa-css-prefix}-inverse {
color: @fa-inverse;
}

View File

@ -0,0 +1,799 @@
// Variables
// --------------------------
@fa-font-path: '../fonts';
@fa-font-size-base: 14px;
@fa-line-height-base: 1;
//@fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.7.0/fonts"; // for referencing Bootstrap CDN font files directly
@fa-css-prefix: fa;
@fa-version: '4.7.0';
@fa-border-color: #eee;
@fa-inverse: #fff;
@fa-li-width: (30em / 14);
@fa-var-500px: '\f26e';
@fa-var-address-book: '\f2b9';
@fa-var-address-book-o: '\f2ba';
@fa-var-address-card: '\f2bb';
@fa-var-address-card-o: '\f2bc';
@fa-var-adjust: '\f042';
@fa-var-adn: '\f170';
@fa-var-align-center: '\f037';
@fa-var-align-justify: '\f039';
@fa-var-align-left: '\f036';
@fa-var-align-right: '\f038';
@fa-var-amazon: '\f270';
@fa-var-ambulance: '\f0f9';
@fa-var-american-sign-language-interpreting: '\f2a3';
@fa-var-anchor: '\f13d';
@fa-var-android: '\f17b';
@fa-var-angellist: '\f209';
@fa-var-angle-double-down: '\f103';
@fa-var-angle-double-left: '\f100';
@fa-var-angle-double-right: '\f101';
@fa-var-angle-double-up: '\f102';
@fa-var-angle-down: '\f107';
@fa-var-angle-left: '\f104';
@fa-var-angle-right: '\f105';
@fa-var-angle-up: '\f106';
@fa-var-apple: '\f179';
@fa-var-archive: '\f187';
@fa-var-area-chart: '\f1fe';
@fa-var-arrow-circle-down: '\f0ab';
@fa-var-arrow-circle-left: '\f0a8';
@fa-var-arrow-circle-o-down: '\f01a';
@fa-var-arrow-circle-o-left: '\f190';
@fa-var-arrow-circle-o-right: '\f18e';
@fa-var-arrow-circle-o-up: '\f01b';
@fa-var-arrow-circle-right: '\f0a9';
@fa-var-arrow-circle-up: '\f0aa';
@fa-var-arrow-down: '\f063';
@fa-var-arrow-left: '\f060';
@fa-var-arrow-right: '\f061';
@fa-var-arrow-up: '\f062';
@fa-var-arrows: '\f047';
@fa-var-arrows-alt: '\f0b2';
@fa-var-arrows-h: '\f07e';
@fa-var-arrows-v: '\f07d';
@fa-var-asl-interpreting: '\f2a3';
@fa-var-assistive-listening-systems: '\f2a2';
@fa-var-asterisk: '\f069';
@fa-var-at: '\f1fa';
@fa-var-audio-description: '\f29e';
@fa-var-automobile: '\f1b9';
@fa-var-backward: '\f04a';
@fa-var-balance-scale: '\f24e';
@fa-var-ban: '\f05e';
@fa-var-bandcamp: '\f2d5';
@fa-var-bank: '\f19c';
@fa-var-bar-chart: '\f080';
@fa-var-bar-chart-o: '\f080';
@fa-var-barcode: '\f02a';
@fa-var-bars: '\f0c9';
@fa-var-bath: '\f2cd';
@fa-var-bathtub: '\f2cd';
@fa-var-battery: '\f240';
@fa-var-battery-0: '\f244';
@fa-var-battery-1: '\f243';
@fa-var-battery-2: '\f242';
@fa-var-battery-3: '\f241';
@fa-var-battery-4: '\f240';
@fa-var-battery-empty: '\f244';
@fa-var-battery-full: '\f240';
@fa-var-battery-half: '\f242';
@fa-var-battery-quarter: '\f243';
@fa-var-battery-three-quarters: '\f241';
@fa-var-bed: '\f236';
@fa-var-beer: '\f0fc';
@fa-var-behance: '\f1b4';
@fa-var-behance-square: '\f1b5';
@fa-var-bell: '\f0f3';
@fa-var-bell-o: '\f0a2';
@fa-var-bell-slash: '\f1f6';
@fa-var-bell-slash-o: '\f1f7';
@fa-var-bicycle: '\f206';
@fa-var-binoculars: '\f1e5';
@fa-var-birthday-cake: '\f1fd';
@fa-var-bitbucket: '\f171';
@fa-var-bitbucket-square: '\f172';
@fa-var-bitcoin: '\f15a';
@fa-var-black-tie: '\f27e';
@fa-var-blind: '\f29d';
@fa-var-bluetooth: '\f293';
@fa-var-bluetooth-b: '\f294';
@fa-var-bold: '\f032';
@fa-var-bolt: '\f0e7';
@fa-var-bomb: '\f1e2';
@fa-var-book: '\f02d';
@fa-var-bookmark: '\f02e';
@fa-var-bookmark-o: '\f097';
@fa-var-braille: '\f2a1';
@fa-var-briefcase: '\f0b1';
@fa-var-btc: '\f15a';
@fa-var-bug: '\f188';
@fa-var-building: '\f1ad';
@fa-var-building-o: '\f0f7';
@fa-var-bullhorn: '\f0a1';
@fa-var-bullseye: '\f140';
@fa-var-bus: '\f207';
@fa-var-buysellads: '\f20d';
@fa-var-cab: '\f1ba';
@fa-var-calculator: '\f1ec';
@fa-var-calendar: '\f073';
@fa-var-calendar-check-o: '\f274';
@fa-var-calendar-minus-o: '\f272';
@fa-var-calendar-o: '\f133';
@fa-var-calendar-plus-o: '\f271';
@fa-var-calendar-times-o: '\f273';
@fa-var-camera: '\f030';
@fa-var-camera-retro: '\f083';
@fa-var-car: '\f1b9';
@fa-var-caret-down: '\f0d7';
@fa-var-caret-left: '\f0d9';
@fa-var-caret-right: '\f0da';
@fa-var-caret-square-o-down: '\f150';
@fa-var-caret-square-o-left: '\f191';
@fa-var-caret-square-o-right: '\f152';
@fa-var-caret-square-o-up: '\f151';
@fa-var-caret-up: '\f0d8';
@fa-var-cart-arrow-down: '\f218';
@fa-var-cart-plus: '\f217';
@fa-var-cc: '\f20a';
@fa-var-cc-amex: '\f1f3';
@fa-var-cc-diners-club: '\f24c';
@fa-var-cc-discover: '\f1f2';
@fa-var-cc-jcb: '\f24b';
@fa-var-cc-mastercard: '\f1f1';
@fa-var-cc-paypal: '\f1f4';
@fa-var-cc-stripe: '\f1f5';
@fa-var-cc-visa: '\f1f0';
@fa-var-certificate: '\f0a3';
@fa-var-chain: '\f0c1';
@fa-var-chain-broken: '\f127';
@fa-var-check: '\f00c';
@fa-var-check-circle: '\f058';
@fa-var-check-circle-o: '\f05d';
@fa-var-check-square: '\f14a';
@fa-var-check-square-o: '\f046';
@fa-var-chevron-circle-down: '\f13a';
@fa-var-chevron-circle-left: '\f137';
@fa-var-chevron-circle-right: '\f138';
@fa-var-chevron-circle-up: '\f139';
@fa-var-chevron-down: '\f078';
@fa-var-chevron-left: '\f053';
@fa-var-chevron-right: '\f054';
@fa-var-chevron-up: '\f077';
@fa-var-child: '\f1ae';
@fa-var-chrome: '\f268';
@fa-var-circle: '\f111';
@fa-var-circle-o: '\f10c';
@fa-var-circle-o-notch: '\f1ce';
@fa-var-circle-thin: '\f1db';
@fa-var-clipboard: '\f0ea';
@fa-var-clock-o: '\f017';
@fa-var-clone: '\f24d';
@fa-var-close: '\f00d';
@fa-var-cloud: '\f0c2';
@fa-var-cloud-download: '\f0ed';
@fa-var-cloud-upload: '\f0ee';
@fa-var-cny: '\f157';
@fa-var-code: '\f121';
@fa-var-code-fork: '\f126';
@fa-var-codepen: '\f1cb';
@fa-var-codiepie: '\f284';
@fa-var-coffee: '\f0f4';
@fa-var-cog: '\f013';
@fa-var-cogs: '\f085';
@fa-var-columns: '\f0db';
@fa-var-comment: '\f075';
@fa-var-comment-o: '\f0e5';
@fa-var-commenting: '\f27a';
@fa-var-commenting-o: '\f27b';
@fa-var-comments: '\f086';
@fa-var-comments-o: '\f0e6';
@fa-var-compass: '\f14e';
@fa-var-compress: '\f066';
@fa-var-connectdevelop: '\f20e';
@fa-var-contao: '\f26d';
@fa-var-copy: '\f0c5';
@fa-var-copyright: '\f1f9';
@fa-var-creative-commons: '\f25e';
@fa-var-credit-card: '\f09d';
@fa-var-credit-card-alt: '\f283';
@fa-var-crop: '\f125';
@fa-var-crosshairs: '\f05b';
@fa-var-css3: '\f13c';
@fa-var-cube: '\f1b2';
@fa-var-cubes: '\f1b3';
@fa-var-cut: '\f0c4';
@fa-var-cutlery: '\f0f5';
@fa-var-dashboard: '\f0e4';
@fa-var-dashcube: '\f210';
@fa-var-database: '\f1c0';
@fa-var-deaf: '\f2a4';
@fa-var-deafness: '\f2a4';
@fa-var-dedent: '\f03b';
@fa-var-delicious: '\f1a5';
@fa-var-desktop: '\f108';
@fa-var-deviantart: '\f1bd';
@fa-var-diamond: '\f219';
@fa-var-digg: '\f1a6';
@fa-var-dollar: '\f155';
@fa-var-dot-circle-o: '\f192';
@fa-var-download: '\f019';
@fa-var-dribbble: '\f17d';
@fa-var-drivers-license: '\f2c2';
@fa-var-drivers-license-o: '\f2c3';
@fa-var-dropbox: '\f16b';
@fa-var-drupal: '\f1a9';
@fa-var-edge: '\f282';
@fa-var-edit: '\f044';
@fa-var-eercast: '\f2da';
@fa-var-eject: '\f052';
@fa-var-ellipsis-h: '\f141';
@fa-var-ellipsis-v: '\f142';
@fa-var-empire: '\f1d1';
@fa-var-envelope: '\f0e0';
@fa-var-envelope-o: '\f003';
@fa-var-envelope-open: '\f2b6';
@fa-var-envelope-open-o: '\f2b7';
@fa-var-envelope-square: '\f199';
@fa-var-envira: '\f299';
@fa-var-eraser: '\f12d';
@fa-var-etsy: '\f2d7';
@fa-var-eur: '\f153';
@fa-var-euro: '\f153';
@fa-var-exchange: '\f0ec';
@fa-var-exclamation: '\f12a';
@fa-var-exclamation-circle: '\f06a';
@fa-var-exclamation-triangle: '\f071';
@fa-var-expand: '\f065';
@fa-var-expeditedssl: '\f23e';
@fa-var-external-link: '\f08e';
@fa-var-external-link-square: '\f14c';
@fa-var-eye: '\f06e';
@fa-var-eye-slash: '\f070';
@fa-var-eyedropper: '\f1fb';
@fa-var-fa: '\f2b4';
@fa-var-facebook: '\f09a';
@fa-var-facebook-f: '\f09a';
@fa-var-facebook-official: '\f230';
@fa-var-facebook-square: '\f082';
@fa-var-fast-backward: '\f049';
@fa-var-fast-forward: '\f050';
@fa-var-fax: '\f1ac';
@fa-var-feed: '\f09e';
@fa-var-female: '\f182';
@fa-var-fighter-jet: '\f0fb';
@fa-var-file: '\f15b';
@fa-var-file-archive-o: '\f1c6';
@fa-var-file-audio-o: '\f1c7';
@fa-var-file-code-o: '\f1c9';
@fa-var-file-excel-o: '\f1c3';
@fa-var-file-image-o: '\f1c5';
@fa-var-file-movie-o: '\f1c8';
@fa-var-file-o: '\f016';
@fa-var-file-pdf-o: '\f1c1';
@fa-var-file-photo-o: '\f1c5';
@fa-var-file-picture-o: '\f1c5';
@fa-var-file-powerpoint-o: '\f1c4';
@fa-var-file-sound-o: '\f1c7';
@fa-var-file-text: '\f15c';
@fa-var-file-text-o: '\f0f6';
@fa-var-file-video-o: '\f1c8';
@fa-var-file-word-o: '\f1c2';
@fa-var-file-zip-o: '\f1c6';
@fa-var-files-o: '\f0c5';
@fa-var-film: '\f008';
@fa-var-filter: '\f0b0';
@fa-var-fire: '\f06d';
@fa-var-fire-extinguisher: '\f134';
@fa-var-firefox: '\f269';
@fa-var-first-order: '\f2b0';
@fa-var-flag: '\f024';
@fa-var-flag-checkered: '\f11e';
@fa-var-flag-o: '\f11d';
@fa-var-flash: '\f0e7';
@fa-var-flask: '\f0c3';
@fa-var-flickr: '\f16e';
@fa-var-floppy-o: '\f0c7';
@fa-var-folder: '\f07b';
@fa-var-folder-o: '\f114';
@fa-var-folder-open: '\f07c';
@fa-var-folder-open-o: '\f115';
@fa-var-font: '\f031';
@fa-var-font-awesome: '\f2b4';
@fa-var-fonticons: '\f280';
@fa-var-fort-awesome: '\f286';
@fa-var-forumbee: '\f211';
@fa-var-forward: '\f04e';
@fa-var-foursquare: '\f180';
@fa-var-free-code-camp: '\f2c5';
@fa-var-frown-o: '\f119';
@fa-var-futbol-o: '\f1e3';
@fa-var-gamepad: '\f11b';
@fa-var-gavel: '\f0e3';
@fa-var-gbp: '\f154';
@fa-var-ge: '\f1d1';
@fa-var-gear: '\f013';
@fa-var-gears: '\f085';
@fa-var-genderless: '\f22d';
@fa-var-get-pocket: '\f265';
@fa-var-gg: '\f260';
@fa-var-gg-circle: '\f261';
@fa-var-gift: '\f06b';
@fa-var-git: '\f1d3';
@fa-var-git-square: '\f1d2';
@fa-var-github: '\f09b';
@fa-var-github-alt: '\f113';
@fa-var-github-square: '\f092';
@fa-var-gitlab: '\f296';
@fa-var-gittip: '\f184';
@fa-var-glass: '\f000';
@fa-var-glide: '\f2a5';
@fa-var-glide-g: '\f2a6';
@fa-var-globe: '\f0ac';
@fa-var-google: '\f1a0';
@fa-var-google-plus: '\f0d5';
@fa-var-google-plus-circle: '\f2b3';
@fa-var-google-plus-official: '\f2b3';
@fa-var-google-plus-square: '\f0d4';
@fa-var-google-wallet: '\f1ee';
@fa-var-graduation-cap: '\f19d';
@fa-var-gratipay: '\f184';
@fa-var-grav: '\f2d6';
@fa-var-group: '\f0c0';
@fa-var-h-square: '\f0fd';
@fa-var-hacker-news: '\f1d4';
@fa-var-hand-grab-o: '\f255';
@fa-var-hand-lizard-o: '\f258';
@fa-var-hand-o-down: '\f0a7';
@fa-var-hand-o-left: '\f0a5';
@fa-var-hand-o-right: '\f0a4';
@fa-var-hand-o-up: '\f0a6';
@fa-var-hand-paper-o: '\f256';
@fa-var-hand-peace-o: '\f25b';
@fa-var-hand-pointer-o: '\f25a';
@fa-var-hand-rock-o: '\f255';
@fa-var-hand-scissors-o: '\f257';
@fa-var-hand-spock-o: '\f259';
@fa-var-hand-stop-o: '\f256';
@fa-var-handshake-o: '\f2b5';
@fa-var-hard-of-hearing: '\f2a4';
@fa-var-hashtag: '\f292';
@fa-var-hdd-o: '\f0a0';
@fa-var-header: '\f1dc';
@fa-var-headphones: '\f025';
@fa-var-heart: '\f004';
@fa-var-heart-o: '\f08a';
@fa-var-heartbeat: '\f21e';
@fa-var-history: '\f1da';
@fa-var-home: '\f015';
@fa-var-hospital-o: '\f0f8';
@fa-var-hotel: '\f236';
@fa-var-hourglass: '\f254';
@fa-var-hourglass-1: '\f251';
@fa-var-hourglass-2: '\f252';
@fa-var-hourglass-3: '\f253';
@fa-var-hourglass-end: '\f253';
@fa-var-hourglass-half: '\f252';
@fa-var-hourglass-o: '\f250';
@fa-var-hourglass-start: '\f251';
@fa-var-houzz: '\f27c';
@fa-var-html5: '\f13b';
@fa-var-i-cursor: '\f246';
@fa-var-id-badge: '\f2c1';
@fa-var-id-card: '\f2c2';
@fa-var-id-card-o: '\f2c3';
@fa-var-ils: '\f20b';
@fa-var-image: '\f03e';
@fa-var-imdb: '\f2d8';
@fa-var-inbox: '\f01c';
@fa-var-indent: '\f03c';
@fa-var-industry: '\f275';
@fa-var-info: '\f129';
@fa-var-info-circle: '\f05a';
@fa-var-inr: '\f156';
@fa-var-instagram: '\f16d';
@fa-var-institution: '\f19c';
@fa-var-internet-explorer: '\f26b';
@fa-var-intersex: '\f224';
@fa-var-ioxhost: '\f208';
@fa-var-italic: '\f033';
@fa-var-joomla: '\f1aa';
@fa-var-jpy: '\f157';
@fa-var-jsfiddle: '\f1cc';
@fa-var-key: '\f084';
@fa-var-keyboard-o: '\f11c';
@fa-var-krw: '\f159';
@fa-var-language: '\f1ab';
@fa-var-laptop: '\f109';
@fa-var-lastfm: '\f202';
@fa-var-lastfm-square: '\f203';
@fa-var-leaf: '\f06c';
@fa-var-leanpub: '\f212';
@fa-var-legal: '\f0e3';
@fa-var-lemon-o: '\f094';
@fa-var-level-down: '\f149';
@fa-var-level-up: '\f148';
@fa-var-life-bouy: '\f1cd';
@fa-var-life-buoy: '\f1cd';
@fa-var-life-ring: '\f1cd';
@fa-var-life-saver: '\f1cd';
@fa-var-lightbulb-o: '\f0eb';
@fa-var-line-chart: '\f201';
@fa-var-link: '\f0c1';
@fa-var-linkedin: '\f0e1';
@fa-var-linkedin-square: '\f08c';
@fa-var-linode: '\f2b8';
@fa-var-linux: '\f17c';
@fa-var-list: '\f03a';
@fa-var-list-alt: '\f022';
@fa-var-list-ol: '\f0cb';
@fa-var-list-ul: '\f0ca';
@fa-var-location-arrow: '\f124';
@fa-var-lock: '\f023';
@fa-var-long-arrow-down: '\f175';
@fa-var-long-arrow-left: '\f177';
@fa-var-long-arrow-right: '\f178';
@fa-var-long-arrow-up: '\f176';
@fa-var-low-vision: '\f2a8';
@fa-var-magic: '\f0d0';
@fa-var-magnet: '\f076';
@fa-var-mail-forward: '\f064';
@fa-var-mail-reply: '\f112';
@fa-var-mail-reply-all: '\f122';
@fa-var-male: '\f183';
@fa-var-map: '\f279';
@fa-var-map-marker: '\f041';
@fa-var-map-o: '\f278';
@fa-var-map-pin: '\f276';
@fa-var-map-signs: '\f277';
@fa-var-mars: '\f222';
@fa-var-mars-double: '\f227';
@fa-var-mars-stroke: '\f229';
@fa-var-mars-stroke-h: '\f22b';
@fa-var-mars-stroke-v: '\f22a';
@fa-var-maxcdn: '\f136';
@fa-var-meanpath: '\f20c';
@fa-var-medium: '\f23a';
@fa-var-medkit: '\f0fa';
@fa-var-meetup: '\f2e0';
@fa-var-meh-o: '\f11a';
@fa-var-mercury: '\f223';
@fa-var-microchip: '\f2db';
@fa-var-microphone: '\f130';
@fa-var-microphone-slash: '\f131';
@fa-var-minus: '\f068';
@fa-var-minus-circle: '\f056';
@fa-var-minus-square: '\f146';
@fa-var-minus-square-o: '\f147';
@fa-var-mixcloud: '\f289';
@fa-var-mobile: '\f10b';
@fa-var-mobile-phone: '\f10b';
@fa-var-modx: '\f285';
@fa-var-money: '\f0d6';
@fa-var-moon-o: '\f186';
@fa-var-mortar-board: '\f19d';
@fa-var-motorcycle: '\f21c';
@fa-var-mouse-pointer: '\f245';
@fa-var-music: '\f001';
@fa-var-navicon: '\f0c9';
@fa-var-neuter: '\f22c';
@fa-var-newspaper-o: '\f1ea';
@fa-var-object-group: '\f247';
@fa-var-object-ungroup: '\f248';
@fa-var-odnoklassniki: '\f263';
@fa-var-odnoklassniki-square: '\f264';
@fa-var-opencart: '\f23d';
@fa-var-openid: '\f19b';
@fa-var-opera: '\f26a';
@fa-var-optin-monster: '\f23c';
@fa-var-outdent: '\f03b';
@fa-var-pagelines: '\f18c';
@fa-var-paint-brush: '\f1fc';
@fa-var-paper-plane: '\f1d8';
@fa-var-paper-plane-o: '\f1d9';
@fa-var-paperclip: '\f0c6';
@fa-var-paragraph: '\f1dd';
@fa-var-paste: '\f0ea';
@fa-var-pause: '\f04c';
@fa-var-pause-circle: '\f28b';
@fa-var-pause-circle-o: '\f28c';
@fa-var-paw: '\f1b0';
@fa-var-paypal: '\f1ed';
@fa-var-pencil: '\f040';
@fa-var-pencil-square: '\f14b';
@fa-var-pencil-square-o: '\f044';
@fa-var-percent: '\f295';
@fa-var-phone: '\f095';
@fa-var-phone-square: '\f098';
@fa-var-photo: '\f03e';
@fa-var-picture-o: '\f03e';
@fa-var-pie-chart: '\f200';
@fa-var-pied-piper: '\f2ae';
@fa-var-pied-piper-alt: '\f1a8';
@fa-var-pied-piper-pp: '\f1a7';
@fa-var-pinterest: '\f0d2';
@fa-var-pinterest-p: '\f231';
@fa-var-pinterest-square: '\f0d3';
@fa-var-plane: '\f072';
@fa-var-play: '\f04b';
@fa-var-play-circle: '\f144';
@fa-var-play-circle-o: '\f01d';
@fa-var-plug: '\f1e6';
@fa-var-plus: '\f067';
@fa-var-plus-circle: '\f055';
@fa-var-plus-square: '\f0fe';
@fa-var-plus-square-o: '\f196';
@fa-var-podcast: '\f2ce';
@fa-var-power-off: '\f011';
@fa-var-print: '\f02f';
@fa-var-product-hunt: '\f288';
@fa-var-puzzle-piece: '\f12e';
@fa-var-qq: '\f1d6';
@fa-var-qrcode: '\f029';
@fa-var-question: '\f128';
@fa-var-question-circle: '\f059';
@fa-var-question-circle-o: '\f29c';
@fa-var-quora: '\f2c4';
@fa-var-quote-left: '\f10d';
@fa-var-quote-right: '\f10e';
@fa-var-ra: '\f1d0';
@fa-var-random: '\f074';
@fa-var-ravelry: '\f2d9';
@fa-var-rebel: '\f1d0';
@fa-var-recycle: '\f1b8';
@fa-var-reddit: '\f1a1';
@fa-var-reddit-alien: '\f281';
@fa-var-reddit-square: '\f1a2';
@fa-var-refresh: '\f021';
@fa-var-registered: '\f25d';
@fa-var-remove: '\f00d';
@fa-var-renren: '\f18b';
@fa-var-reorder: '\f0c9';
@fa-var-repeat: '\f01e';
@fa-var-reply: '\f112';
@fa-var-reply-all: '\f122';
@fa-var-resistance: '\f1d0';
@fa-var-retweet: '\f079';
@fa-var-rmb: '\f157';
@fa-var-road: '\f018';
@fa-var-rocket: '\f135';
@fa-var-rotate-left: '\f0e2';
@fa-var-rotate-right: '\f01e';
@fa-var-rouble: '\f158';
@fa-var-rss: '\f09e';
@fa-var-rss-square: '\f143';
@fa-var-rub: '\f158';
@fa-var-ruble: '\f158';
@fa-var-rupee: '\f156';
@fa-var-s15: '\f2cd';
@fa-var-safari: '\f267';
@fa-var-save: '\f0c7';
@fa-var-scissors: '\f0c4';
@fa-var-scribd: '\f28a';
@fa-var-search: '\f002';
@fa-var-search-minus: '\f010';
@fa-var-search-plus: '\f00e';
@fa-var-sellsy: '\f213';
@fa-var-send: '\f1d8';
@fa-var-send-o: '\f1d9';
@fa-var-server: '\f233';
@fa-var-share: '\f064';
@fa-var-share-alt: '\f1e0';
@fa-var-share-alt-square: '\f1e1';
@fa-var-share-square: '\f14d';
@fa-var-share-square-o: '\f045';
@fa-var-shekel: '\f20b';
@fa-var-sheqel: '\f20b';
@fa-var-shield: '\f132';
@fa-var-ship: '\f21a';
@fa-var-shirtsinbulk: '\f214';
@fa-var-shopping-bag: '\f290';
@fa-var-shopping-basket: '\f291';
@fa-var-shopping-cart: '\f07a';
@fa-var-shower: '\f2cc';
@fa-var-sign-in: '\f090';
@fa-var-sign-language: '\f2a7';
@fa-var-sign-out: '\f08b';
@fa-var-signal: '\f012';
@fa-var-signing: '\f2a7';
@fa-var-simplybuilt: '\f215';
@fa-var-sitemap: '\f0e8';
@fa-var-skyatlas: '\f216';
@fa-var-skype: '\f17e';
@fa-var-slack: '\f198';
@fa-var-sliders: '\f1de';
@fa-var-slideshare: '\f1e7';
@fa-var-smile-o: '\f118';
@fa-var-snapchat: '\f2ab';
@fa-var-snapchat-ghost: '\f2ac';
@fa-var-snapchat-square: '\f2ad';
@fa-var-snowflake-o: '\f2dc';
@fa-var-soccer-ball-o: '\f1e3';
@fa-var-sort: '\f0dc';
@fa-var-sort-alpha-asc: '\f15d';
@fa-var-sort-alpha-desc: '\f15e';
@fa-var-sort-amount-asc: '\f160';
@fa-var-sort-amount-desc: '\f161';
@fa-var-sort-asc: '\f0de';
@fa-var-sort-desc: '\f0dd';
@fa-var-sort-down: '\f0dd';
@fa-var-sort-numeric-asc: '\f162';
@fa-var-sort-numeric-desc: '\f163';
@fa-var-sort-up: '\f0de';
@fa-var-soundcloud: '\f1be';
@fa-var-space-shuttle: '\f197';
@fa-var-spinner: '\f110';
@fa-var-spoon: '\f1b1';
@fa-var-spotify: '\f1bc';
@fa-var-square: '\f0c8';
@fa-var-square-o: '\f096';
@fa-var-stack-exchange: '\f18d';
@fa-var-stack-overflow: '\f16c';
@fa-var-star: '\f005';
@fa-var-star-half: '\f089';
@fa-var-star-half-empty: '\f123';
@fa-var-star-half-full: '\f123';
@fa-var-star-half-o: '\f123';
@fa-var-star-o: '\f006';
@fa-var-steam: '\f1b6';
@fa-var-steam-square: '\f1b7';
@fa-var-step-backward: '\f048';
@fa-var-step-forward: '\f051';
@fa-var-stethoscope: '\f0f1';
@fa-var-sticky-note: '\f249';
@fa-var-sticky-note-o: '\f24a';
@fa-var-stop: '\f04d';
@fa-var-stop-circle: '\f28d';
@fa-var-stop-circle-o: '\f28e';
@fa-var-street-view: '\f21d';
@fa-var-strikethrough: '\f0cc';
@fa-var-stumbleupon: '\f1a4';
@fa-var-stumbleupon-circle: '\f1a3';
@fa-var-subscript: '\f12c';
@fa-var-subway: '\f239';
@fa-var-suitcase: '\f0f2';
@fa-var-sun-o: '\f185';
@fa-var-superpowers: '\f2dd';
@fa-var-superscript: '\f12b';
@fa-var-support: '\f1cd';
@fa-var-table: '\f0ce';
@fa-var-tablet: '\f10a';
@fa-var-tachometer: '\f0e4';
@fa-var-tag: '\f02b';
@fa-var-tags: '\f02c';
@fa-var-tasks: '\f0ae';
@fa-var-taxi: '\f1ba';
@fa-var-telegram: '\f2c6';
@fa-var-television: '\f26c';
@fa-var-tencent-weibo: '\f1d5';
@fa-var-terminal: '\f120';
@fa-var-text-height: '\f034';
@fa-var-text-width: '\f035';
@fa-var-th: '\f00a';
@fa-var-th-large: '\f009';
@fa-var-th-list: '\f00b';
@fa-var-themeisle: '\f2b2';
@fa-var-thermometer: '\f2c7';
@fa-var-thermometer-0: '\f2cb';
@fa-var-thermometer-1: '\f2ca';
@fa-var-thermometer-2: '\f2c9';
@fa-var-thermometer-3: '\f2c8';
@fa-var-thermometer-4: '\f2c7';
@fa-var-thermometer-empty: '\f2cb';
@fa-var-thermometer-full: '\f2c7';
@fa-var-thermometer-half: '\f2c9';
@fa-var-thermometer-quarter: '\f2ca';
@fa-var-thermometer-three-quarters: '\f2c8';
@fa-var-thumb-tack: '\f08d';
@fa-var-thumbs-down: '\f165';
@fa-var-thumbs-o-down: '\f088';
@fa-var-thumbs-o-up: '\f087';
@fa-var-thumbs-up: '\f164';
@fa-var-ticket: '\f145';
@fa-var-times: '\f00d';
@fa-var-times-circle: '\f057';
@fa-var-times-circle-o: '\f05c';
@fa-var-times-rectangle: '\f2d3';
@fa-var-times-rectangle-o: '\f2d4';
@fa-var-tint: '\f043';
@fa-var-toggle-down: '\f150';
@fa-var-toggle-left: '\f191';
@fa-var-toggle-off: '\f204';
@fa-var-toggle-on: '\f205';
@fa-var-toggle-right: '\f152';
@fa-var-toggle-up: '\f151';
@fa-var-trademark: '\f25c';
@fa-var-train: '\f238';
@fa-var-transgender: '\f224';
@fa-var-transgender-alt: '\f225';
@fa-var-trash: '\f1f8';
@fa-var-trash-o: '\f014';
@fa-var-tree: '\f1bb';
@fa-var-trello: '\f181';
@fa-var-tripadvisor: '\f262';
@fa-var-trophy: '\f091';
@fa-var-truck: '\f0d1';
@fa-var-try: '\f195';
@fa-var-tty: '\f1e4';
@fa-var-tumblr: '\f173';
@fa-var-tumblr-square: '\f174';
@fa-var-turkish-lira: '\f195';
@fa-var-tv: '\f26c';
@fa-var-twitch: '\f1e8';
@fa-var-twitter: '\f099';
@fa-var-twitter-square: '\f081';
@fa-var-umbrella: '\f0e9';
@fa-var-underline: '\f0cd';
@fa-var-undo: '\f0e2';
@fa-var-universal-access: '\f29a';
@fa-var-university: '\f19c';
@fa-var-unlink: '\f127';
@fa-var-unlock: '\f09c';
@fa-var-unlock-alt: '\f13e';
@fa-var-unsorted: '\f0dc';
@fa-var-upload: '\f093';
@fa-var-usb: '\f287';
@fa-var-usd: '\f155';
@fa-var-user: '\f007';
@fa-var-user-circle: '\f2bd';
@fa-var-user-circle-o: '\f2be';
@fa-var-user-md: '\f0f0';
@fa-var-user-o: '\f2c0';
@fa-var-user-plus: '\f234';
@fa-var-user-secret: '\f21b';
@fa-var-user-times: '\f235';
@fa-var-users: '\f0c0';
@fa-var-vcard: '\f2bb';
@fa-var-vcard-o: '\f2bc';
@fa-var-venus: '\f221';
@fa-var-venus-double: '\f226';
@fa-var-venus-mars: '\f228';
@fa-var-viacoin: '\f237';
@fa-var-viadeo: '\f2a9';
@fa-var-viadeo-square: '\f2aa';
@fa-var-video-camera: '\f03d';
@fa-var-vimeo: '\f27d';
@fa-var-vimeo-square: '\f194';
@fa-var-vine: '\f1ca';
@fa-var-vk: '\f189';
@fa-var-volume-control-phone: '\f2a0';
@fa-var-volume-down: '\f027';
@fa-var-volume-off: '\f026';
@fa-var-volume-up: '\f028';
@fa-var-warning: '\f071';
@fa-var-wechat: '\f1d7';
@fa-var-weibo: '\f18a';
@fa-var-weixin: '\f1d7';
@fa-var-whatsapp: '\f232';
@fa-var-wheelchair: '\f193';
@fa-var-wheelchair-alt: '\f29b';
@fa-var-wifi: '\f1eb';
@fa-var-wikipedia-w: '\f266';
@fa-var-window-close: '\f2d3';
@fa-var-window-close-o: '\f2d4';
@fa-var-window-maximize: '\f2d0';
@fa-var-window-minimize: '\f2d1';
@fa-var-window-restore: '\f2d2';
@fa-var-windows: '\f17a';
@fa-var-won: '\f159';
@fa-var-wordpress: '\f19a';
@fa-var-wpbeginner: '\f297';
@fa-var-wpexplorer: '\f2de';
@fa-var-wpforms: '\f298';
@fa-var-wrench: '\f0ad';
@fa-var-xing: '\f168';
@fa-var-xing-square: '\f169';
@fa-var-y-combinator: '\f23b';
@fa-var-y-combinator-square: '\f1d4';
@fa-var-yahoo: '\f19e';
@fa-var-yc: '\f23b';
@fa-var-yc-square: '\f1d4';
@fa-var-yelp: '\f1e9';
@fa-var-yen: '\f157';
@fa-var-yoast: '\f2b1';
@fa-var-youtube: '\f167';
@fa-var-youtube-play: '\f16a';
@fa-var-youtube-square: '\f166';

View File

@ -0,0 +1,34 @@
// Spinning Icons
// --------------------------
.#{$fa-css-prefix}-spin {
-webkit-animation: fa-spin 2s infinite linear;
animation: fa-spin 2s infinite linear;
}
.#{$fa-css-prefix}-pulse {
-webkit-animation: fa-spin 1s infinite steps(8);
animation: fa-spin 1s infinite steps(8);
}
@-webkit-keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}

View File

@ -0,0 +1,41 @@
// Bordered & Pulled
// -------------------------
.#{$fa-css-prefix}-border {
padding: 0.2em 0.25em 0.15em;
border: solid 0.08em $fa-border-color;
border-radius: 0.1em;
}
.#{$fa-css-prefix}-pull-left {
float: left;
}
.#{$fa-css-prefix}-pull-right {
float: right;
}
.#{$fa-css-prefix} {
&.#{$fa-css-prefix}-pull-left {
margin-right: 0.3em;
}
&.#{$fa-css-prefix}-pull-right {
margin-left: 0.3em;
}
}
/* Deprecated as of 4.4.0 */
.pull-right {
float: right;
}
.pull-left {
float: left;
}
.#{$fa-css-prefix} {
&.pull-left {
margin-right: 0.3em;
}
&.pull-right {
margin-left: 0.3em;
}
}

View File

@ -0,0 +1,11 @@
// Base Class Definition
// -------------------------
.#{$fa-css-prefix} {
display: inline-block;
font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration
font-size: inherit; // can't have font-size inherit on line above, so need to override
text-rendering: auto; // optimizelegibility throws things off #1094
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -0,0 +1,6 @@
// Fixed Width Icons
// -------------------------
.#{$fa-css-prefix}-fw {
width: (18em / 14);
text-align: center;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
// Icon Sizes
// -------------------------
/* makes the font 33% larger relative to the icon container */
.#{$fa-css-prefix}-lg {
font-size: (4em / 3);
line-height: (3em / 4);
vertical-align: -15%;
}
.#{$fa-css-prefix}-2x {
font-size: 2em;
}
.#{$fa-css-prefix}-3x {
font-size: 3em;
}
.#{$fa-css-prefix}-4x {
font-size: 4em;
}
.#{$fa-css-prefix}-5x {
font-size: 5em;
}

View File

@ -0,0 +1,21 @@
// List Icons
// -------------------------
.#{$fa-css-prefix}-ul {
padding-left: 0;
margin-left: $fa-li-width;
list-style-type: none;
> li {
position: relative;
}
}
.#{$fa-css-prefix}-li {
position: absolute;
left: -$fa-li-width;
width: $fa-li-width;
top: (2em / 14);
text-align: center;
&.#{$fa-css-prefix}-lg {
left: -$fa-li-width + (4em / 14);
}
}

View File

@ -0,0 +1,58 @@
// Mixins
// --------------------------
@mixin fa-icon() {
display: inline-block;
font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration
font-size: inherit; // can't have font-size inherit on line above, so need to override
text-rendering: auto; // optimizelegibility throws things off #1094
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@mixin fa-icon-rotate($degrees, $rotation) {
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})';
-webkit-transform: rotate($degrees);
-ms-transform: rotate($degrees);
transform: rotate($degrees);
}
@mixin fa-icon-flip($horiz, $vert, $rotation) {
-ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)';
-webkit-transform: scale($horiz, $vert);
-ms-transform: scale($horiz, $vert);
transform: scale($horiz, $vert);
}
// Only display content to screen readers. A la Bootstrap 4.
//
// See: http://a11yproject.com/posts/how-to-hide-content/
@mixin sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
// Use in conjunction with .sr-only to only display content when it's focused.
//
// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
//
// Credit: HTML5 Boilerplate
@mixin sr-only-focusable {
&:active,
&:focus {
position: static;
width: auto;
height: auto;
margin: 0;
overflow: visible;
clip: auto;
}
}

View File

@ -0,0 +1,17 @@
/* FONT PATH
* -------------------------- */
@font-face {
font-family: 'FontAwesome';
src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}');
src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}')
format('embedded-opentype'),
url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'),
url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'),
url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'),
url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular')
format('svg');
// src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts
font-weight: normal;
font-style: normal;
}

View File

@ -0,0 +1,30 @@
// Rotated & Flipped Icons
// -------------------------
.#{$fa-css-prefix}-rotate-90 {
@include fa-icon-rotate(90deg, 1);
}
.#{$fa-css-prefix}-rotate-180 {
@include fa-icon-rotate(180deg, 2);
}
.#{$fa-css-prefix}-rotate-270 {
@include fa-icon-rotate(270deg, 3);
}
.#{$fa-css-prefix}-flip-horizontal {
@include fa-icon-flip(-1, 1, 0);
}
.#{$fa-css-prefix}-flip-vertical {
@include fa-icon-flip(1, -1, 2);
}
// Hook for IE8-9
// -------------------------
:root .#{$fa-css-prefix}-rotate-90,
:root .#{$fa-css-prefix}-rotate-180,
:root .#{$fa-css-prefix}-rotate-270,
:root .#{$fa-css-prefix}-flip-horizontal,
:root .#{$fa-css-prefix}-flip-vertical {
filter: none;
}

View File

@ -0,0 +1,9 @@
// Screen Readers
// -------------------------
.sr-only {
@include sr-only();
}
.sr-only-focusable {
@include sr-only-focusable();
}

View File

@ -0,0 +1,27 @@
// Stacked Icons
// -------------------------
.#{$fa-css-prefix}-stack {
position: relative;
display: inline-block;
width: 2em;
height: 2em;
line-height: 2em;
vertical-align: middle;
}
.#{$fa-css-prefix}-stack-1x,
.#{$fa-css-prefix}-stack-2x {
position: absolute;
left: 0;
width: 100%;
text-align: center;
}
.#{$fa-css-prefix}-stack-1x {
line-height: inherit;
}
.#{$fa-css-prefix}-stack-2x {
font-size: 2em;
}
.#{$fa-css-prefix}-inverse {
color: $fa-inverse;
}

View File

@ -0,0 +1,799 @@
// Variables
// --------------------------
$fa-font-path: '../fonts' !default;
$fa-font-size-base: 14px !default;
$fa-line-height-base: 1 !default;
//$fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.7.0/fonts" !default; // for referencing Bootstrap CDN font files directly
$fa-css-prefix: fa !default;
$fa-version: '4.7.0' !default;
$fa-border-color: #eee !default;
$fa-inverse: #fff !default;
$fa-li-width: (30em / 14) !default;
$fa-var-500px: '\f26e';
$fa-var-address-book: '\f2b9';
$fa-var-address-book-o: '\f2ba';
$fa-var-address-card: '\f2bb';
$fa-var-address-card-o: '\f2bc';
$fa-var-adjust: '\f042';
$fa-var-adn: '\f170';
$fa-var-align-center: '\f037';
$fa-var-align-justify: '\f039';
$fa-var-align-left: '\f036';
$fa-var-align-right: '\f038';
$fa-var-amazon: '\f270';
$fa-var-ambulance: '\f0f9';
$fa-var-american-sign-language-interpreting: '\f2a3';
$fa-var-anchor: '\f13d';
$fa-var-android: '\f17b';
$fa-var-angellist: '\f209';
$fa-var-angle-double-down: '\f103';
$fa-var-angle-double-left: '\f100';
$fa-var-angle-double-right: '\f101';
$fa-var-angle-double-up: '\f102';
$fa-var-angle-down: '\f107';
$fa-var-angle-left: '\f104';
$fa-var-angle-right: '\f105';
$fa-var-angle-up: '\f106';
$fa-var-apple: '\f179';
$fa-var-archive: '\f187';
$fa-var-area-chart: '\f1fe';
$fa-var-arrow-circle-down: '\f0ab';
$fa-var-arrow-circle-left: '\f0a8';
$fa-var-arrow-circle-o-down: '\f01a';
$fa-var-arrow-circle-o-left: '\f190';
$fa-var-arrow-circle-o-right: '\f18e';
$fa-var-arrow-circle-o-up: '\f01b';
$fa-var-arrow-circle-right: '\f0a9';
$fa-var-arrow-circle-up: '\f0aa';
$fa-var-arrow-down: '\f063';
$fa-var-arrow-left: '\f060';
$fa-var-arrow-right: '\f061';
$fa-var-arrow-up: '\f062';
$fa-var-arrows: '\f047';
$fa-var-arrows-alt: '\f0b2';
$fa-var-arrows-h: '\f07e';
$fa-var-arrows-v: '\f07d';
$fa-var-asl-interpreting: '\f2a3';
$fa-var-assistive-listening-systems: '\f2a2';
$fa-var-asterisk: '\f069';
$fa-var-at: '\f1fa';
$fa-var-audio-description: '\f29e';
$fa-var-automobile: '\f1b9';
$fa-var-backward: '\f04a';
$fa-var-balance-scale: '\f24e';
$fa-var-ban: '\f05e';
$fa-var-bandcamp: '\f2d5';
$fa-var-bank: '\f19c';
$fa-var-bar-chart: '\f080';
$fa-var-bar-chart-o: '\f080';
$fa-var-barcode: '\f02a';
$fa-var-bars: '\f0c9';
$fa-var-bath: '\f2cd';
$fa-var-bathtub: '\f2cd';
$fa-var-battery: '\f240';
$fa-var-battery-0: '\f244';
$fa-var-battery-1: '\f243';
$fa-var-battery-2: '\f242';
$fa-var-battery-3: '\f241';
$fa-var-battery-4: '\f240';
$fa-var-battery-empty: '\f244';
$fa-var-battery-full: '\f240';
$fa-var-battery-half: '\f242';
$fa-var-battery-quarter: '\f243';
$fa-var-battery-three-quarters: '\f241';
$fa-var-bed: '\f236';
$fa-var-beer: '\f0fc';
$fa-var-behance: '\f1b4';
$fa-var-behance-square: '\f1b5';
$fa-var-bell: '\f0f3';
$fa-var-bell-o: '\f0a2';
$fa-var-bell-slash: '\f1f6';
$fa-var-bell-slash-o: '\f1f7';
$fa-var-bicycle: '\f206';
$fa-var-binoculars: '\f1e5';
$fa-var-birthday-cake: '\f1fd';
$fa-var-bitbucket: '\f171';
$fa-var-bitbucket-square: '\f172';
$fa-var-bitcoin: '\f15a';
$fa-var-black-tie: '\f27e';
$fa-var-blind: '\f29d';
$fa-var-bluetooth: '\f293';
$fa-var-bluetooth-b: '\f294';
$fa-var-bold: '\f032';
$fa-var-bolt: '\f0e7';
$fa-var-bomb: '\f1e2';
$fa-var-book: '\f02d';
$fa-var-bookmark: '\f02e';
$fa-var-bookmark-o: '\f097';
$fa-var-braille: '\f2a1';
$fa-var-briefcase: '\f0b1';
$fa-var-btc: '\f15a';
$fa-var-bug: '\f188';
$fa-var-building: '\f1ad';
$fa-var-building-o: '\f0f7';
$fa-var-bullhorn: '\f0a1';
$fa-var-bullseye: '\f140';
$fa-var-bus: '\f207';
$fa-var-buysellads: '\f20d';
$fa-var-cab: '\f1ba';
$fa-var-calculator: '\f1ec';
$fa-var-calendar: '\f073';
$fa-var-calendar-check-o: '\f274';
$fa-var-calendar-minus-o: '\f272';
$fa-var-calendar-o: '\f133';
$fa-var-calendar-plus-o: '\f271';
$fa-var-calendar-times-o: '\f273';
$fa-var-camera: '\f030';
$fa-var-camera-retro: '\f083';
$fa-var-car: '\f1b9';
$fa-var-caret-down: '\f0d7';
$fa-var-caret-left: '\f0d9';
$fa-var-caret-right: '\f0da';
$fa-var-caret-square-o-down: '\f150';
$fa-var-caret-square-o-left: '\f191';
$fa-var-caret-square-o-right: '\f152';
$fa-var-caret-square-o-up: '\f151';
$fa-var-caret-up: '\f0d8';
$fa-var-cart-arrow-down: '\f218';
$fa-var-cart-plus: '\f217';
$fa-var-cc: '\f20a';
$fa-var-cc-amex: '\f1f3';
$fa-var-cc-diners-club: '\f24c';
$fa-var-cc-discover: '\f1f2';
$fa-var-cc-jcb: '\f24b';
$fa-var-cc-mastercard: '\f1f1';
$fa-var-cc-paypal: '\f1f4';
$fa-var-cc-stripe: '\f1f5';
$fa-var-cc-visa: '\f1f0';
$fa-var-certificate: '\f0a3';
$fa-var-chain: '\f0c1';
$fa-var-chain-broken: '\f127';
$fa-var-check: '\f00c';
$fa-var-check-circle: '\f058';
$fa-var-check-circle-o: '\f05d';
$fa-var-check-square: '\f14a';
$fa-var-check-square-o: '\f046';
$fa-var-chevron-circle-down: '\f13a';
$fa-var-chevron-circle-left: '\f137';
$fa-var-chevron-circle-right: '\f138';
$fa-var-chevron-circle-up: '\f139';
$fa-var-chevron-down: '\f078';
$fa-var-chevron-left: '\f053';
$fa-var-chevron-right: '\f054';
$fa-var-chevron-up: '\f077';
$fa-var-child: '\f1ae';
$fa-var-chrome: '\f268';
$fa-var-circle: '\f111';
$fa-var-circle-o: '\f10c';
$fa-var-circle-o-notch: '\f1ce';
$fa-var-circle-thin: '\f1db';
$fa-var-clipboard: '\f0ea';
$fa-var-clock-o: '\f017';
$fa-var-clone: '\f24d';
$fa-var-close: '\f00d';
$fa-var-cloud: '\f0c2';
$fa-var-cloud-download: '\f0ed';
$fa-var-cloud-upload: '\f0ee';
$fa-var-cny: '\f157';
$fa-var-code: '\f121';
$fa-var-code-fork: '\f126';
$fa-var-codepen: '\f1cb';
$fa-var-codiepie: '\f284';
$fa-var-coffee: '\f0f4';
$fa-var-cog: '\f013';
$fa-var-cogs: '\f085';
$fa-var-columns: '\f0db';
$fa-var-comment: '\f075';
$fa-var-comment-o: '\f0e5';
$fa-var-commenting: '\f27a';
$fa-var-commenting-o: '\f27b';
$fa-var-comments: '\f086';
$fa-var-comments-o: '\f0e6';
$fa-var-compass: '\f14e';
$fa-var-compress: '\f066';
$fa-var-connectdevelop: '\f20e';
$fa-var-contao: '\f26d';
$fa-var-copy: '\f0c5';
$fa-var-copyright: '\f1f9';
$fa-var-creative-commons: '\f25e';
$fa-var-credit-card: '\f09d';
$fa-var-credit-card-alt: '\f283';
$fa-var-crop: '\f125';
$fa-var-crosshairs: '\f05b';
$fa-var-css3: '\f13c';
$fa-var-cube: '\f1b2';
$fa-var-cubes: '\f1b3';
$fa-var-cut: '\f0c4';
$fa-var-cutlery: '\f0f5';
$fa-var-dashboard: '\f0e4';
$fa-var-dashcube: '\f210';
$fa-var-database: '\f1c0';
$fa-var-deaf: '\f2a4';
$fa-var-deafness: '\f2a4';
$fa-var-dedent: '\f03b';
$fa-var-delicious: '\f1a5';
$fa-var-desktop: '\f108';
$fa-var-deviantart: '\f1bd';
$fa-var-diamond: '\f219';
$fa-var-digg: '\f1a6';
$fa-var-dollar: '\f155';
$fa-var-dot-circle-o: '\f192';
$fa-var-download: '\f019';
$fa-var-dribbble: '\f17d';
$fa-var-drivers-license: '\f2c2';
$fa-var-drivers-license-o: '\f2c3';
$fa-var-dropbox: '\f16b';
$fa-var-drupal: '\f1a9';
$fa-var-edge: '\f282';
$fa-var-edit: '\f044';
$fa-var-eercast: '\f2da';
$fa-var-eject: '\f052';
$fa-var-ellipsis-h: '\f141';
$fa-var-ellipsis-v: '\f142';
$fa-var-empire: '\f1d1';
$fa-var-envelope: '\f0e0';
$fa-var-envelope-o: '\f003';
$fa-var-envelope-open: '\f2b6';
$fa-var-envelope-open-o: '\f2b7';
$fa-var-envelope-square: '\f199';
$fa-var-envira: '\f299';
$fa-var-eraser: '\f12d';
$fa-var-etsy: '\f2d7';
$fa-var-eur: '\f153';
$fa-var-euro: '\f153';
$fa-var-exchange: '\f0ec';
$fa-var-exclamation: '\f12a';
$fa-var-exclamation-circle: '\f06a';
$fa-var-exclamation-triangle: '\f071';
$fa-var-expand: '\f065';
$fa-var-expeditedssl: '\f23e';
$fa-var-external-link: '\f08e';
$fa-var-external-link-square: '\f14c';
$fa-var-eye: '\f06e';
$fa-var-eye-slash: '\f070';
$fa-var-eyedropper: '\f1fb';
$fa-var-fa: '\f2b4';
$fa-var-facebook: '\f09a';
$fa-var-facebook-f: '\f09a';
$fa-var-facebook-official: '\f230';
$fa-var-facebook-square: '\f082';
$fa-var-fast-backward: '\f049';
$fa-var-fast-forward: '\f050';
$fa-var-fax: '\f1ac';
$fa-var-feed: '\f09e';
$fa-var-female: '\f182';
$fa-var-fighter-jet: '\f0fb';
$fa-var-file: '\f15b';
$fa-var-file-archive-o: '\f1c6';
$fa-var-file-audio-o: '\f1c7';
$fa-var-file-code-o: '\f1c9';
$fa-var-file-excel-o: '\f1c3';
$fa-var-file-image-o: '\f1c5';
$fa-var-file-movie-o: '\f1c8';
$fa-var-file-o: '\f016';
$fa-var-file-pdf-o: '\f1c1';
$fa-var-file-photo-o: '\f1c5';
$fa-var-file-picture-o: '\f1c5';
$fa-var-file-powerpoint-o: '\f1c4';
$fa-var-file-sound-o: '\f1c7';
$fa-var-file-text: '\f15c';
$fa-var-file-text-o: '\f0f6';
$fa-var-file-video-o: '\f1c8';
$fa-var-file-word-o: '\f1c2';
$fa-var-file-zip-o: '\f1c6';
$fa-var-files-o: '\f0c5';
$fa-var-film: '\f008';
$fa-var-filter: '\f0b0';
$fa-var-fire: '\f06d';
$fa-var-fire-extinguisher: '\f134';
$fa-var-firefox: '\f269';
$fa-var-first-order: '\f2b0';
$fa-var-flag: '\f024';
$fa-var-flag-checkered: '\f11e';
$fa-var-flag-o: '\f11d';
$fa-var-flash: '\f0e7';
$fa-var-flask: '\f0c3';
$fa-var-flickr: '\f16e';
$fa-var-floppy-o: '\f0c7';
$fa-var-folder: '\f07b';
$fa-var-folder-o: '\f114';
$fa-var-folder-open: '\f07c';
$fa-var-folder-open-o: '\f115';
$fa-var-font: '\f031';
$fa-var-font-awesome: '\f2b4';
$fa-var-fonticons: '\f280';
$fa-var-fort-awesome: '\f286';
$fa-var-forumbee: '\f211';
$fa-var-forward: '\f04e';
$fa-var-foursquare: '\f180';
$fa-var-free-code-camp: '\f2c5';
$fa-var-frown-o: '\f119';
$fa-var-futbol-o: '\f1e3';
$fa-var-gamepad: '\f11b';
$fa-var-gavel: '\f0e3';
$fa-var-gbp: '\f154';
$fa-var-ge: '\f1d1';
$fa-var-gear: '\f013';
$fa-var-gears: '\f085';
$fa-var-genderless: '\f22d';
$fa-var-get-pocket: '\f265';
$fa-var-gg: '\f260';
$fa-var-gg-circle: '\f261';
$fa-var-gift: '\f06b';
$fa-var-git: '\f1d3';
$fa-var-git-square: '\f1d2';
$fa-var-github: '\f09b';
$fa-var-github-alt: '\f113';
$fa-var-github-square: '\f092';
$fa-var-gitlab: '\f296';
$fa-var-gittip: '\f184';
$fa-var-glass: '\f000';
$fa-var-glide: '\f2a5';
$fa-var-glide-g: '\f2a6';
$fa-var-globe: '\f0ac';
$fa-var-google: '\f1a0';
$fa-var-google-plus: '\f0d5';
$fa-var-google-plus-circle: '\f2b3';
$fa-var-google-plus-official: '\f2b3';
$fa-var-google-plus-square: '\f0d4';
$fa-var-google-wallet: '\f1ee';
$fa-var-graduation-cap: '\f19d';
$fa-var-gratipay: '\f184';
$fa-var-grav: '\f2d6';
$fa-var-group: '\f0c0';
$fa-var-h-square: '\f0fd';
$fa-var-hacker-news: '\f1d4';
$fa-var-hand-grab-o: '\f255';
$fa-var-hand-lizard-o: '\f258';
$fa-var-hand-o-down: '\f0a7';
$fa-var-hand-o-left: '\f0a5';
$fa-var-hand-o-right: '\f0a4';
$fa-var-hand-o-up: '\f0a6';
$fa-var-hand-paper-o: '\f256';
$fa-var-hand-peace-o: '\f25b';
$fa-var-hand-pointer-o: '\f25a';
$fa-var-hand-rock-o: '\f255';
$fa-var-hand-scissors-o: '\f257';
$fa-var-hand-spock-o: '\f259';
$fa-var-hand-stop-o: '\f256';
$fa-var-handshake-o: '\f2b5';
$fa-var-hard-of-hearing: '\f2a4';
$fa-var-hashtag: '\f292';
$fa-var-hdd-o: '\f0a0';
$fa-var-header: '\f1dc';
$fa-var-headphones: '\f025';
$fa-var-heart: '\f004';
$fa-var-heart-o: '\f08a';
$fa-var-heartbeat: '\f21e';
$fa-var-history: '\f1da';
$fa-var-home: '\f015';
$fa-var-hospital-o: '\f0f8';
$fa-var-hotel: '\f236';
$fa-var-hourglass: '\f254';
$fa-var-hourglass-1: '\f251';
$fa-var-hourglass-2: '\f252';
$fa-var-hourglass-3: '\f253';
$fa-var-hourglass-end: '\f253';
$fa-var-hourglass-half: '\f252';
$fa-var-hourglass-o: '\f250';
$fa-var-hourglass-start: '\f251';
$fa-var-houzz: '\f27c';
$fa-var-html5: '\f13b';
$fa-var-i-cursor: '\f246';
$fa-var-id-badge: '\f2c1';
$fa-var-id-card: '\f2c2';
$fa-var-id-card-o: '\f2c3';
$fa-var-ils: '\f20b';
$fa-var-image: '\f03e';
$fa-var-imdb: '\f2d8';
$fa-var-inbox: '\f01c';
$fa-var-indent: '\f03c';
$fa-var-industry: '\f275';
$fa-var-info: '\f129';
$fa-var-info-circle: '\f05a';
$fa-var-inr: '\f156';
$fa-var-instagram: '\f16d';
$fa-var-institution: '\f19c';
$fa-var-internet-explorer: '\f26b';
$fa-var-intersex: '\f224';
$fa-var-ioxhost: '\f208';
$fa-var-italic: '\f033';
$fa-var-joomla: '\f1aa';
$fa-var-jpy: '\f157';
$fa-var-jsfiddle: '\f1cc';
$fa-var-key: '\f084';
$fa-var-keyboard-o: '\f11c';
$fa-var-krw: '\f159';
$fa-var-language: '\f1ab';
$fa-var-laptop: '\f109';
$fa-var-lastfm: '\f202';
$fa-var-lastfm-square: '\f203';
$fa-var-leaf: '\f06c';
$fa-var-leanpub: '\f212';
$fa-var-legal: '\f0e3';
$fa-var-lemon-o: '\f094';
$fa-var-level-down: '\f149';
$fa-var-level-up: '\f148';
$fa-var-life-bouy: '\f1cd';
$fa-var-life-buoy: '\f1cd';
$fa-var-life-ring: '\f1cd';
$fa-var-life-saver: '\f1cd';
$fa-var-lightbulb-o: '\f0eb';
$fa-var-line-chart: '\f201';
$fa-var-link: '\f0c1';
$fa-var-linkedin: '\f0e1';
$fa-var-linkedin-square: '\f08c';
$fa-var-linode: '\f2b8';
$fa-var-linux: '\f17c';
$fa-var-list: '\f03a';
$fa-var-list-alt: '\f022';
$fa-var-list-ol: '\f0cb';
$fa-var-list-ul: '\f0ca';
$fa-var-location-arrow: '\f124';
$fa-var-lock: '\f023';
$fa-var-long-arrow-down: '\f175';
$fa-var-long-arrow-left: '\f177';
$fa-var-long-arrow-right: '\f178';
$fa-var-long-arrow-up: '\f176';
$fa-var-low-vision: '\f2a8';
$fa-var-magic: '\f0d0';
$fa-var-magnet: '\f076';
$fa-var-mail-forward: '\f064';
$fa-var-mail-reply: '\f112';
$fa-var-mail-reply-all: '\f122';
$fa-var-male: '\f183';
$fa-var-map: '\f279';
$fa-var-map-marker: '\f041';
$fa-var-map-o: '\f278';
$fa-var-map-pin: '\f276';
$fa-var-map-signs: '\f277';
$fa-var-mars: '\f222';
$fa-var-mars-double: '\f227';
$fa-var-mars-stroke: '\f229';
$fa-var-mars-stroke-h: '\f22b';
$fa-var-mars-stroke-v: '\f22a';
$fa-var-maxcdn: '\f136';
$fa-var-meanpath: '\f20c';
$fa-var-medium: '\f23a';
$fa-var-medkit: '\f0fa';
$fa-var-meetup: '\f2e0';
$fa-var-meh-o: '\f11a';
$fa-var-mercury: '\f223';
$fa-var-microchip: '\f2db';
$fa-var-microphone: '\f130';
$fa-var-microphone-slash: '\f131';
$fa-var-minus: '\f068';
$fa-var-minus-circle: '\f056';
$fa-var-minus-square: '\f146';
$fa-var-minus-square-o: '\f147';
$fa-var-mixcloud: '\f289';
$fa-var-mobile: '\f10b';
$fa-var-mobile-phone: '\f10b';
$fa-var-modx: '\f285';
$fa-var-money: '\f0d6';
$fa-var-moon-o: '\f186';
$fa-var-mortar-board: '\f19d';
$fa-var-motorcycle: '\f21c';
$fa-var-mouse-pointer: '\f245';
$fa-var-music: '\f001';
$fa-var-navicon: '\f0c9';
$fa-var-neuter: '\f22c';
$fa-var-newspaper-o: '\f1ea';
$fa-var-object-group: '\f247';
$fa-var-object-ungroup: '\f248';
$fa-var-odnoklassniki: '\f263';
$fa-var-odnoklassniki-square: '\f264';
$fa-var-opencart: '\f23d';
$fa-var-openid: '\f19b';
$fa-var-opera: '\f26a';
$fa-var-optin-monster: '\f23c';
$fa-var-outdent: '\f03b';
$fa-var-pagelines: '\f18c';
$fa-var-paint-brush: '\f1fc';
$fa-var-paper-plane: '\f1d8';
$fa-var-paper-plane-o: '\f1d9';
$fa-var-paperclip: '\f0c6';
$fa-var-paragraph: '\f1dd';
$fa-var-paste: '\f0ea';
$fa-var-pause: '\f04c';
$fa-var-pause-circle: '\f28b';
$fa-var-pause-circle-o: '\f28c';
$fa-var-paw: '\f1b0';
$fa-var-paypal: '\f1ed';
$fa-var-pencil: '\f040';
$fa-var-pencil-square: '\f14b';
$fa-var-pencil-square-o: '\f044';
$fa-var-percent: '\f295';
$fa-var-phone: '\f095';
$fa-var-phone-square: '\f098';
$fa-var-photo: '\f03e';
$fa-var-picture-o: '\f03e';
$fa-var-pie-chart: '\f200';
$fa-var-pied-piper: '\f2ae';
$fa-var-pied-piper-alt: '\f1a8';
$fa-var-pied-piper-pp: '\f1a7';
$fa-var-pinterest: '\f0d2';
$fa-var-pinterest-p: '\f231';
$fa-var-pinterest-square: '\f0d3';
$fa-var-plane: '\f072';
$fa-var-play: '\f04b';
$fa-var-play-circle: '\f144';
$fa-var-play-circle-o: '\f01d';
$fa-var-plug: '\f1e6';
$fa-var-plus: '\f067';
$fa-var-plus-circle: '\f055';
$fa-var-plus-square: '\f0fe';
$fa-var-plus-square-o: '\f196';
$fa-var-podcast: '\f2ce';
$fa-var-power-off: '\f011';
$fa-var-print: '\f02f';
$fa-var-product-hunt: '\f288';
$fa-var-puzzle-piece: '\f12e';
$fa-var-qq: '\f1d6';
$fa-var-qrcode: '\f029';
$fa-var-question: '\f128';
$fa-var-question-circle: '\f059';
$fa-var-question-circle-o: '\f29c';
$fa-var-quora: '\f2c4';
$fa-var-quote-left: '\f10d';
$fa-var-quote-right: '\f10e';
$fa-var-ra: '\f1d0';
$fa-var-random: '\f074';
$fa-var-ravelry: '\f2d9';
$fa-var-rebel: '\f1d0';
$fa-var-recycle: '\f1b8';
$fa-var-reddit: '\f1a1';
$fa-var-reddit-alien: '\f281';
$fa-var-reddit-square: '\f1a2';
$fa-var-refresh: '\f021';
$fa-var-registered: '\f25d';
$fa-var-remove: '\f00d';
$fa-var-renren: '\f18b';
$fa-var-reorder: '\f0c9';
$fa-var-repeat: '\f01e';
$fa-var-reply: '\f112';
$fa-var-reply-all: '\f122';
$fa-var-resistance: '\f1d0';
$fa-var-retweet: '\f079';
$fa-var-rmb: '\f157';
$fa-var-road: '\f018';
$fa-var-rocket: '\f135';
$fa-var-rotate-left: '\f0e2';
$fa-var-rotate-right: '\f01e';
$fa-var-rouble: '\f158';
$fa-var-rss: '\f09e';
$fa-var-rss-square: '\f143';
$fa-var-rub: '\f158';
$fa-var-ruble: '\f158';
$fa-var-rupee: '\f156';
$fa-var-s15: '\f2cd';
$fa-var-safari: '\f267';
$fa-var-save: '\f0c7';
$fa-var-scissors: '\f0c4';
$fa-var-scribd: '\f28a';
$fa-var-search: '\f002';
$fa-var-search-minus: '\f010';
$fa-var-search-plus: '\f00e';
$fa-var-sellsy: '\f213';
$fa-var-send: '\f1d8';
$fa-var-send-o: '\f1d9';
$fa-var-server: '\f233';
$fa-var-share: '\f064';
$fa-var-share-alt: '\f1e0';
$fa-var-share-alt-square: '\f1e1';
$fa-var-share-square: '\f14d';
$fa-var-share-square-o: '\f045';
$fa-var-shekel: '\f20b';
$fa-var-sheqel: '\f20b';
$fa-var-shield: '\f132';
$fa-var-ship: '\f21a';
$fa-var-shirtsinbulk: '\f214';
$fa-var-shopping-bag: '\f290';
$fa-var-shopping-basket: '\f291';
$fa-var-shopping-cart: '\f07a';
$fa-var-shower: '\f2cc';
$fa-var-sign-in: '\f090';
$fa-var-sign-language: '\f2a7';
$fa-var-sign-out: '\f08b';
$fa-var-signal: '\f012';
$fa-var-signing: '\f2a7';
$fa-var-simplybuilt: '\f215';
$fa-var-sitemap: '\f0e8';
$fa-var-skyatlas: '\f216';
$fa-var-skype: '\f17e';
$fa-var-slack: '\f198';
$fa-var-sliders: '\f1de';
$fa-var-slideshare: '\f1e7';
$fa-var-smile-o: '\f118';
$fa-var-snapchat: '\f2ab';
$fa-var-snapchat-ghost: '\f2ac';
$fa-var-snapchat-square: '\f2ad';
$fa-var-snowflake-o: '\f2dc';
$fa-var-soccer-ball-o: '\f1e3';
$fa-var-sort: '\f0dc';
$fa-var-sort-alpha-asc: '\f15d';
$fa-var-sort-alpha-desc: '\f15e';
$fa-var-sort-amount-asc: '\f160';
$fa-var-sort-amount-desc: '\f161';
$fa-var-sort-asc: '\f0de';
$fa-var-sort-desc: '\f0dd';
$fa-var-sort-down: '\f0dd';
$fa-var-sort-numeric-asc: '\f162';
$fa-var-sort-numeric-desc: '\f163';
$fa-var-sort-up: '\f0de';
$fa-var-soundcloud: '\f1be';
$fa-var-space-shuttle: '\f197';
$fa-var-spinner: '\f110';
$fa-var-spoon: '\f1b1';
$fa-var-spotify: '\f1bc';
$fa-var-square: '\f0c8';
$fa-var-square-o: '\f096';
$fa-var-stack-exchange: '\f18d';
$fa-var-stack-overflow: '\f16c';
$fa-var-star: '\f005';
$fa-var-star-half: '\f089';
$fa-var-star-half-empty: '\f123';
$fa-var-star-half-full: '\f123';
$fa-var-star-half-o: '\f123';
$fa-var-star-o: '\f006';
$fa-var-steam: '\f1b6';
$fa-var-steam-square: '\f1b7';
$fa-var-step-backward: '\f048';
$fa-var-step-forward: '\f051';
$fa-var-stethoscope: '\f0f1';
$fa-var-sticky-note: '\f249';
$fa-var-sticky-note-o: '\f24a';
$fa-var-stop: '\f04d';
$fa-var-stop-circle: '\f28d';
$fa-var-stop-circle-o: '\f28e';
$fa-var-street-view: '\f21d';
$fa-var-strikethrough: '\f0cc';
$fa-var-stumbleupon: '\f1a4';
$fa-var-stumbleupon-circle: '\f1a3';
$fa-var-subscript: '\f12c';
$fa-var-subway: '\f239';
$fa-var-suitcase: '\f0f2';
$fa-var-sun-o: '\f185';
$fa-var-superpowers: '\f2dd';
$fa-var-superscript: '\f12b';
$fa-var-support: '\f1cd';
$fa-var-table: '\f0ce';
$fa-var-tablet: '\f10a';
$fa-var-tachometer: '\f0e4';
$fa-var-tag: '\f02b';
$fa-var-tags: '\f02c';
$fa-var-tasks: '\f0ae';
$fa-var-taxi: '\f1ba';
$fa-var-telegram: '\f2c6';
$fa-var-television: '\f26c';
$fa-var-tencent-weibo: '\f1d5';
$fa-var-terminal: '\f120';
$fa-var-text-height: '\f034';
$fa-var-text-width: '\f035';
$fa-var-th: '\f00a';
$fa-var-th-large: '\f009';
$fa-var-th-list: '\f00b';
$fa-var-themeisle: '\f2b2';
$fa-var-thermometer: '\f2c7';
$fa-var-thermometer-0: '\f2cb';
$fa-var-thermometer-1: '\f2ca';
$fa-var-thermometer-2: '\f2c9';
$fa-var-thermometer-3: '\f2c8';
$fa-var-thermometer-4: '\f2c7';
$fa-var-thermometer-empty: '\f2cb';
$fa-var-thermometer-full: '\f2c7';
$fa-var-thermometer-half: '\f2c9';
$fa-var-thermometer-quarter: '\f2ca';
$fa-var-thermometer-three-quarters: '\f2c8';
$fa-var-thumb-tack: '\f08d';
$fa-var-thumbs-down: '\f165';
$fa-var-thumbs-o-down: '\f088';
$fa-var-thumbs-o-up: '\f087';
$fa-var-thumbs-up: '\f164';
$fa-var-ticket: '\f145';
$fa-var-times: '\f00d';
$fa-var-times-circle: '\f057';
$fa-var-times-circle-o: '\f05c';
$fa-var-times-rectangle: '\f2d3';
$fa-var-times-rectangle-o: '\f2d4';
$fa-var-tint: '\f043';
$fa-var-toggle-down: '\f150';
$fa-var-toggle-left: '\f191';
$fa-var-toggle-off: '\f204';
$fa-var-toggle-on: '\f205';
$fa-var-toggle-right: '\f152';
$fa-var-toggle-up: '\f151';
$fa-var-trademark: '\f25c';
$fa-var-train: '\f238';
$fa-var-transgender: '\f224';
$fa-var-transgender-alt: '\f225';
$fa-var-trash: '\f1f8';
$fa-var-trash-o: '\f014';
$fa-var-tree: '\f1bb';
$fa-var-trello: '\f181';
$fa-var-tripadvisor: '\f262';
$fa-var-trophy: '\f091';
$fa-var-truck: '\f0d1';
$fa-var-try: '\f195';
$fa-var-tty: '\f1e4';
$fa-var-tumblr: '\f173';
$fa-var-tumblr-square: '\f174';
$fa-var-turkish-lira: '\f195';
$fa-var-tv: '\f26c';
$fa-var-twitch: '\f1e8';
$fa-var-twitter: '\f099';
$fa-var-twitter-square: '\f081';
$fa-var-umbrella: '\f0e9';
$fa-var-underline: '\f0cd';
$fa-var-undo: '\f0e2';
$fa-var-universal-access: '\f29a';
$fa-var-university: '\f19c';
$fa-var-unlink: '\f127';
$fa-var-unlock: '\f09c';
$fa-var-unlock-alt: '\f13e';
$fa-var-unsorted: '\f0dc';
$fa-var-upload: '\f093';
$fa-var-usb: '\f287';
$fa-var-usd: '\f155';
$fa-var-user: '\f007';
$fa-var-user-circle: '\f2bd';
$fa-var-user-circle-o: '\f2be';
$fa-var-user-md: '\f0f0';
$fa-var-user-o: '\f2c0';
$fa-var-user-plus: '\f234';
$fa-var-user-secret: '\f21b';
$fa-var-user-times: '\f235';
$fa-var-users: '\f0c0';
$fa-var-vcard: '\f2bb';
$fa-var-vcard-o: '\f2bc';
$fa-var-venus: '\f221';
$fa-var-venus-double: '\f226';
$fa-var-venus-mars: '\f228';
$fa-var-viacoin: '\f237';
$fa-var-viadeo: '\f2a9';
$fa-var-viadeo-square: '\f2aa';
$fa-var-video-camera: '\f03d';
$fa-var-vimeo: '\f27d';
$fa-var-vimeo-square: '\f194';
$fa-var-vine: '\f1ca';
$fa-var-vk: '\f189';
$fa-var-volume-control-phone: '\f2a0';
$fa-var-volume-down: '\f027';
$fa-var-volume-off: '\f026';
$fa-var-volume-up: '\f028';
$fa-var-warning: '\f071';
$fa-var-wechat: '\f1d7';
$fa-var-weibo: '\f18a';
$fa-var-weixin: '\f1d7';
$fa-var-whatsapp: '\f232';
$fa-var-wheelchair: '\f193';
$fa-var-wheelchair-alt: '\f29b';
$fa-var-wifi: '\f1eb';
$fa-var-wikipedia-w: '\f266';
$fa-var-window-close: '\f2d3';
$fa-var-window-close-o: '\f2d4';
$fa-var-window-maximize: '\f2d0';
$fa-var-window-minimize: '\f2d1';
$fa-var-window-restore: '\f2d2';
$fa-var-windows: '\f17a';
$fa-var-won: '\f159';
$fa-var-wordpress: '\f19a';
$fa-var-wpbeginner: '\f297';
$fa-var-wpexplorer: '\f2de';
$fa-var-wpforms: '\f298';
$fa-var-wrench: '\f0ad';
$fa-var-xing: '\f168';
$fa-var-xing-square: '\f169';
$fa-var-y-combinator: '\f23b';
$fa-var-y-combinator-square: '\f1d4';
$fa-var-yahoo: '\f19e';
$fa-var-yc: '\f23b';
$fa-var-yc-square: '\f1d4';
$fa-var-yelp: '\f1e9';
$fa-var-yen: '\f157';
$fa-var-yoast: '\f2b1';
$fa-var-youtube: '\f167';
$fa-var-youtube-play: '\f16a';
$fa-var-youtube-square: '\f166';

View File

@ -0,0 +1,18 @@
/*!
* Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
* License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
*/
@import 'variables';
@import 'mixins';
@import 'path';
@import 'core';
@import 'larger';
@import 'fixed-width';
@import 'list';
@import 'bordered-pulled';
@import 'animated';
@import 'rotated-flipped';
@import 'stacked';
@import 'icons';
@import 'screen-reader';

View File

@ -0,0 +1,19 @@
@font-face {
font-family: "iconfont"; /* Project id 3814452 */
src: url('iconfont.woff2?t=1670405177902') format('woff2'),
url('iconfont.woff?t=1670405177902') format('woff'),
url('iconfont.ttf?t=1670405177902') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-enterprise:before {
content: "\e724";
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

36
public/iframe/index.html Normal file
View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title></title>
<script></script>
<style>
html,
body,
.box {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
.iframe {
width: 100%;
margin: 0;
padding: 0;
margin-top: -124px;
height: calc(100% + 124px);
}
</style>
</head>
<body>
<div class="box">
<iframe id="iframe" frameborder="no" class="iframe" src=""></iframe>
</div>
</body>
<script>
const root = 'iframe/index.html?url='
const href = window.location.href
const idx = href.indexOf(root)
document.getElementById('iframe').src = href.substring(idx + root.length)
</script>
</html>

36
src/App.vue Normal file
View File

@ -0,0 +1,36 @@
<script setup lang="ts">
// import enUS from '@arco-design/web-vue/es/locale/lang/en-us'
import { provide } from 'vue'
import { getStore } from '@/utils/storage'
import systemInfo from '@/mixins/system_info'
import { useVersionUpdateListener } from '@/hooks/version'
if (getStore('darkMode') === true) {
document.body.setAttribute('arco-theme', 'dark')
}
provide('systemInfo', systemInfo)
onMounted(() => {
useVersionUpdateListener()
})
</script>
<template>
<a-config-provider> <router-view></router-view></a-config-provider>
</template>
<style lang="scss">
html,
body,
#app {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--color-bg-3);
}
</style>

25
src/api/api.ts Normal file
View File

@ -0,0 +1,25 @@
import axios from '@/utils/axios'
import { encrypt } from '@/utils/crypto'
export function login(form: any) {
// 处理逻辑
const timestamp = new Date().getTime().toString()
return axios.post('/sys/api/login', {
secret: timestamp,
captchaVerification: form.captchaVerification,
phone: encrypt(timestamp + form.phone),
password: encrypt(timestamp + form.password)
})
}
export function forget(form: any) {
// 处理逻辑
const timestamp = new Date().getTime().toString()
return axios.post('/sys/api/forget', {
secret: timestamp,
email: encrypt(timestamp + form.email),
password: encrypt(timestamp + form.password),
code: encrypt(timestamp + form.code),
inviteCode: form.inviteCode
})
}

45
src/api/upms/dept.ts Normal file
View File

@ -0,0 +1,45 @@
import axios from '@/utils/axios'
/**
*
* @returns Promise
*/
export function fetchAllTree() {
return axios.post('/sys/dept/fetch/all')
}
/**
*
* @returns Promise
*/
export function fetchUserTree() {
return axios.post('/sys/dept/fetch/user')
}
/**
* id获取用户ids
*
* @param deptId id
* @return ids
* @date 2022.12.12
*/
export function userIdsByDeptId(deptId: string) {
return axios.post(`/sys/dept/user/ids/by/${deptId}`)
}
/**
*
* @param deptId id
* @param userIds ids
*/
export function resetDeptUser(deptId: string, userIds: string[]) {
return axios.post('/sys/dept/reset/dept/user', { id: deptId, userIds })
}
/**
*
* @param deptId id
* @param userId id
*/
export function removeDeptUser(deptId: string, userId: string) {
return axios.post('/sys/dept/remove/dept/user', { deptId, userId })
}

30
src/api/upms/group.ts Normal file
View File

@ -0,0 +1,30 @@
import axios from '@/utils/axios'
/**
* id获取用户ids
*
* @param groupId id
* @return ids
* @date 2022.12.12
*/
export function userIdsByGroupId(groupId: string) {
return axios.post(`/sys/group/user/ids/by/${groupId}`)
}
/**
*
* @param groupId id
* @param userIds ids
*/
export function userReset(groupId: string, userIds: string[]) {
return axios.post('/sys/group/user/reset', { id: groupId, userIds })
}
/**
*
* @param groupId id
* @param userId id
*/
export function userRemove(groupId: string, userId: string) {
return axios.post('/sys/group/user/remove', { groupId, userId })
}

26
src/api/upms/menu.ts Normal file
View File

@ -0,0 +1,26 @@
import axios from '@/utils/axios'
/**
*
* @returns Promise
*/
// eslint-disable-next-line import/prefer-default-export
export function getMenuTree() {
return axios.post('/sys/menu/tree')
}
export function getMenuTreeByRoleId(roleId: string) {
return axios.post(`/sys/menu/tree/role/${roleId}`)
}
export function editRoleMenus(roleId: string, menuIds: Array<string>) {
return axios.post(`/sys/menu/role/${roleId}`, menuIds)
}
export function getMenuTreeByPackageId(packageId: string) {
return axios.post(`/sys/menu/tree/package/${packageId}`)
}
export function editPackageMenus(packageId: string, menuIds: Array<string>) {
return axios.post(`/sys/menu/package/${packageId}`, menuIds)
}

View File

@ -0,0 +1,19 @@
import { SysNotifyChannel } from '@/entity/upms/notify-channel'
import axios from '@/utils/axios'
const baseUrl = 'sys/notify/channel'
/**
*
* @returns Array<SysNotifyChannel>
*/
export function getConfig() {
return axios.post(`${baseUrl}/get/config`)
}
/**
*
* @param data {type,config}
*/
export function editConfig(data: SysNotifyChannel) {
return axios.post(`${baseUrl}/edit/config`, data)
}

10
src/api/upms/user.ts Normal file
View File

@ -0,0 +1,10 @@
import axios from '@/utils/axios'
/**
*
* @param password
* @param newPassword
*/
export function updatePassword(password: string, newPassword: string) {
return axios.post('/sys/user/password', { password, newPassword })
}

71
src/assets/css/arco.scss Normal file
View File

@ -0,0 +1,71 @@
// 设置暗黑模式下spin的背景色透明
body[arco-theme='dark'] {
.arco-spin-mask {
background-color: unset !important;
}
}
.arco-modal-body {
max-height: calc(100vh - 48px - 65px - 48px);
overflow-y: auto;
overflow-x: hidden;
// 未引入tailwind需要此
// width: calc(100% - 40px);
}
@media screen and (max-width: 530px) {
.arco-modal {
width: 94vw;
}
}
.arco-scrollbar-thumb-direction-vertical .arco-scrollbar-thumb-bar {
width: 7px;
margin: 0 5px;
}
.arco-popconfirm-content {
white-space: pre-wrap;
}
// 自定义arco表格滚动条样式
.arco-table-content-scroll-x,
.arco-table-content-scroll-y,
.arco-table.arco-table-empty .arco-table-header {
/*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/
&::-webkit-scrollbar {
background-color: var(--color-fill-1);
border-bottom-right-radius: var(--border-radius-medium);
}
/*定义滚动条轨道 内阴影+圆角*/
&::-webkit-scrollbar-track {
border-radius: 6px;
}
/*定义滑块 内阴影+圆角*/
&::-webkit-scrollbar-thumb {
border-radius: 6px;
background-color: var(--color-neutral-4);
cursor: pointer;
&:hover {
background-color: var(--color-neutral-5);
}
&:active {
background-color: var(--color-neutral-6);
}
}
}
.arco-table-content-scroll-x,
.arco-table.arco-table-empty .arco-table-header {
&::-webkit-scrollbar {
height: 9px;
}
}
.arco-table-content-scroll-y {
&::-webkit-scrollbar {
width: 9px;
}
}
.arco-trigger-popup{
white-space:pre-wrap;
}

274
src/assets/css/flex.scss Normal file
View File

@ -0,0 +1,274 @@
.un-select {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-user-drag: none;
img {
-webkit-user-drag: none;
}
}
.flex-view {
position: relative;
display: flex;
flex-direction: column;
}
.flex {
display: flex;
}
/* Flexbox 布局 */
/* Flex Direction */
.flex-row {
@extend .flex-view;
flex-direction: row;
}
.flex-row-reverse {
flex-direction: row-reverse !important;
@extend .flex-view;
}
.flex-column {
flex-direction: column !important;
@extend .flex-view;
}
.flex-column-reverse {
flex-direction: column-reverse !important;
@extend .flex-view;
}
/* Flex Wrap */
.flex-nowrap {
flex-wrap: nowrap !important;
@extend .flex-view;
}
.flex-wrap {
flex-wrap: wrap !important;
@extend .flex-view;
}
.flex-wrap-reverse {
flex-wrap: wrap-reverse !important;
@extend .flex-view;
}
/* Align Items */
.align-stretch {
align-items: stretch !important;
@extend .flex-view;
}
.align-start {
align-items: flex-start !important;
@extend .flex-view;
}
.align-center {
align-items: center !important;
@extend .flex-view;
}
.align-end {
align-items: flex-end !important;
@extend .flex-view;
}
/* Justify Content */
.justify-start {
justify-content: flex-start !important;
@extend .flex-view;
}
.justify-center {
justify-content: center !important;
@extend .flex-view;
}
.justify-end {
justify-content: flex-end !important;
@extend .flex-view;
}
.justify-between {
justify-content: space-between !important;
@extend .flex-view;
}
.justify-around {
justify-content: space-around !important;
@extend .flex-view;
}
/* 字体粗细 */
.text-bold {
font-weight: bold;
}
/* 文本对齐 */
.text-left {
text-align: left;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
/* 文本装饰 */
.text-underline {
text-decoration: underline;
}
.text-through {
text-decoration: line-through;
}
/* flex */
.flex-1 {
flex: 1 !important;
}
.flex-2 {
flex: 2 !important;
}
.flex-3 {
flex: 3 !important;
}
.flex-4 {
flex: 4 !important;
}
.flex-5 {
flex: 5 !important;
}
.flex-6 {
flex: 6 !important;
}
.flex-7 {
flex: 7 !important;
}
.flex-8 {
flex: 8 !important;
}
/* 文本大小 */
.text-xs {
font-size: 12px !important;
}
.text-sm {
font-size: 14px !important;
}
.text-md {
font-size: 16px !important;
}
.text-lg {
font-size: 20px !important;
}
.text-xl {
font-size: 24px !important;
}
/* 行高 */
.leading-xs {
line-height: 14px;
}
.leading-sm {
line-height: 16px;
}
.leading-md {
line-height: 20px;
}
.leading-lg {
line-height: 24px;
}
.leading-xl {
line-height: 28px;
}
@mixin lines($num) {
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: $num;
lines: $num;
}
/* 超出行省略 */
.lines-1 {
@include lines(1);
}
.lines-2 {
@include lines(2);
}
.lines-3 {
@include lines(3);
}
.lines-4 {
@include lines(4);
}
.lines-5 {
@include lines(5);
}
/* 定位 */
.absolute {
position: absolute !important;
}
.absolute-screen {
position: absolute !important;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 998;
}
.fixed {
position: fixed !important;
}
.fixed-screen {
position: fixed !important;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 999;
}
.sticky {
position: sticky !important;
top: 0;
z-index: 969;
}
/* 内外边距 */
$margin-map: (
xs: 8px,
sm: 16px,
md: 24px,
lg: 32px,
xl: 48px
);
@each $parameter in xs, sm, md, lg, xl {
.ma-#{$parameter} {
margin: map-get($map: $margin-map, $key: $parameter);
}
.mt-#{$parameter} {
margin-top: map-get($map: $margin-map, $key: $parameter);
}
.mb-#{$parameter} {
margin-bottom: map-get($map: $margin-map, $key: $parameter);
}
.ml-#{$parameter} {
margin-left: map-get($map: $margin-map, $key: $parameter);
}
.mr-#{$parameter} {
margin-right: map-get($map: $margin-map, $key: $parameter);
}
.mx-#{$parameter} {
margin-left: map-get($map: $margin-map, $key: $parameter);
margin-right: map-get($map: $margin-map, $key: $parameter);
}
.my-#{$parameter} {
margin-top: map-get($map: $margin-map, $key: $parameter);
margin-bottom: map-get($map: $margin-map, $key: $parameter);
}
.pa-#{$parameter} {
padding: map-get($map: $margin-map, $key: $parameter);
}
.pt-#{$parameter} {
padding-top: map-get($map: $margin-map, $key: $parameter);
}
.pb-#{$parameter} {
padding-bottom: map-get($map: $margin-map, $key: $parameter);
}
.pl-#{$parameter} {
padding-left: map-get($map: $margin-map, $key: $parameter);
}
.pr-#{$parameter} {
padding-right: map-get($map: $margin-map, $key: $parameter);
}
.px-#{$parameter} {
padding-left: map-get($map: $margin-map, $key: $parameter);
padding-right: map-get($map: $margin-map, $key: $parameter);
}
.py-#{$parameter} {
padding-top: map-get($map: $margin-map, $key: $parameter);
padding-bottom: map-get($map: $margin-map, $key: $parameter);
}
}

View File

@ -0,0 +1,28 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Flexbox 布局 */
/* Flex Direction */
.flex-row {
display: flex;
}
.flex-row-reverse {
display: flex;
}
.flex-col {
display: flex;
}
.flex-col-reverse {
display: flex;
}
/* 解决冲突 */
.arco-switch{
background-color: var(--color-fill-4);
-webkit-appearance:unset;
background-image:unset;
}
.arco-switch-checked {
background-color: rgb(var(--primary-6));
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

14
src/axios.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
import { AxiosRequestConfig } from 'axios'
declare module 'axios' {
export interface AxiosInstance {
<T = any>(config: AxiosRequestConfig): Promise<T>
request<T = any>(config: AxiosRequestConfig): Promise<T>
get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>
delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>
head<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>
post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>
put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>
patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>
}
}

View File

@ -0,0 +1,62 @@
import { computed, ref } from 'vue'
const searchData = (val: string | undefined, data: any[]): any[] => {
if (!val) {
return data
}
const result: any[] = []
data.forEach((item) => {
if (item.name.toLowerCase().indexOf(val.toLowerCase()) > -1) {
result.push({ ...item })
} else if (item.children) {
const filterSearchData = searchData(val, item.children)
if (filterSearchData.length) {
result.push({
...item,
children: filterSearchData
})
}
}
})
return result
}
const filterData = (tree: any[], ignoreIds: string[]) => {
const traverse = (nodes: any) => {
const result: any[] = []
nodes.forEach((node: any) => {
if (!ignoreIds.includes(node.id)) {
const children = traverse(node.children)
result.push({
...node,
children: children || []
})
}
})
return result
}
return traverse(tree)
}
export default (ignoreIds?: string[]) => {
const searchKey = ref('')
const treeData = ref([])
const searchTreeData = computed(() => searchData(searchKey.value, treeData.value))
const realTreeData = computed(() => filterData(searchTreeData.value, ignoreIds || []))
let timeout: any
const inputChange = (val: string) => {
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
searchKey.value = val
}, 300)
}
return {
searchKey,
treeData,
realTreeData,
inputChange
}
}

View File

@ -0,0 +1,36 @@
<template>
<template v-if="index < 0">{{ nodeTitleArr[0] }}</template>
<span v-else>
{{ nodeTitleArr[0]
}}<span style="color: rgb(var(--primary-5)); font-weight: bold"> {{ nodeTitleArr[1] }}</span
>{{ nodeTitleArr[2] }}
</span>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
searchKey: string
itemData: any
titleField: string
}>()
const getMatchIndex = (title: string) => {
if (!props.searchKey) return -1
return title.toLowerCase().indexOf(props.searchKey.toLowerCase())
}
const index = computed(() => getMatchIndex(props.itemData[props.titleField || 'title']))
const nodeTitleArr = computed<string[]>(() => {
const title: string = props.itemData[props.titleField || 'title'] || ''
if (index.value < 0) {
return [title]
}
return [
title.substring(0, index.value),
title.substring(index.value, index.value + props.searchKey.length),
title.substring(index.value + props.searchKey.length)
]
})
</script>

View File

@ -0,0 +1,126 @@
<template>
<a-skeleton :animation="true" :loading="loading">
<a-card>
<a-skeleton-line :rows="8" />
</a-card>
<template #content>
<a-card
:body-style="{ minHeight: '100px', padding: '8px' }"
:header-style="{ padding: '8px 3px 8px 8px' }"
>
<template #title>
<div class="align-center flex-row">
<a-input
class="flex-1"
:allow-clear="true"
placeholder="搜索部门.."
@input="inputChange"
@clear="inputChange('')"
><template #prefix> <icon-search /> </template
></a-input>
<a-tooltip content="仅展示部门的直属成员" :popup-visible="deptTooltipVisible">
<template #content>
仅展示部门的直属成员
<a-tooltip content="不再提示">
<icon-close style="cursor: pointer" @click="handleCloseDeptTooltip" />
</a-tooltip>
</template>
<a-tooltip content="仅展示部门的直属成员">
<a-checkbox
style="padding-right: 5px"
v-model="innerOnly"
@change="handleCheckboxChange"
></a-checkbox>
</a-tooltip>
</a-tooltip>
</div>
</template>
<a-empty v-if="realTreeData.length === 0" description="没有匹配的部门"></a-empty>
<a-tree
v-else
:data="realTreeData"
:field-names="fieldNames"
:block-node="true"
:default-expand-all="true"
@select="handleTreeSelect"
>
<template #icon="{ node }">
<div v-if="node.id === '0'" class="iconfont icon-enterprise dept-tree-icon"></div>
<icon-user-group class="dept-tree-icon" v-else />
</template>
<template #title="nodeData">
<item-node :item-data="nodeData" :search-key="searchKey" title-field="name"></item-node>
</template>
</a-tree>
</a-card>
</template>
</a-skeleton>
</template>
<script setup lang="ts">
import { TreeFieldNames } from '@arco-design/web-vue'
import { IconSearch, IconUserGroup, IconClose } from '@arco-design/web-vue/es/icon'
import { ref } from 'vue'
import { fetchAllTree } from '@/api/upms/dept'
import useSearch from '../_hooks/use-tree-search'
import useDeptTooltip from './use-dept-tooltip'
import ItemNode from './f-dept-tree-item-node.vue'
const emit = defineEmits<{
(event: 'change', val: { deptId?: string; innerOnly: boolean }): void
}>()
const { deptTooltipVisible, handleCloseDeptTooltip } = useDeptTooltip()
const { searchKey, treeData, realTreeData, inputChange } = useSearch()
const fieldNames: TreeFieldNames = {
key: 'id',
title: 'name'
}
const loading = ref(true)
fetchAllTree().then((res) => {
treeData.value = res
loading.value = false
})
const selectDeptId = ref<string | undefined>(undefined)
const innerOnly = ref(false)
const handleCheckboxChange = () => {
emit('change', {
deptId: selectDeptId.value,
innerOnly: innerOnly.value
})
}
const handleTreeSelect = (keys: (string | number)[]) => {
if (selectDeptId.value === keys[0]) {
return
}
// eslint-disable-next-line prefer-destructuring
selectDeptId.value = keys[0] as string
emit('change', {
deptId: selectDeptId.value,
innerOnly: innerOnly.value
})
}
</script>
<style scoped lang="scss">
.dept-tree-icon {
padding-bottom: 2px;
}
.arco-avatar {
margin-bottom: 2px;
}
::v-deep(.arco-tree-node-icon) {
margin-right: 6px;
}
.arco-tree-node-selected {
.dept-tree-icon {
color: rgb(var(--primary-6));
}
}
</style>

View File

@ -0,0 +1,21 @@
import { ref } from 'vue'
import { getStore, setStore } from '@/utils/storage'
export default () => {
const deptTooltipVisible = ref(false)
if (getStore('dept_tooltip_visible') !== true) {
setTimeout(() => {
deptTooltipVisible.value = true
setTimeout(() => {
deptTooltipVisible.value = false
}, 3000)
}, 1000)
}
const handleCloseDeptTooltip = () => {
setStore('dept_tooltip_visible', true)
}
return {
deptTooltipVisible,
handleCloseDeptTooltip
}
}

View File

@ -0,0 +1,41 @@
<template>
<a-avatar :customClass="customClass" :shape="shape" :size="size" v-if="src">
<img alt="avatar" :src="src" />
</a-avatar>
<a-avatar :customClass="customClass" :shape="shape" :size="size" v-else-if="text">
{{ showText }}
</a-avatar>
<a-avatar :customClass="customClass" :shape="shape" :size="size" v-else>
<IconUser />
</a-avatar>
</template>
<script setup lang="ts">
import { computed, withDefaults } from 'vue'
import { IconUser } from '@arco-design/web-vue/es/icon'
const props = withDefaults(
defineProps<{
customClass?: string
shape?: 'circle' | 'square'
size?: number
text?: string
firstText?: boolean
src?: string
}>(),
{
shape: 'square',
text: '',
firstText: true,
size: 20
}
)
const showText = computed(() => {
if (!props.text) {
return ''
}
return props.firstText ? props.text.substring(0, 1) : props.text
})
</script>
<style scoped></style>

View File

@ -0,0 +1,36 @@
<template>
<span v-if="index < 0" class="select-none">{{ nodeTitleArr[0] }}</span>
<span v-else class="select-none">
{{ nodeTitleArr[0]
}}<span style="color: rgb(var(--primary-5)); font-weight: bold"> {{ nodeTitleArr[1] }}</span
>{{ nodeTitleArr[2] }}
</span>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
searchKey: string
itemData: any
titleField: string
}>()
const getMatchIndex = (title: string) => {
if (!props.searchKey) return -1
return title.toLowerCase().indexOf(props.searchKey.toLowerCase())
}
const index = computed(() => getMatchIndex(props.itemData[props.titleField || 'title']))
const nodeTitleArr = computed<string[]>(() => {
const title: string = props.itemData[props.titleField || 'title'] || ''
if (index.value < 0) {
return [title]
}
return [
title.substring(0, index.value),
title.substring(index.value, index.value + props.searchKey.length),
title.substring(index.value + props.searchKey.length)
]
})
</script>

View File

@ -0,0 +1,270 @@
<template>
<a-modal
v-model:visible="myVisible"
title="选择成员"
:unmount-on-close="true"
:footer="false"
:width="width"
:mask-closable="false"
modal-class="user-select-modal"
>
<div class="user-select-modal-body flex-row">
<div class="tree-box flex-1">
<div class="search-box">
<a-input
:allow-clear="true"
placeholder="搜索"
@input="inputChange"
@clear="inputChange('')"
>
<template #prefix>
<icon-search />
</template>
</a-input>
</div>
<div
v-if="realTreeData.length === 0"
:style="{ height: `${height - 20}px` }"
class="justify-center"
>
<a-empty description="没有匹配的部门/成员"></a-empty>
</div>
<a-tree
v-else
:checked-keys="checkedKeys"
:checkable="checkedKeys.length < limit"
:data="realTreeData"
:field-names="{
key: 'id',
title: 'name'
}"
:block-node="true"
:default-expand-all="true"
:virtual-list-props="{
height: `${height - 20}px`,
fixedSize: true,
buffer: 30
}"
:disabled="checkedKeys.length === limit"
@check="handleCheck"
@select="handleSelect"
>
<template #icon="{ node }">
<div v-if="node.id === '0'" class="iconfont icon-enterprise dept-tree-icon"></div>
<f-user-avatar
v-else-if="node.type === 'user'"
custom-class="dept-tree-icon"
:text="node.name"
:src="node.avatarUrl"
></f-user-avatar>
<icon-user-group class="dept-tree-icon" v-else />
</template>
<template #title="nodeData">
<item-node :item-data="nodeData" :search-key="searchKey" title-field="name"></item-node>
</template>
</a-tree>
</div>
<div class="list-box flex-1">
<div>
<div style="margin-bottom: 10px">
<a-typography-text>
{{
checkedKeys.length === 0
? '请从左侧选择成员'
: `已选择了${checkedKeys.length}位成员`
}}{{ checkedKeys.length === limit ? `` : '' }} </a-typography-text
><a-typography-text type="danger">
{{ checkedKeys.length === limit ? `最多选择${limit}` : '' }}
</a-typography-text>
</div>
<a-scrollbar
style="overflow-y: auto; padding-right: 20px"
:style="{ height: `${height - 54}px` }"
>
<div
v-for="node in checkedNodes"
:key="node.id"
class="align-center flex-row justify-between"
style="padding: 6px 0"
>
<div class="align-center flex-row">
<a-avatar style="margin-right: 10px" shape="square" :size="20"
>{{ node.name.substring(0, 1) }}
</a-avatar>
<a-typography-text>{{ node.name }}</a-typography-text>
</div>
<div style="padding: 0 4px; cursor: pointer" @click="handleUncheck(node)">
<icon-close-circle-fill />
</div>
</div>
</a-scrollbar>
<div class="flex-row justify-end" style="padding-right: 20px; margin-top: 10px">
<a-space>
<a-button @click="handleCancel">取消</a-button>
<c-button type="primary" @click="handleSubmit">确定</c-button>
</a-space>
</div>
</div>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { computed, nextTick, ref, watch, watchEffect, withDefaults } from 'vue'
import { IconCloseCircleFill, IconUserGroup } from '@arco-design/web-vue/es/icon'
import _ from 'lodash'
import useSearch from '../_hooks/use-tree-search'
import { fetchUserTree } from '@/api/upms/dept'
import useCheck from './use-check'
import useAdaptSize from '@/components/f-user-select-modal/use-adapt-size'
import FUserAvatar from '../f-user-avatar/index.vue'
import { searchUserList } from './use-search'
import ItemNode from './f-user-select-modal-item-node.vue'
const props = withDefaults(
defineProps<{
visible: boolean
limit?: number
selectIds?: string[]
/**
* 忽略的ids
*/
ignoreIds?: string[]
autoClose?: boolean
}>(),
{
limit: 99,
selectIds: () => [],
ignoreIds: () => [],
autoClose: true
}
)
const emit = defineEmits<{
(event: 'update:visible', val: boolean): void
(event: 'userListChange', val: any[]): void
(event: 'change', checkKeys: string[], checkNodes: any[]): void
(event: 'change', checkKeys: string[], checkNodes: any[], done: Function): void
}>()
// search
const { searchKey, treeData, realTreeData, inputChange } = useSearch(props.ignoreIds)
const userListData = computed(() => searchUserList(treeData.value))
// check
const { handleSelect, handleCheck, handleUncheck, synchronize, checkedKeys, checkedNodes } =
useCheck(treeData, props.limit)
// load data
const load = () => {
fetchUserTree().then((res) => {
treeData.value = res
nextTick(() => {
emit('userListChange', userListData.value)
})
synchronize()
})
}
load()
// v-model:select-ids
watch(
() => props.selectIds,
() => {
checkedKeys.value = _.cloneDeep(props.selectIds)
synchronize()
},
{
immediate: true,
deep: true
}
)
// cal height
const { width, height } = useAdaptSize()
// modal visible
const myVisible = ref(false)
const backSelectIds = ref<string[]>([])
watchEffect(() => {
emit('update:visible', myVisible.value)
})
watch(
() => props.visible,
() => {
myVisible.value = props.visible
// ids
if (myVisible.value) {
backSelectIds.value = _.cloneDeep(checkedKeys.value)
}
searchKey.value = ''
}
)
// operation
const handleCancel = () => {
myVisible.value = false
// ids
checkedKeys.value = backSelectIds.value
synchronize()
}
const handleSubmit = (done: Function) => {
emit('change', _.cloneDeep(checkedKeys.value), _.cloneDeep(checkedNodes.value), (flag: any) => {
if (flag !== false) {
myVisible.value = false
load()
}
done()
})
if (props.autoClose) {
myVisible.value = false
done()
load()
}
}
defineExpose({
load
})
</script>
<style lang="scss">
.user-select-modal {
.arco-modal-header {
display: none;
}
.arco-modal-body {
padding: 0;
width: 100%;
max-height: 100vh;
overflow: hidden;
}
.tree-box,
.list-box {
padding: 20px 0 20px 20px;
}
.tree-box {
border-right: 1px solid var(--color-neutral-3);
.search-box {
padding-right: 20px;
padding-bottom: 8px;
}
}
}
.user-select-modal-body {
.arco-virtual-list {
overflow-x: hidden !important;
}
.arco-tree-node-selected {
// .arco-avatar {
// background-color: rgb(var(--primary-5));
// }
.arco-tree-node-title {
color: var(--color-text-1);
}
}
.arco-card-body {
padding: 0;
}
.arco-tree-node-title-text {
min-width: 80px;
}
}
</style>

View File

@ -0,0 +1,44 @@
import { inject, computed, Ref } from 'vue'
import { isUndefined } from 'lodash'
import { SystemInfo } from '@/types/types'
export default () => {
const systemInfo = inject<Ref<SystemInfo>>('systemInfo')
const height = computed(() => {
if (isUndefined(systemInfo)) {
return 100
}
if (systemInfo.value.clientHeight > 1000) {
return 900
}
if (systemInfo.value.clientHeight > 800) {
return 600
}
if (systemInfo.value.clientHeight > 600) {
return 440
}
if (systemInfo.value.clientHeight > 500) {
return 440
}
return systemInfo.value.clientHeight - 60
})
const width = computed(() => {
if (!systemInfo) {
return '520px'
}
if (systemInfo.value.clientWidth > 1920) {
return '720px'
}
if (systemInfo.value.clientWidth > 1360) {
return '620px'
}
if (systemInfo.value.clientWidth < 600) {
return `${systemInfo.value.clientWidth}px`
}
return '520px'
})
return {
width,
height
}
}

View File

@ -0,0 +1,115 @@
import { TreeNodeData } from '@arco-design/web-vue'
import { Ref, ref } from 'vue'
import _, { isUndefined } from 'lodash'
/**
* ids搜索出nodes
*
* @param ids ids
* @param tree
*/
const searchTree = (ids: string[], tree: any[]): any[] => {
const finds: any[] = []
if (tree) {
tree.forEach((node) => {
if (node) {
if (ids.findIndex((id) => id === node.id) >= 0) {
finds.push(node)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const findChild = searchTree(ids, node.children)
finds.push(...findChild)
}
})
}
return _.unionWith(finds, (arrVal, othVal) => {
return arrVal.id === othVal.id
})
}
export default (treeData: Ref<any[]>, limit: number) => {
/**
* ids
*/
const checkedKeys = ref<string[]>([])
/**
* nodes
*/
const checkedNodes = ref<any[]>([])
const canCheck = computed(() => limit > checkedKeys.value.length)
/**
* checkedKeys和checkedNodes
* @param node {id,name,...}
*/
const checkChange = (node?: any) => {
if (!isUndefined(node)) {
const idx = checkedKeys.value.findIndex((item) => item === node.id)
if (idx >= 0) {
checkedKeys.value.splice(idx, 1)
checkedNodes.value.splice(idx, 1)
} else {
if (!canCheck.value) {
checkedKeys.value.splice(checkedKeys.value.length - 1, 1)
checkedNodes.value.splice(checkedNodes.value.length - 1, 1)
}
checkedKeys.value.push(node.id)
checkedNodes.value.push(node)
}
}
}
/**
* a-tree的@select事件
* @param keys keys,string[]
* @param data
*/
const handleSelect = (
keys: (string | number)[],
data: { selected?: boolean; selectedNodes: TreeNodeData[]; node?: TreeNodeData; e?: Event }
) => {
if (data.node && data.node.checkable) {
checkChange(data.node)
}
}
/**
* a-tree的@check事件
* @param keys keys,string[]
* @param data
*/
const handleCheck = (
keys: Array<string | number>,
data: {
checked?: boolean
checkedNodes: TreeNodeData[]
node?: TreeNodeData
e?: Event
halfCheckedKeys: (string | number)[]
halfCheckedNodes: TreeNodeData[]
}
) => {
if (data.node && data.node.checkable) {
checkChange(data.node)
}
}
/**
*
* @param node {id,name,...}
*/
const handleUncheck = (node: any) => {
checkChange(node)
}
/**
* checkedNodes根据checkedKeys同步数据
*/
const synchronize = () => {
checkedNodes.value = searchTree(checkedKeys.value, treeData.value)
}
return {
checkedKeys,
checkedNodes,
handleSelect,
handleUncheck,
handleCheck,
synchronize
}
}

View File

@ -0,0 +1,24 @@
import _ from 'lodash'
/**
* list
*
* @param tree
*/
export const searchUserList = (tree: any[]): any[] => {
const finds: any[] = []
if (tree) {
tree.forEach((node) => {
if (node) {
if (node.type === 'user') {
finds.push(node)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const findChild = searchUserList(node.children)
finds.push(...findChild)
}
})
}
return _.unionWith(finds, (arrVal, othVal) => {
return arrVal.id === othVal.id
})
}

View File

@ -0,0 +1,93 @@
<template>
<a-select
:popup-visible="false"
:fallback-option="false"
:multiple="limit === 1 ? false : true"
:allow-search="false"
:allow-clear="allowClear"
:placeholder="placeholder"
@popup-visible-change="handleVisible"
@remove="handleRemove"
@clear="handleRemove"
v-model="mySelectIds"
>
<a-option v-for="item of options" :key="item.id" :value="item.id" :label="item.name" />
</a-select>
<f-user-select-modal
v-model:visible="modalVisible"
:select-ids="mySelectIds"
:limit="limit"
:ignoreIds="ignoreIds"
@change="handleChange"
@user-list-change="handleUserListChange"
></f-user-select-modal>
</template>
<script setup lang="ts">
import { ref, watch, withDefaults } from 'vue'
import _ from 'lodash'
import FUserSelectModal from '../f-user-select-modal/index.vue'
const props = withDefaults(
defineProps<{
/**
* 已选择
*/
selectIds?: string[]
/**
* 忽略的ids
*/
ignoreIds?: string[]
/**
* 最多可选几个
*/
limit?: number
allowClear?: boolean
placeholder?: string
}>(),
{
allowClear: true,
selectIds: () => [],
ignoreIds: () => [],
placeholder: '请选择人员'
}
)
const emit = defineEmits<{
(event: 'change', val: string[]): void
}>()
const modalVisible = ref(false)
const handleVisible = () => {
modalVisible.value = true
}
const mySelectIds = ref<string[]>([])
watch(
() => props.selectIds,
() => {
mySelectIds.value = _.cloneDeep(props.selectIds || [])
},
{
deep: true,
immediate: true
}
)
const options = ref<any[]>([])
const handleChange = (ids: string[], nodes: any[]) => {
options.value = nodes
mySelectIds.value = ids
emit('change', ids)
}
const handleRemove = () => {
emit('change', mySelectIds.value)
}
const handleUserListChange = (list: any[]) => {
options.value = list
}
</script>
<style scoped></style>

View File

@ -0,0 +1,32 @@
import { TableOption } from '@/types'
const option: TableOption = {
api: {
base: '/sys/user',
page: '/page/res'
},
addBtn: false,
editBtn: false,
delBtn: false,
menuProps: {
display: false
},
rowSelection: {},
columns: [
{
name: '姓名',
prop: 'fullName'
},
{
name: '手机号',
prop: 'phone'
},
{
name: '部门',
prop: 'deptNames',
addDisplay: false,
editDisplay: false
}
]
}
export default option

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,291 @@
<template>
<div style="position: relative">
<div class="verify-img-out">
<div
class="verify-img-panel"
:style="{
width: setSize.imgWidth,
height: setSize.imgHeight,
'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
'margin-bottom': vSpace + 'px'
}"
>
<div class="verify-refresh" style="z-index: 3" @click="refresh" v-show="showRefresh">
<i class="iconfont icon-refresh"></i>
</div>
<img
:src="'data:image/png;base64,' + pointBackImgBase"
ref="canvas"
alt=""
style="width: 100%; height: 100%; display: block"
@click="bindingClick ? canvasClick($event) : undefined"
/>
<div
v-for="(tempPoint, index) in tempPoints"
:key="index"
class="point-area"
:style="{
'background-color': '#1abd6c',
color: '#fff',
'z-index': 9999,
width: '20px',
height: '20px',
'text-align': 'center',
'line-height': '20px',
'border-radius': '50%',
position: 'absolute',
top: parseInt(tempPoint.y - 10) + 'px',
left: parseInt(tempPoint.x - 10) + 'px'
}"
>
{{ index + 1 }}
</div>
</div>
</div>
<!-- 'height': this.barSize.height, -->
<div
class="verify-bar-area"
:style="{
width: setSize.imgWidth,
color: this.barAreaColor,
'border-color': this.barAreaBorderColor,
'line-height': this.barSize.height
}"
>
<span class="verify-msg">{{ text }}</span>
</div>
</div>
</template>
<script type="text/babel">
/* eslint-disable */
/**
* VerifyPoints
* @description 点选
* */
import {
computed,
onMounted,
reactive,
ref,
watch,
nextTick,
toRefs,
watchEffect,
getCurrentInstance
} from 'vue'
import { resetSize, _code_chars, _code_color1, _code_color2 } from '../utils/util'
import { aesEncrypt } from '../utils/ase'
import { reqGet, reqCheck } from '../api/index'
export default {
name: 'VerifyPoints',
props: {
// popfixed
mode: {
type: String,
default: 'fixed'
},
captchaType: {
type: String
},
//
vSpace: {
type: Number,
default: 5
},
imgSize: {
type: Object,
default() {
return {
width: '310px',
height: '155px'
}
}
},
barSize: {
type: Object,
default() {
return {
width: '310px',
height: '40px'
}
}
}
},
setup(props, context) {
const { mode, captchaType, vSpace, imgSize, barSize } = toRefs(props)
const { proxy } = getCurrentInstance()
const secretKey = ref('') // ase
const checkNum = ref(3) //
const fontPos = reactive([]) //
const checkPosArr = reactive([]) //
const num = ref(1) //
const pointBackImgBase = ref('') //
const poinTextList = reactive([]) //
const backToken = ref('') // token
const setSize = reactive({
imgHeight: 0,
imgWidth: 0,
barHeight: 0,
barWidth: 0
})
const tempPoints = reactive([])
const text = ref('')
const barAreaColor = ref(undefined)
const barAreaBorderColor = ref(undefined)
const showRefresh = ref(true)
const bindingClick = ref(true)
const init = () => {
//
fontPos.splice(0, fontPos.length)
checkPosArr.splice(0, checkPosArr.length)
num.value = 1
getPictrue()
nextTick(() => {
const { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy)
setSize.imgHeight = imgHeight
setSize.imgWidth = imgWidth
setSize.barHeight = barHeight
setSize.barWidth = barWidth
proxy.$parent.$emit('ready', proxy)
})
}
onMounted(() => {
//
init()
proxy.$el.onselectstart = function () {
return false
}
})
const canvas = ref(null)
const canvasClick = (e) => {
checkPosArr.push(getMousePos(canvas, e))
if (num.value == checkNum.value) {
num.value = createPoint(getMousePos(canvas, e))
//
const arr = pointTransfrom(checkPosArr, setSize)
checkPosArr.length = 0
checkPosArr.push(...arr)
//
setTimeout(() => {
// var flag = this.comparePos(this.fontPos, this.checkPosArr);
//
const captchaVerification = secretKey.value
? aesEncrypt(`${backToken.value}---${JSON.stringify(checkPosArr)}`, secretKey.value)
: `${backToken.value}---${JSON.stringify(checkPosArr)}`
const data = {
captchaType: captchaType.value,
pointJson: secretKey.value
? aesEncrypt(JSON.stringify(checkPosArr), secretKey.value)
: JSON.stringify(checkPosArr),
token: backToken.value
}
reqCheck(data).then((res) => {
if (res.repCode == '0000') {
barAreaColor.value = '#4cae4c'
barAreaBorderColor.value = '#5cb85c'
text.value = '验证成功'
bindingClick.value = false
if (mode.value == 'pop') {
setTimeout(() => {
proxy.$parent.clickShow = false
refresh()
}, 800)
}
proxy.$parent.$emit('success', { captchaVerification })
} else {
proxy.$parent.$emit('error', proxy)
barAreaColor.value = '#d9534f'
barAreaBorderColor.value = '#d9534f'
text.value = '验证失败'
setTimeout(() => {
refresh()
}, 400)
}
})
}, 400)
}
if (num.value < checkNum.value) {
num.value = createPoint(getMousePos(canvas, e))
}
}
//
const getMousePos = function (obj, e) {
const x = e.offsetX
const y = e.offsetY
return { x, y }
}
//
const createPoint = function (pos) {
tempPoints.push({ ...pos })
return num.value + 1
}
const refresh = function () {
tempPoints.splice(0, tempPoints.length)
barAreaColor.value = '#000'
barAreaBorderColor.value = '#ddd'
bindingClick.value = true
fontPos.splice(0, fontPos.length)
checkPosArr.splice(0, checkPosArr.length)
num.value = 1
getPictrue()
text.value = '验证失败'
showRefresh.value = true
}
//
function getPictrue() {
const data = {
captchaType: captchaType.value
}
reqGet(data).then((res) => {
if (res.repCode == '0000') {
pointBackImgBase.value = res.repData.originalImageBase64
backToken.value = res.repData.token
secretKey.value = res.repData.secretKey
poinTextList.value = res.repData.wordList
text.value = `请依次点击【${poinTextList.value.join(',')}`
} else {
text.value = res.repMsg
}
})
}
//
const pointTransfrom = function (pointArr, imgSize) {
const newPointArr = pointArr.map((p) => {
const x = Math.round((310 * p.x) / parseInt(imgSize.imgWidth))
const y = Math.round((155 * p.y) / parseInt(imgSize.imgHeight))
return { x, y }
})
return newPointArr
}
return {
secretKey,
checkNum,
fontPos,
checkPosArr,
num,
pointBackImgBase,
poinTextList,
backToken,
setSize,
tempPoints,
text,
barAreaColor,
barAreaBorderColor,
showRefresh,
bindingClick,
init,
canvas,
canvasClick,
getMousePos,
createPoint,
refresh,
getPictrue,
pointTransfrom
}
}
}
</script>

View File

@ -0,0 +1,423 @@
<template>
<div style="position: relative">
<div
v-if="type === '2'"
class="verify-img-out"
:style="{ height: parseInt(setSize.imgHeight) + vSpace + 'px' }"
>
<div class="verify-img-panel" :style="{ width: setSize.imgWidth, height: setSize.imgHeight }">
<img
:src="'data:image/png;base64,' + backImgBase"
alt=""
style="width: 100%; height: 100%; display: block; -webkit-user-drag: none"
/>
<div class="verify-refresh" @click="refresh" v-show="showRefresh">
<i class="iconfont icon-refresh"></i>
</div>
<transition name="tips">
<span class="verify-tips" v-if="tipWords" :class="passFlag ? 'suc-bg' : 'err-bg'">{{
tipWords
}}</span>
</transition>
</div>
</div>
<!-- 公共部分 -->
<div
class="verify-bar-area"
:style="{ width: setSize.imgWidth, height: barSize.height, 'line-height': barSize.height }"
>
<span class="verify-msg" v-text="text"></span>
<div
class="verify-left-bar"
:style="{
width: leftBarWidth !== undefined ? leftBarWidth : barSize.height,
height: barSize.height,
'border-color': leftBarBorderColor,
transaction: transitionWidth
}"
>
<span class="verify-msg" v-text="finishText"></span>
<div
class="verify-move-block"
@touchstart="start"
@mousedown="start"
:style="{
width: barSize.height,
height: barSize.height,
'background-color': moveBlockBackgroundColor,
left: moveBlockLeft,
transition: transitionLeft
}"
>
<i :class="['verify-icon iconfont', iconClass]" :style="{ color: iconColor }"></i>
<div
v-if="type === '2'"
class="verify-sub-block"
:style="{
width: Math.floor((parseInt(setSize.imgWidth) * 47) / 310) + 'px',
height: setSize.imgHeight,
top: '-' + (parseInt(setSize.imgHeight) + vSpace) + 'px',
'background-size': setSize.imgWidth + ' ' + setSize.imgHeight
}"
>
<img
:src="'data:image/png;base64,' + blockBackImgBase"
alt=""
style="width: 100%; height: 100%; display: block; -webkit-user-drag: none"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script type="text/babel">
/* eslint-disable */
/**
* VerifySlide
* @description 滑块
* */
import { aesEncrypt } from './../utils/ase'
import { resetSize } from './../utils/util'
import { reqGet, reqCheck } from './../api/index'
import {
computed,
onMounted,
reactive,
ref,
watch,
nextTick,
toRefs,
watchEffect,
getCurrentInstance
} from 'vue'
// "captchaType":"blockPuzzle",
export default {
name: 'VerifySlide',
props: {
captchaType: {
type: String
},
type: {
type: String,
default: '1'
},
//popfixed
mode: {
type: String,
default: 'fixed'
},
vSpace: {
type: Number,
default: 5
},
explain: {
type: String,
default: '向右滑动完成验证'
},
imgSize: {
type: Object,
default() {
return {
width: '310px',
height: '155px'
}
}
},
blockSize: {
type: Object,
default() {
return {
width: '50px',
height: '50px'
}
}
},
barSize: {
type: Object,
default() {
return {
width: '310px',
height: '40px'
}
}
}
},
setup(props, context) {
const { mode, captchaType, vSpace, imgSize, barSize, type, blockSize, explain } = toRefs(props)
const { proxy } = getCurrentInstance()
let secretKey = ref(''), //ase
passFlag = ref(''), //
backImgBase = ref(''), //
blockBackImgBase = ref(''), //
backToken = ref(''), //token
startMoveTime = ref(''), //
endMovetime = ref(''), //
tipsBackColor = ref(''), //
tipWords = ref(''),
text = ref(''),
finishText = ref(''),
setSize = reactive({
imgHeight: 0,
imgWidth: 0,
barHeight: 0,
barWidth: 0
}),
top = ref(0),
left = ref(0),
moveBlockLeft = ref(undefined),
leftBarWidth = ref(undefined),
//
moveBlockBackgroundColor = ref(undefined),
leftBarBorderColor = ref('#ddd'),
iconColor = ref(undefined),
iconClass = ref('icon-right'),
status = ref(false), //
isEnd = ref(false), //
showRefresh = ref(true),
transitionLeft = ref(''),
transitionWidth = ref(''),
startLeft = ref(0)
const barArea = computed(() => {
return proxy.$el.querySelector('.verify-bar-area')
})
function init() {
text.value = explain.value
getPictrue()
nextTick(() => {
let { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy)
setSize.imgHeight = imgHeight
setSize.imgWidth = imgWidth
setSize.barHeight = barHeight
setSize.barWidth = barWidth
proxy.$parent.$emit('ready', proxy)
})
window.removeEventListener('touchmove', function (e) {
move(e)
})
window.removeEventListener('mousemove', function (e) {
move(e)
})
//
window.removeEventListener('touchend', function () {
end()
})
window.removeEventListener('mouseup', function () {
end()
})
window.addEventListener('touchmove', function (e) {
move(e)
})
window.addEventListener('mousemove', function (e) {
move(e)
})
//
window.addEventListener('touchend', function () {
end()
})
window.addEventListener('mouseup', function () {
end()
})
}
watch(type, () => {
init()
})
onMounted(() => {
//
init()
proxy.$el.onselectstart = function () {
return false
}
})
//
function start(e) {
e = e || window.event
if (!e.touches) {
//PC
var x = e.clientX
} else {
//
var x = e.touches[0].pageX
}
startLeft.value = Math.floor(x - barArea.value.getBoundingClientRect().left)
startMoveTime.value = +new Date() //
if (isEnd.value == false) {
text.value = ''
moveBlockBackgroundColor.value = '#337ab7'
leftBarBorderColor.value = '#337AB7'
iconColor.value = '#fff'
e.stopPropagation()
status.value = true
}
}
//
function move(e) {
e = e || window.event
if (status.value && isEnd.value == false) {
if (!e.touches) {
//PC
var x = e.clientX
} else {
//
var x = e.touches[0].pageX
}
var bar_area_left = barArea.value.getBoundingClientRect().left
var move_block_left = x - bar_area_left //left
if (
move_block_left >=
barArea.value.offsetWidth - parseInt(parseInt(blockSize.value.width) / 2) - 2
) {
move_block_left =
barArea.value.offsetWidth - parseInt(parseInt(blockSize.value.width) / 2) - 2
}
if (move_block_left <= 0) {
move_block_left = parseInt(parseInt(blockSize.value.width) / 2)
}
//left
moveBlockLeft.value = move_block_left - startLeft.value + 'px'
leftBarWidth.value = move_block_left - startLeft.value + 'px'
}
}
//
function end() {
endMovetime.value = +new Date()
//
if (status.value && isEnd.value == false) {
var moveLeftDistance = parseInt((moveBlockLeft.value || '').replace('px', ''))
moveLeftDistance = (moveLeftDistance * 310) / parseInt(setSize.imgWidth)
let data = {
captchaType: captchaType.value,
pointJson: secretKey.value
? aesEncrypt(JSON.stringify({ x: moveLeftDistance, y: 5.0 }), secretKey.value)
: JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
token: backToken.value
}
reqCheck(data).then((res) => {
if (res.repCode == '0000') {
moveBlockBackgroundColor.value = '#5cb85c'
leftBarBorderColor.value = '#5cb85c'
iconColor.value = '#fff'
iconClass.value = 'icon-check'
showRefresh.value = false
isEnd.value = true
if (mode.value == 'pop') {
setTimeout(() => {
proxy.$parent.clickShow = false
refresh()
}, 1500)
}
passFlag.value = true
tipWords.value = `${((endMovetime.value - startMoveTime.value) / 1000).toFixed(
2
)}s验证成功`
var captchaVerification = secretKey.value
? aesEncrypt(
backToken.value + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
secretKey.value
)
: backToken.value + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 })
setTimeout(() => {
tipWords.value = ''
proxy.$parent.closeBox()
proxy.$parent.$emit('success', { captchaVerification })
}, 1000)
} else {
moveBlockBackgroundColor.value = '#d9534f'
leftBarBorderColor.value = '#d9534f'
iconColor.value = '#fff'
iconClass.value = 'icon-close'
passFlag.value = false
setTimeout(function () {
refresh()
}, 1000)
proxy.$parent.$emit('error', proxy)
tipWords.value = '验证失败'
setTimeout(() => {
tipWords.value = ''
}, 1000)
}
})
status.value = false
}
}
const refresh = () => {
showRefresh.value = true
finishText.value = ''
transitionLeft.value = 'left .3s'
moveBlockLeft.value = 0
leftBarWidth.value = undefined
transitionWidth.value = 'width .3s'
leftBarBorderColor.value = '#ddd'
moveBlockBackgroundColor.value = '#fff'
iconColor.value = '#000'
iconClass.value = 'icon-right'
isEnd.value = false
getPictrue()
setTimeout(() => {
transitionWidth.value = ''
transitionLeft.value = ''
text.value = explain.value
}, 300)
}
//
function getPictrue() {
let data = {
captchaType: captchaType.value
}
reqGet(data).then((res) => {
if (res.repCode == '0000') {
backImgBase.value = res.repData.originalImageBase64
blockBackImgBase.value = res.repData.jigsawImageBase64
backToken.value = res.repData.token
secretKey.value = res.repData.secretKey
} else {
tipWords.value = res.repMsg
}
})
}
return {
secretKey, //ase
passFlag, //
backImgBase, //
blockBackImgBase, //
backToken, //token
startMoveTime, //
endMovetime, //
tipsBackColor, //
tipWords,
text,
finishText,
setSize,
top,
left,
moveBlockLeft,
leftBarWidth,
//
moveBlockBackgroundColor,
leftBarBorderColor,
iconColor,
iconClass,
status, //
isEnd, //
showRefresh,
transitionLeft,
transitionWidth,
barArea,
refresh,
start
}
}
}
</script>

View File

@ -0,0 +1,23 @@
/**
* 此处可直接引用自己项目封装好的 axios 配合后端联调
*/
import request from '@/utils/axios' // 组件内部封装的axios
// 获取验证图片 以及token
export function reqGet(data) {
return request({
url: '/captcha/get',
method: 'post',
data
})
}
// 滑动或者点选验证
export function reqCheck(data) {
return request({
url: '/captcha/check',
method: 'post',
data
})
}

View File

@ -0,0 +1,15 @@
import CryptoJS from 'crypto-js'
/**
* @word 要加密的内容
* @keyWord String 服务器随机返回的关键字
* */
// eslint-disable-next-line import/prefer-default-export
export function aesEncrypt(word, keyWord = 'XwKsGlMcdPMEhR1B') {
const key = CryptoJS.enc.Utf8.parse(keyWord)
const srcs = CryptoJS.enc.Utf8.parse(word)
const encrypted = CryptoJS.AES.encrypt(srcs, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
})
return encrypted.toString()
}

View File

@ -0,0 +1,101 @@
/* eslint-disable */
export function resetSize(vm) {
let img_width
let img_height
let bar_width
let bar_height // 图片的宽度、高度,移动条的宽度、高度
const parentWidth = vm.$el.parentNode.offsetWidth || window.offsetWidth
const parentHeight = vm.$el.parentNode.offsetHeight || window.offsetHeight
if (vm.imgSize.width.indexOf('%') != -1) {
img_width = `${(parseInt(vm.imgSize.width) / 100) * parentWidth}px`
} else {
img_width = vm.imgSize.width
}
if (vm.imgSize.height.indexOf('%') != -1) {
img_height = `${(parseInt(vm.imgSize.height) / 100) * parentHeight}px`
} else {
img_height = vm.imgSize.height
}
if (vm.barSize.width.indexOf('%') != -1) {
bar_width = `${(parseInt(vm.barSize.width) / 100) * parentWidth}px`
} else {
bar_width = vm.barSize.width
}
if (vm.barSize.height.indexOf('%') != -1) {
bar_height = `${(parseInt(vm.barSize.height) / 100) * parentHeight}px`
} else {
bar_height = vm.barSize.height
}
return { imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height }
}
export const _code_chars = [
1,
2,
3,
4,
5,
6,
7,
8,
9,
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z'
]
export const _code_color1 = ['#fffff0', '#f0ffff', '#f0fff0', '#fff0f0']
export const _code_color2 = ['#FF0033', '#006699', '#993366', '#FF9900', '#66CC66', '#FF33CC']

Some files were not shown because too many files have changed in this diff Show More