├── .editorconfig ├── .env ├── .env.development ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── gh-pages.yml ├── .gitignore ├── .husky └── pre-commit ├── .stylelintignore ├── .stylelintrc.js ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public └── favicon.ico ├── src ├── App.vue ├── api │ ├── decorate │ │ └── decorate.ts │ ├── mall │ │ ├── goods.ts │ │ ├── group.ts │ │ ├── spec.ts │ │ └── value.ts │ └── system │ │ ├── menu.ts │ │ └── user.ts ├── assets │ └── logo.png ├── components │ ├── application │ │ ├── Application.tsx │ │ ├── Context.tsx │ │ └── index.ts │ ├── editor │ │ ├── Editor.vue │ │ └── index.ts │ ├── free-nutui │ │ ├── goods-card │ │ │ ├── assets │ │ │ │ └── thumb.png │ │ │ ├── index.ts │ │ │ └── src │ │ │ │ ├── GoodsCard.tsx │ │ │ │ └── style.scss │ │ ├── image-ad │ │ │ ├── assets │ │ │ │ ├── ad.png │ │ │ │ ├── carousel-block.svg │ │ │ │ ├── carousel-dot.svg │ │ │ │ ├── carousel-rectangle.svg │ │ │ │ ├── carousel-small.svg │ │ │ │ ├── carousel.svg │ │ │ │ └── thumb.png │ │ │ ├── index.ts │ │ │ └── src │ │ │ │ ├── ImageAd.tsx │ │ │ │ ├── action.tsx │ │ │ │ ├── components │ │ │ │ └── AdItem.tsx │ │ │ │ └── style.scss │ │ ├── image-nav │ │ │ ├── assets │ │ │ │ └── thumb.png │ │ │ ├── index.ts │ │ │ └── src │ │ │ │ ├── ImageNav.tsx │ │ │ │ ├── action.tsx │ │ │ │ ├── component │ │ │ │ └── NavItem.tsx │ │ │ │ └── style.scss │ │ ├── navigation │ │ │ ├── assets │ │ │ │ └── thumb.png │ │ │ ├── index.ts │ │ │ └── src │ │ │ │ ├── Navigation.tsx │ │ │ │ └── style.scss │ │ ├── notice-bar │ │ │ ├── assets │ │ │ │ └── thumb.png │ │ │ ├── index.ts │ │ │ └── src │ │ │ │ ├── NoticeBar.tsx │ │ │ │ └── style.scss │ │ ├── search-bar │ │ │ ├── assets │ │ │ │ └── thumb.png │ │ │ ├── index.ts │ │ │ └── src │ │ │ │ ├── SearchBar.tsx │ │ │ │ └── style.scss │ │ └── video-player │ │ │ ├── assets │ │ │ └── thumb.png │ │ │ ├── index.ts │ │ │ └── src │ │ │ ├── VideoPlayer.tsx │ │ │ └── style.scss │ ├── goods-type │ │ ├── GoodsType.vue │ │ └── index.ts │ ├── naive-ui │ │ ├── dynamic-tags │ │ │ ├── DynamicTags.tsx │ │ │ ├── index.ts │ │ │ └── interface.d.ts │ │ ├── form │ │ │ ├── FormItem.tsx │ │ │ ├── index.ts │ │ │ ├── light.ts │ │ │ └── styles │ │ │ │ └── form-item.cssr.ts │ │ └── tabs │ │ │ ├── Tabs.tsx │ │ │ ├── index.tsx │ │ │ └── interface.ts │ ├── router-button │ │ ├── RouterButton.tsx │ │ └── index.ts │ ├── sku │ │ ├── Sku.tsx │ │ ├── Table.tsx │ │ ├── components │ │ │ ├── Button.tsx │ │ │ ├── Group.tsx │ │ │ ├── Image.tsx │ │ │ └── Value.tsx │ │ ├── index.ts │ │ ├── interface.ts │ │ ├── styles │ │ │ ├── light.ts │ │ │ └── sku.cssr.ts │ │ └── utils.ts │ ├── space-view │ │ ├── SpaceView.vue │ │ └── index.ts │ ├── spin-view │ │ ├── SpinView.tsx │ │ ├── index.ts │ │ └── style.scss │ ├── title-divider │ │ ├── TitleDivider.vue │ │ └── index.ts │ └── upload │ │ ├── UploadMain.tsx │ │ ├── a.tsx │ │ ├── hooks │ │ └── main.ts │ │ ├── image │ │ ├── FileList.tsx │ │ ├── ImageItem.tsx │ │ ├── UploadImage.tsx │ │ ├── UploadImageMain.tsx │ │ └── UploadLIst.tsx │ │ ├── index.ts │ │ ├── styles │ │ ├── main.scss │ │ └── upload.scss │ │ └── video │ │ ├── UploadVideo.tsx │ │ └── UploadVideoMain.tsx ├── enums │ ├── page.ts │ └── theme.ts ├── env.d.ts ├── global.d.ts ├── hooks │ └── form.ts ├── layout │ ├── Base.vue │ ├── Light.tsx │ ├── Normal.vue │ ├── Secondary.tsx │ ├── components │ │ ├── aside │ │ │ ├── Aside.vue │ │ │ └── index.ts │ │ ├── header │ │ │ ├── Header.vue │ │ │ └── index.ts │ │ └── sider │ │ │ ├── Sider.vue │ │ │ └── index.ts │ └── style.module.scss ├── main.ts ├── router │ ├── constant.ts │ ├── generator.ts │ ├── guards.ts │ ├── icons.ts │ ├── index.ts │ ├── interface.ts │ └── modules │ │ ├── dashboard.ts │ │ └── system.ts ├── store │ ├── index.ts │ ├── modules │ │ ├── layout.ts │ │ ├── router.ts │ │ ├── theme.ts │ │ └── user.ts │ └── types.ts ├── styles │ ├── common.scss │ ├── core.scss │ ├── nutui │ │ └── variables.scss │ └── tailwind.css ├── theme.ts ├── utils │ ├── http │ │ ├── http.ts │ │ ├── index.ts │ │ ├── interface.ts │ │ └── status.ts │ ├── index.ts │ ├── naive-ui.ts │ └── storage.ts └── views │ ├── article │ ├── category │ │ └── index.vue │ └── index.vue │ ├── assets │ ├── dashboard.tsx │ └── reconciliation.tsx │ ├── dashboard │ ├── components │ │ ├── Line.tsx │ │ ├── Pie.tsx │ │ └── dataSeries.ts │ ├── console.tsx │ └── workspace.vue │ ├── data │ ├── analysis.tsx │ └── dashboard.tsx │ ├── diy │ ├── decorate.tsx │ ├── style.scss │ └── widgets.ts │ ├── mall │ ├── goods │ │ ├── action.tsx │ │ ├── base.vue │ │ ├── components │ │ │ ├── card │ │ │ │ ├── card.vue │ │ │ │ └── index.ts │ │ │ ├── entity │ │ │ │ ├── entity.tsx │ │ │ │ ├── form │ │ │ │ │ ├── base.tsx │ │ │ │ │ ├── data.tsx │ │ │ │ │ ├── delivery.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── other.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── interface.ts │ │ │ │ └── style.scss │ │ │ ├── interface.d.ts │ │ │ ├── types.tsx │ │ │ └── virtual │ │ │ │ ├── index.ts │ │ │ │ └── virtual.vue │ │ ├── hooks │ │ │ ├── goods.ts │ │ │ └── sku.ts │ │ ├── index.vue │ │ ├── interface.ts │ │ └── style.scss │ ├── group │ │ ├── action.vue │ │ └── index.vue │ └── page │ │ ├── Tabs.tsx │ │ ├── category.tsx │ │ ├── draft.tsx │ │ └── index.tsx │ ├── member │ ├── action │ │ └── index.tsx │ ├── dashboard.tsx │ └── index.tsx │ ├── order │ ├── index.vue │ └── refund.vue │ ├── resource │ └── index.tsx │ ├── settings │ ├── base │ │ ├── components │ │ │ ├── card.tsx │ │ │ ├── goods.tsx │ │ │ ├── order.tsx │ │ │ ├── payment.tsx │ │ │ ├── section.tsx │ │ │ └── store.tsx │ │ ├── index.tsx │ │ └── style.module.scss │ ├── contact.tsx │ ├── info.tsx │ └── refund.tsx │ └── system │ ├── auth │ ├── index.tsx │ ├── role │ │ └── action.tsx │ └── sigin.vue │ └── settings │ └── index.vue ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = off 14 | trim_trailing_whitespace = false 15 | 16 | [*.{yml,yaml}] 17 | indent_size = 2 18 | 19 | [docker-compose.yml] 20 | indent_size = 4 21 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_EMESH_TITLE=Emesh 2 | VITE_EMESH_VERSION=0.0.1-dev 3 | VITE_HOST=https://api.v1.emesh.cc 4 | VITE_API_PREFIX=api 5 | VITE_API_VERSION=v1 6 | 7 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | VITE_EMESH_TITLE=Emesh 2 | VITE_EMESH_VERSION=0.0.1-dev 3 | VITE_HOST=http://localhost 4 | VITE_API_PREFIX=api 5 | VITE_API_VERSION=v1 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.sh 2 | node_modules 3 | *.md 4 | *.woff 5 | *.ttf 6 | .vscode 7 | .idea 8 | dist 9 | lib 10 | /public 11 | .husky 12 | .local 13 | Dockerfile 14 | components.d.ts 15 | components.d.ts -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | node: true, 7 | 'vue/setup-compiler-macros': true 8 | }, 9 | plugins: [ 10 | 'vue', 11 | '@typescript-eslint' 12 | ], 13 | extends: [ 14 | // 'plugin:@typescript-eslint/recommended', 15 | // 'plugin:vue/vue3-recommended', 16 | // 'standard', 17 | 'eslint:recommended', 18 | 'plugin:vue/vue3-recommended', 19 | 'plugin:@typescript-eslint/recommended' 20 | ], 21 | parser: 'vue-eslint-parser', 22 | parserOptions: { 23 | ecmaVersion: '2021', 24 | parser: '@typescript-eslint/parser', 25 | sourceType: 'module', 26 | jsxPragma: 'React', 27 | ecmaFeatures: { 28 | jsx: true 29 | } 30 | }, 31 | rules: { 32 | semi: ['error', 'always'], 33 | quotes: ['error', 'single'], 34 | 'no-useless-catch': 'off', 35 | 'no-async-promise-executor': 'off', 36 | 'vue/multi-word-component-names': 'off', 37 | 38 | '@typescript-eslint/no-explicit-any': 'off', 39 | '@typescript-eslint/ban-ts-ignore': 'off', 40 | '@typescript-eslint/explicit-function-return-type': 'off', 41 | '@typescript-eslint/no-var-requires': 'off', 42 | '@typescript-eslint/no-empty-function': 'off', 43 | '@typescript-eslint/no-use-before-define': 'off', 44 | '@typescript-eslint/ban-ts-comment': 'off', 45 | '@typescript-eslint/ban-types': 'off', 46 | '@typescript-eslint/no-non-null-assertion': 'off', 47 | '@typescript-eslint/explicit-module-boundary-types': 'off', 48 | '@typescript-eslint/no-unused-vars': [ 49 | 'error', 50 | { 51 | argsIgnorePattern: '^_', 52 | varsIgnorePattern: '^_' 53 | } 54 | ] 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ dev ] 9 | pull_request: 10 | branches: [ dev ] 11 | 12 | jobs: 13 | build: 14 | name: Build and deploy gh-pages 15 | env: 16 | MY_SECRET: ${{secrets.gh_pages}} 17 | USER_NAME: eamesh 18 | USER_EMAIL: easeava@gmail.com 19 | BUILD_PATH: ./dist 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | matrix: 24 | node-version: [16.x] 25 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 26 | 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: pnpm/action-setup@646cdf48217256a3d0b80361c5a50727664284f2 30 | with: 31 | version: 6.10.0 32 | - uses: actions/setup-node@v3 33 | with: 34 | node-version: ${{ matrix.node-version }} 35 | cache: 'pnpm' 36 | 37 | - run: pnpm install 38 | - run: pnpm build:gh-pages 39 | 40 | - name: Set Script 41 | run: | 42 | script='i\ 43 | 52 | ' 53 | sed -i "/<\/body>/ $script" ./dist/index.html 54 | 55 | - name: Commit gh-pages 56 | run: | 57 | cd $BUILD_PATH 58 | git init 59 | git config --local user.name $USER_NAME 60 | git config --local user.email $USER_EMAIL 61 | git status 62 | git remote add origin https://$MY_SECRET@github.com/$GITHUB_REPOSITORY.git 63 | git checkout -b gh-pages 64 | git add --all 65 | git commit -m "deploy to Github pages" 66 | git push origin gh-pages -f 67 | echo 🤘 deploy gh-pages complete. 68 | 69 | - name: Deploy to Server 70 | uses: easingthemes/ssh-deploy@main 71 | env: 72 | SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }} 73 | ARGS: "-rltgoDzvO --delete" 74 | SOURCE: "dist/" 75 | REMOTE_HOST: ${{ secrets.REMOTE_HOST }} 76 | REMOTE_USER: ${{ secrets.REMOTE_USER }} 77 | TARGET: ${{ secrets.REMOTE_TARGET }} 78 | EXCLUDE: "/node_modules/" 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | # .vscode/* 17 | # !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | /dist/* 2 | /public/* 3 | public/* 4 | /lib/* -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | 'stylelint-config-standard', 5 | // 'stylelint-config-html/vue', 6 | 'stylelint-config-standard-scss', 7 | 'stylelint-config-recommended-vue/scss', 8 | ], 9 | plugins: ['stylelint-order'], 10 | rules: { 11 | 'indentation': 2, 12 | 'selector-pseudo-element-no-unknown': [ 13 | true, 14 | { 15 | ignorePseudoElements: [ 16 | 'v-deep', 17 | 'deep', 18 | 'input-placeholder' 19 | ] 20 | } 21 | ], 22 | 'selector-class-pattern': null, 23 | 'at-rule-no-unknown': null, 24 | 'scss/at-rule-no-unknown': null, 25 | 'number-leading-zero': 'never', 26 | 'no-descending-specificity': null, 27 | 'font-family-no-missing-generic-family-keyword': null, 28 | 'selector-type-no-unknown': null, 29 | 'no-duplicate-selectors': null, 30 | 'no-empty-source':null, 31 | 'selector-pseudo-class-no-unknown': [ 32 | true, 33 | { 34 | ignorePseudoClasses: [ 35 | 'global', 36 | 'deep' 37 | ], 38 | } 39 | ], 40 | "number-max-precision": null, 41 | 'scss/dollar-variable-pattern': null, 42 | 'max-line-length': 160 43 | }, 44 | ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts'], 45 | }; 46 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "johnsoncodehk.volar", 4 | "dbaeumer.vscode-eslint", 5 | "stylelint.vscode-stylelint", 6 | "EditorConfig.EditorConfig" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": true, 4 | }, 5 | "eslint.validate": [ 6 | "javascript", 7 | "javascriptreact", 8 | "typescript", 9 | "typescriptreact" 10 | ], 11 | "eslint.alwaysShowStatus": true, 12 | "stylelint.validate": [ 13 | "css", 14 | "postcss", 15 | "scss", 16 | "vue", 17 | "sass" 18 | ], 19 | "typescript.tsdk": "node_modules/typescript/lib" 20 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 太年轻 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Emesh 多端商城应用 2 | 3 | Vite + TypeScript + Naive UI + Free Core + Nutui 4 | 一个跨端小程序商城应用 5 | 6 | [free-core](https://github.com/eamesh/free-core) 7 | [free-nutui](https://github.com/eamesh/free-nutui) 8 | [emesh-taro](https://github.com/eamesh/emesh-taro) 9 | 10 | api部分是目前是私有项目, 基础功能开发完会开源 11 | 12 | ## Preview 13 | 14 | [预览](https://preview.v1.emesh.cc) 15 | [跨端小程序](https://github.com/eamesh/emesh-taro) 16 | 17 | ## Feature 18 | 19 | - 微页面 20 | - [x] 微页面列表 21 | - [x] 微页面添加 22 | - [x] 微页面修改 23 | - 店铺管理 24 | - [x] 微页面(ui 完成) [free-nutui](https://github.com/eamesh/free-nutui) 25 | - 商品管理 26 | - [x] 商品列表(筛选ui完成, 未对接接口) 27 | - [x] 添加商品 28 | - [x] 更新商品 29 | - [x] 商品分组 30 | - [ ] 商品分类 31 | - 内容管理 32 | - [x] 内容列表(ui完成) 33 | - [x] 内容分类(ui完成) 34 | - [ ] 素材资源 35 | - 会员管理 36 | - [ ] 会员列表 37 | - [ ] 会员等级 38 | - 活动管理 39 | - [ ] 折扣 40 | - [ ] 优惠券 41 | - [ ] 秒杀 42 | - [ ] 团购 43 | - 订单管理 44 | - [x] 订单列表(ui完成) 45 | - [x] 售后列表(ui完成) 46 | - 店铺设置 47 | - [ ] 基础设置 48 | - [ ] 商品设置 49 | - [ ] 订单设置 50 | - [ ] 门店管理 51 | - 多端小程序 52 | - [ ] 微信小程序 53 | - [ ] ... 54 | - 系统设置 55 | - [ ] 权限控制 56 | - [ ] 用户管理 57 | 58 | ## Component 59 | 60 | - [x] 商品Sku[Sku](https://github.com/eamesh/emesh/tree/dev/src/components/sku) 61 | - [x] 上传[Upload](https://github.com/eamesh/emesh/tree/dev/src/components/upload) 62 | - [x] [UploadImageMain](https://github.com/eamesh/emesh/blob/dev/src/components/upload/image/UploadImageMain.tsx) 63 | - [ ] [UploadVideoMain](https://github.com/eamesh/emesh/blob/dev/src/components/upload/video/UploadVideoMain.tsx) 64 | - [x] [UploadImage](https://github.com/eamesh/emesh/blob/dev/src/components/upload/image/UploadImage.tsx) 65 | - [ ] [UploadVideo](https://github.com/eamesh/emesh/blob/dev/src/components/upload/video/UploadVideo.tsx) 66 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Emesh 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "free-nutui", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vue-tsc --noEmit && vite build", 8 | "build:gh-pages": "vue-tsc --noEmit && vite build --base=./", 9 | "preview": "vite preview", 10 | "lint:eslint": "eslint \"src/**/*.{vue,js,ts,tsx}\" --fix", 11 | "lint:stylelint": "stylelint --fix \"**/*.{vue,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/", 12 | "prepare": "husky install" 13 | }, 14 | "dependencies": { 15 | "@nutui/nutui": "^3.1.18", 16 | "@vitejs/plugin-vue-jsx": "^1.3.9", 17 | "@vueup/vue-quill": "^1.0.0-beta.8", 18 | "apexcharts": "^3.35.0", 19 | "async-validator": "^4.0.7", 20 | "axios": "^0.26.1", 21 | "lodash-es": "^4.17.21", 22 | "pinia": "^2.0.13", 23 | "seemly": "^0.3.3", 24 | "store2": "^2.13.2", 25 | "vooks": "^0.2.12", 26 | "vue": "^3.2.25", 27 | "vue-router": "4", 28 | "vue3-apexcharts": "^1.4.1", 29 | "vueuc": "^0.4.28" 30 | }, 31 | "devDependencies": { 32 | "@types/node": "^17.0.23", 33 | "@types/quill": "^2.0.9", 34 | "@typescript-eslint/eslint-plugin": "^5.17.0", 35 | "@typescript-eslint/parser": "^5.17.0", 36 | "@vicons/antd": "^0.12.0", 37 | "@vicons/fluent": "^0.12.0", 38 | "@vicons/ionicons5": "^0.12.0", 39 | "@vitejs/plugin-vue": "^2.3.0", 40 | "autoprefixer": "^10.4.4", 41 | "consola": "^2.15.3", 42 | "eslint": "^8.12.0", 43 | "eslint-config-standard": "^17.0.0-1", 44 | "eslint-plugin-import": "^2.25.4", 45 | "eslint-plugin-n": "14", 46 | "eslint-plugin-promise": "^6.0.0", 47 | "eslint-plugin-vue": "^8.5.0", 48 | "free-core": "^1.1.8-22.dev", 49 | "husky": ">=7", 50 | "lint-staged": ">=10", 51 | "naive-ui": "^2.27.0", 52 | "postcss": "^8.4.12", 53 | "postcss-html": "^1.3.0", 54 | "sass": "^1.49.11", 55 | "stylelint": "^14.6.1", 56 | "stylelint-config-recommended-vue": "^1.4.0", 57 | "stylelint-config-standard": "^25.0.0", 58 | "stylelint-config-standard-scss": "^3.0.0", 59 | "stylelint-order": "^5.0.0", 60 | "tailwindcss": "^3.0.23", 61 | "typescript": "^4.5.4", 62 | "unplugin-vue-components": "^0.18.5", 63 | "vfonts": "^0.0.3", 64 | "vite": "^2.9.0", 65 | "vite-plugin-style-import": "^2.0.0", 66 | "vue-tsc": "^0.29.8" 67 | }, 68 | "lint-staged": { 69 | "*.{ts,tsx,js,vue,md}": "pnpm lint:eslint", 70 | "*.{vue,scss,css,sass}": "pnpm lint:stylelint" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eamesh/emesh/2b5163eddf1960f996a349fc9dbc416595e0205f/public/favicon.ico -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 30 | 31 | 34 | -------------------------------------------------------------------------------- /src/api/decorate/decorate.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils/http'; 2 | 3 | export const create = (data: any): Promise => { 4 | return http.request({ 5 | url: '/decorate', 6 | method: 'POST', 7 | data 8 | }); 9 | }; 10 | 11 | export const update = (id: number | string, data: any): Promise => { 12 | return http.request({ 13 | url: `/decorate/${id}`, 14 | method: 'PUT', 15 | data 16 | }); 17 | }; 18 | 19 | export const detail = (id: number | string) => { 20 | return http.request({ 21 | url: `/decorate/${id}`, 22 | method: 'GET', 23 | }); 24 | }; 25 | 26 | export const setHome = (id: number) => { 27 | return http.request({ 28 | url: '/decorate/home', 29 | data: { 30 | id 31 | }, 32 | method: 'PUT', 33 | }); 34 | }; 35 | 36 | export const getLists = (): Promise => { 37 | return http.request({ 38 | url: '/decorate', 39 | method: 'GET' 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /src/api/mall/goods.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils/http'; 2 | 3 | export const create = (data: any): Promise => { 4 | return http.request({ 5 | url: '/mall/goods', 6 | method: 'POST', 7 | data 8 | }); 9 | }; 10 | 11 | export const update = (id: number, data: any): Promise => { 12 | return http.request({ 13 | url: `/mall/goods/${id}`, 14 | method: 'PUT', 15 | data 16 | }); 17 | }; 18 | 19 | export const getLists = (params: any = {}): Promise => { 20 | return http.request({ 21 | url: '/mall/goods', 22 | method: 'GET', 23 | params 24 | }); 25 | }; 26 | 27 | export const goodsDetail = (id: number | string) => { 28 | return http.request({ 29 | url: `/mall/goods/${id}`, 30 | method: 'GET' 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/api/mall/group.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils/http'; 2 | 3 | export interface GoodsGroupSchema { 4 | id: number | string; 5 | name: string; 6 | alias: string; 7 | goods_count: number; 8 | created_at: string; 9 | updated_at: string; 10 | } 11 | 12 | export const lists = (params: any = {}): Promise => { 13 | return http.request({ 14 | url: 'mall/group', 15 | method: 'GET', 16 | params 17 | }); 18 | }; 19 | 20 | export const create = (data: any): Promise => { 21 | return http.request({ 22 | url: '/mall/group', 23 | method: 'POST', 24 | data 25 | }); 26 | }; 27 | 28 | export const update = (id: number | string, data: any): Promise => { 29 | return http.request({ 30 | url: `/mall/group/${id}`, 31 | method: 'PUT', 32 | data 33 | }); 34 | }; 35 | 36 | export const getDetail = (id: number | string): Promise => { 37 | return http.request({ 38 | url: `/mall/group/${id}`, 39 | method: 'GET' 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /src/api/mall/spec.ts: -------------------------------------------------------------------------------- 1 | import { SkuBaseSchema } from '@/components/sku/interface'; 2 | import { http } from '@/utils/http'; 3 | 4 | export const create = (name: string): Promise => { 5 | return http.request({ 6 | url: '/mall/spec', 7 | method: 'POST', 8 | data: { 9 | name 10 | } 11 | }); 12 | }; 13 | 14 | export const getLists = (): Promise => { 15 | return http.request({ 16 | url: '/mall/spec', 17 | method: 'GET' 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/api/mall/value.ts: -------------------------------------------------------------------------------- 1 | import { SkuValueSchemas } from '@/components/sku/interface'; 2 | import { http } from '@/utils/http'; 3 | 4 | export const create = (name: string | string[]): Promise => { 5 | return http.request({ 6 | url: '/mall/value', 7 | method: 'POST', 8 | data: { 9 | name 10 | } 11 | }); 12 | }; 13 | 14 | export const getLists = (): Promise => { 15 | return http.request({ 16 | url: '/mall/value', 17 | method: 'GET' 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/api/system/menu.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils/http'; 2 | 3 | export const userMenus = () => { 4 | return http.request({ 5 | url: '/system/menu/user', 6 | method: 'GET' 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /src/api/system/user.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils/http'; 2 | 3 | interface AuthToken { 4 | token: string; 5 | } 6 | 7 | export const sigin = (data: any): Promise => { 8 | return http.request({ 9 | url: '/system/auth', 10 | method: 'POST', 11 | data 12 | }); 13 | }; 14 | 15 | export const profile = () => { 16 | return http.request({ 17 | url: '/system/user/profile', 18 | method: 'GET' 19 | }); 20 | }; 21 | 22 | export const signout = () => { 23 | return http.request({ 24 | url: '/system/auth', 25 | method: 'DELETE' 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eamesh/emesh/2b5163eddf1960f996a349fc9dbc416595e0205f/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/application/Application.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue'; 2 | import { NDialogProvider, NMessageProvider, NLoadingBarProvider, NNotificationProvider } from 'naive-ui'; 3 | import Context from './Context'; 4 | 5 | export default defineComponent({ 6 | name: 'Application', 7 | 8 | render () { 9 | const { 10 | $slots 11 | } = this; 12 | 13 | return ( 14 | <> 15 | 16 | 17 | 18 | 19 | 20 | { $slots.default ? $slots.default() : null } 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /src/components/application/Context.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue'; 2 | import { useDialog, useMessage, useLoadingBar, useNotification } from 'naive-ui'; 3 | 4 | export default defineComponent({ 5 | name: 'ApplicationContext', 6 | 7 | setup () { 8 | window.$dialog = useDialog(); 9 | window.$message = useMessage(); 10 | window.$loading = useLoadingBar(); 11 | window.$notify = useNotification(); 12 | }, 13 | 14 | render () { 15 | return null; 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/application/index.ts: -------------------------------------------------------------------------------- 1 | import Application from './Application'; 2 | 3 | export { Application }; 4 | -------------------------------------------------------------------------------- /src/components/editor/Editor.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 65 | 66 | 84 | -------------------------------------------------------------------------------- /src/components/editor/index.ts: -------------------------------------------------------------------------------- 1 | import Editor from './Editor.vue'; 2 | 3 | export { 4 | Editor 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/free-nutui/goods-card/assets/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eamesh/emesh/2b5163eddf1960f996a349fc9dbc416595e0205f/src/components/free-nutui/goods-card/assets/thumb.png -------------------------------------------------------------------------------- /src/components/free-nutui/goods-card/index.ts: -------------------------------------------------------------------------------- 1 | import { Widget } from 'free-core/lib/types/core/src/interface'; 2 | import GoodsCard, { NutuiGoodsCardProps } from './src/GoodsCard'; 3 | import Thumb from './assets/thumb.png'; 4 | import { markRaw } from 'vue'; 5 | 6 | const NutuiGoodsCardWidget: Widget = { 7 | name: '商品卡片', 8 | key: 'goods-card', 9 | thumb: Thumb, 10 | component: markRaw(GoodsCard), 11 | allowCount: 10 12 | }; 13 | 14 | export default NutuiGoodsCardWidget; 15 | -------------------------------------------------------------------------------- /src/components/free-nutui/goods-card/src/GoodsCard.tsx: -------------------------------------------------------------------------------- 1 | import { FreeActionTitle, widgetDataProps } from 'free-core'; 2 | import { NText } from 'naive-ui'; 3 | import { defineComponent, ref } from 'vue'; 4 | 5 | import './style.scss'; 6 | 7 | export interface NutuiGoodsCardProps { 8 | title: string; 9 | } 10 | 11 | const nutuiGoodsCardProps = widgetDataProps({ 12 | title: '' 13 | }); 14 | 15 | export default defineComponent({ 16 | name: 'GoodsCard', 17 | 18 | props: nutuiGoodsCardProps, 19 | 20 | setup () { 21 | const model = ref({ 22 | imgUrl: 23 | '//img10.360buyimg.com/n2/s240x240_jfs/t1/210890/22/4728/163829/6163a590Eb7c6f4b5/6390526d49791cb9.jpg!q70.jpg', 24 | title: '活蟹】湖塘煙雨 阳澄湖大闸蟹公4.5两 母3.5两 4对8只 鲜活生鲜螃蟹现货水产礼盒海鲜水', 25 | price: '388', 26 | vipPrice: '378', 27 | shopDesc: '自营', 28 | delivery: '厂商配送', 29 | shopName: '阳澄湖大闸蟹自营店>' 30 | }); 31 | 32 | function renderAction () { 33 | return ( 34 | <> 35 | 36 |
37 | 开发中 38 |
39 | 40 | ); 41 | } 42 | 43 | return { 44 | model, 45 | renderAction 46 | }; 47 | }, 48 | 49 | render () { 50 | return ( 51 |
52 | 61 | 62 |
63 | ); 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /src/components/free-nutui/goods-card/src/style.scss: -------------------------------------------------------------------------------- 1 | .goods-card { 2 | padding: 4px 16px; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/free-nutui/image-ad/assets/ad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eamesh/emesh/2b5163eddf1960f996a349fc9dbc416595e0205f/src/components/free-nutui/image-ad/assets/ad.png -------------------------------------------------------------------------------- /src/components/free-nutui/image-ad/assets/carousel-block.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/free-nutui/image-ad/assets/carousel-dot.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/free-nutui/image-ad/assets/carousel-rectangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/free-nutui/image-ad/assets/carousel-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/free-nutui/image-ad/assets/carousel.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/free-nutui/image-ad/assets/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eamesh/emesh/2b5163eddf1960f996a349fc9dbc416595e0205f/src/components/free-nutui/image-ad/assets/thumb.png -------------------------------------------------------------------------------- /src/components/free-nutui/image-ad/index.ts: -------------------------------------------------------------------------------- 1 | import { Widget } from 'free-core/lib/types/core/src/interface'; 2 | import ImageAd, { NutuiImageAdProps } from './src/ImageAd'; 3 | import Thumb from './assets/thumb.png'; 4 | import { markRaw } from 'vue'; 5 | 6 | const NutuiImageAdWidget: Widget = { 7 | name: '图片广告', 8 | key: 'image-ad', 9 | thumb: Thumb, 10 | component: markRaw(ImageAd), 11 | allowCount: 300, 12 | data: { 13 | type: 'default', 14 | imageType: 'regular', 15 | radioType: 'square', 16 | pagePadding: 0, 17 | imagePadding: 0, 18 | ads: [] 19 | } 20 | }; 21 | 22 | export default NutuiImageAdWidget; 23 | -------------------------------------------------------------------------------- /src/components/free-nutui/image-ad/src/ImageAd.tsx: -------------------------------------------------------------------------------- 1 | import { widgetDataProps } from 'free-core'; 2 | import { defineComponent } from 'vue'; 3 | import Ad from '../assets/ad.png'; 4 | import { useAction } from './action'; 5 | 6 | import './style.scss'; 7 | 8 | export interface AdItemData { 9 | imgUrl: string; 10 | redirect: object; 11 | } 12 | 13 | export interface NutuiImageAdProps { 14 | type: 'default' | 'small' | 'dot' | 'block' | 'rectangle'; 15 | radioType: 'square' | 'round'; 16 | imageType: 'shadow' | 'regular'; 17 | pagePadding: number; 18 | imagePadding: number; 19 | ads: AdItemData[]; 20 | } 21 | 22 | const nutuiImageAdProps = widgetDataProps({ 23 | type: 'default', 24 | imageType: 'regular', 25 | radioType: 'square', 26 | pagePadding: 0, 27 | imagePadding: 0, 28 | ads: [] 29 | }); 30 | 31 | export default defineComponent({ 32 | name: 'ImageAd', 33 | 34 | props: nutuiImageAdProps, 35 | 36 | setup (props) { 37 | const { 38 | model, 39 | renderAction 40 | } = useAction(props); 41 | 42 | return { 43 | model, 44 | renderAction 45 | }; 46 | }, 47 | 48 | render () { 49 | const { 50 | model 51 | } = this; 52 | 53 | return ( 54 |
55 | 64 | { 65 | model.ads.length ? model.ads.map(ad => { 66 | return ( 67 | 71 | 74 | 75 | ); 76 | }) : [...Array(3)].map(() => { 77 | return ( 78 | 82 | 85 | 86 | ); 87 | }) 88 | } 89 | 90 |
91 | ); 92 | } 93 | }); 94 | -------------------------------------------------------------------------------- /src/components/free-nutui/image-ad/src/components/AdItem.tsx: -------------------------------------------------------------------------------- 1 | import { computed, defineComponent, PropType, ref, unref, watch } from 'vue'; 2 | import { UploadImageMain } from '@/components/upload'; 3 | import { NButton, NCard, NDropdown, NIcon, NSpace } from 'naive-ui'; 4 | import { ChevronDown20Regular, ChevronUp20Regular } from '@vicons/fluent'; 5 | import { FileInfo } from 'naive-ui/lib/upload/src/interface'; 6 | import { AdItemData } from '../ImageAd'; 7 | 8 | const adItemProps = { 9 | index: Number, 10 | data: { 11 | type: Object as PropType, 12 | default: () => ({ 13 | imgUrl: '', 14 | redirect: {} 15 | }) 16 | } 17 | }; 18 | 19 | export default defineComponent({ 20 | name: 'AdItem', 21 | 22 | props: adItemProps, 23 | 24 | emits: ['onUpdate:data'], 25 | 26 | setup (props, { emit }) { 27 | const model = ref(props.data); 28 | const modelUnref = unref(model); 29 | const hoverState = ref(false); 30 | 31 | const fileListCompute = computed({ 32 | get () { 33 | return modelUnref.imgUrl ? [ 34 | { 35 | id: props.index?.toString() as string, 36 | name: '', 37 | status: 'finished', 38 | url: modelUnref.imgUrl, 39 | thumbnailUrl: modelUnref.imgUrl 40 | } 41 | ] : []; 42 | }, 43 | 44 | set (files) { 45 | console.log(files); 46 | modelUnref.imgUrl = files.length === 0 ? '' : files[0].thumbnailUrl! || files[0].url!; 47 | } 48 | }); 49 | 50 | const dropdownOptions = ref([ 51 | { 52 | label: '微页面', 53 | key: '1', 54 | children: [ 55 | { 56 | label: '微页面及分类', 57 | key: '2' 58 | }, 59 | { 60 | label: '店铺主页', 61 | key: '3' 62 | }, 63 | { 64 | label: '个人中心', 65 | key: ' 4' 66 | } 67 | ] 68 | }, 69 | { 70 | label: '商品', 71 | key: '5', 72 | children: [ 73 | { 74 | label: '全部商品', 75 | key: '6' 76 | }, 77 | { 78 | label: '商品及分组', 79 | key: '7' 80 | }, 81 | { 82 | label: '购物车', 83 | key: ' 8' 84 | } 85 | ] 86 | } 87 | ]); 88 | 89 | watch( 90 | () => model.value, 91 | () => { 92 | emit('onUpdate:data', model.value); 93 | } 94 | ); 95 | 96 | return { 97 | model, 98 | hoverState, 99 | dropdownOptions, 100 | fileListCompute 101 | }; 102 | }, 103 | 104 | render () { 105 | const { 106 | hoverState, 107 | dropdownOptions, 108 | } = this; 109 | 110 | return ( 111 | 112 | 113 | 114 | 115 | { 117 | console.log('over'); 118 | this.hoverState = true; 119 | }, 120 | onMouseoutCapture: () => { 121 | this.hoverState = false; 122 | } 123 | }}> 124 | 选择跳转到页面 125 | 126 | { hoverState ? : } 127 | 128 | 129 | 130 | 131 | 132 | ); 133 | } 134 | }); 135 | -------------------------------------------------------------------------------- /src/components/free-nutui/image-ad/src/style.scss: -------------------------------------------------------------------------------- 1 | .image-ad { 2 | width: 100%; 3 | 4 | .nut-swiper-item { 5 | line-height: 200px; 6 | } 7 | 8 | .nut-swiper-item img { 9 | width: 100%; 10 | height: 100%; 11 | } 12 | } 13 | 14 | .image-ad-render { 15 | .type-button { 16 | .n-icon { 17 | margin-top: 1px; 18 | color: #979797; 19 | } 20 | } 21 | 22 | .n-radio-button--checked { 23 | .n-icon { 24 | color: inherit; 25 | } 26 | } 27 | 28 | .carousel-image-type { 29 | .n-icon { 30 | margin-top: 5px; 31 | } 32 | } 33 | } 34 | 35 | .image-ad-card { 36 | .n-upload-file-list.n-upload-file-list--grid { 37 | grid-template-columns: repeat(auto-fill, 46px); 38 | } 39 | 40 | .n-upload-trigger.n-upload-trigger--image-card, 41 | .n-upload-file-list .n-upload-file.n-upload-file--image-card-type { 42 | width: 46px; 43 | height: 46px; 44 | } 45 | } 46 | 47 | .carousel-container { 48 | .carousel-add { 49 | width: 100%; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/free-nutui/image-nav/assets/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eamesh/emesh/2b5163eddf1960f996a349fc9dbc416595e0205f/src/components/free-nutui/image-nav/assets/thumb.png -------------------------------------------------------------------------------- /src/components/free-nutui/image-nav/index.ts: -------------------------------------------------------------------------------- 1 | import { Widget } from 'free-core/lib/types/core/src/interface'; 2 | import ImageNav, { NutuiImageNavProps } from './src/ImageNav'; 3 | import Thumb from './assets/thumb.png'; 4 | import { markRaw } from 'vue'; 5 | 6 | const NutuiImageNavWidget: Widget = { 7 | name: '图文导航', 8 | key: 'image-nav', 9 | thumb: Thumb, 10 | component: markRaw(ImageNav), 11 | allowCount: 10, 12 | data: { 13 | type: 'image', 14 | imageType: 'fixed', 15 | direction: 'horizontal', 16 | style: { 17 | backgroundColor: '#FFFFFF', 18 | color: '#333333', 19 | borderColor: '#f5f6f7' 20 | }, 21 | max: 12, 22 | columnNum: 3, 23 | reverse: false, 24 | navs: [ 25 | { 26 | title: '导航一', 27 | imgUrl: '', 28 | redirect: {} 29 | }, 30 | { 31 | title: '导航二', 32 | imgUrl: '', 33 | redirect: {} 34 | }, 35 | { 36 | title: '导航三', 37 | imgUrl: '', 38 | redirect: {} 39 | }, 40 | { 41 | title: '导航四', 42 | imgUrl: '', 43 | redirect: {} 44 | } 45 | ] 46 | } 47 | }; 48 | 49 | export default NutuiImageNavWidget; 50 | -------------------------------------------------------------------------------- /src/components/free-nutui/image-nav/src/ImageNav.tsx: -------------------------------------------------------------------------------- 1 | import { widgetDataProps } from 'free-core'; 2 | import { defineComponent } from 'vue'; 3 | import { useAction } from './action'; 4 | 5 | import './style.scss'; 6 | 7 | export type ImageNavType = 'image' | 'text'; 8 | export type ImageNavImageType = 'fixed' | 'slide'; 9 | export type ImageNavImageDirection = 'horizontal' | 'vertical'; 10 | 11 | export interface ImageNavStyle { 12 | backgroundColor: string; 13 | color: string; 14 | borderColor: string; 15 | } 16 | 17 | export interface ImageNavItem { 18 | title: string; 19 | imgUrl?: string; 20 | redirect: any; 21 | } 22 | 23 | export interface NutuiImageNavProps { 24 | type: ImageNavType; 25 | imageType: ImageNavImageType; 26 | direction: ImageNavImageDirection; 27 | style: ImageNavStyle; 28 | navs: ImageNavItem[]; 29 | max: number; 30 | columnNum: number; 31 | reverse: boolean; 32 | } 33 | 34 | const nutuiImageNavProps = widgetDataProps({ 35 | type: 'image', 36 | imageType: 'fixed', 37 | direction: 'horizontal', 38 | style: { 39 | backgroundColor: '#FFFFFF', 40 | color: '#333333', 41 | borderColor: '#f5f6f7' 42 | }, 43 | max: 12, 44 | columnNum: 3, 45 | reverse: false, 46 | navs: [ 47 | { 48 | title: '导航一', 49 | imgUrl: '', 50 | redirect: {} 51 | }, 52 | { 53 | title: '导航二', 54 | imgUrl: '', 55 | redirect: {} 56 | }, 57 | { 58 | title: '导航三', 59 | imgUrl: '', 60 | redirect: {} 61 | }, 62 | { 63 | title: '导航四', 64 | imgUrl: '', 65 | redirect: {} 66 | } 67 | ] 68 | }); 69 | 70 | export default defineComponent({ 71 | name: 'ImageNav', 72 | 73 | props: nutuiImageNavProps, 74 | 75 | setup (props) { 76 | const { 77 | model, 78 | renderAction 79 | } = useAction(props); 80 | 81 | return { 82 | model, 83 | renderAction 84 | }; 85 | }, 86 | 87 | render () { 88 | const { 89 | model 90 | } = this; 91 | return ( 92 |
93 | 99 | { 100 | model.type === 'image' 101 | ? model.navs.map(item => ( 102 | 103 | )) 104 | : model.navs.map(item => ( 105 | 106 |
107 | {item.title} 108 |
109 |
110 | )) 111 | } 112 |
113 |
114 | ); 115 | } 116 | }); 117 | -------------------------------------------------------------------------------- /src/components/free-nutui/image-nav/src/component/NavItem.tsx: -------------------------------------------------------------------------------- 1 | import { NButton, NCard, NDropdown, NIcon, NInput, NSpace, NText } from 'naive-ui'; 2 | import { computed, defineComponent, PropType, ref, unref, watch } from 'vue'; 3 | import { ChevronDown20Regular, ChevronUp20Regular } from '@vicons/fluent'; 4 | import { ImageNavType } from '../ImageNav'; 5 | import { UploadImageMain } from '@/components/upload'; 6 | import { FileInfo } from 'naive-ui/lib/upload/src/interface'; 7 | 8 | export interface NavItemData { 9 | title: string; 10 | imgUrl?: string; 11 | redirect: object; 12 | } 13 | 14 | const navItemProps = { 15 | index: Number, 16 | type: String as PropType, 17 | data: { 18 | type: Object as PropType, 19 | default: () => {} 20 | } 21 | }; 22 | 23 | export default defineComponent({ 24 | name: 'NavItem', 25 | 26 | props: navItemProps, 27 | 28 | emits: ['onUpdate:data'], 29 | 30 | setup (props, { emit }) { 31 | const model = ref(props.data); 32 | const modelUnref = unref(model); 33 | const hoverState = ref(false); 34 | 35 | const dropdownOptions = ref([ 36 | { 37 | label: '微页面', 38 | key: '1', 39 | children: [ 40 | { 41 | label: '微页面及分类', 42 | key: '2' 43 | }, 44 | { 45 | label: '店铺主页', 46 | key: '3' 47 | }, 48 | { 49 | label: '个人中心', 50 | key: ' 4' 51 | } 52 | ] 53 | }, 54 | { 55 | label: '商品', 56 | key: '5', 57 | children: [ 58 | { 59 | label: '全部商品', 60 | key: '6' 61 | }, 62 | { 63 | label: '商品及分组', 64 | key: '7' 65 | }, 66 | { 67 | label: '购物车', 68 | key: ' 8' 69 | } 70 | ] 71 | } 72 | ]); 73 | 74 | const fileListCompute = computed({ 75 | get () { 76 | console.log(modelUnref.imgUrl); 77 | return modelUnref.imgUrl ? [ 78 | { 79 | id: props.index?.toString() as string, 80 | name: '', 81 | status: 'finished', 82 | url: modelUnref.imgUrl, 83 | thumbnailUrl: modelUnref.imgUrl 84 | } 85 | ] : []; 86 | }, 87 | 88 | set (files) { 89 | console.log(files); 90 | modelUnref.imgUrl = files.length === 0 ? '' : files[0].thumbnailUrl! || files[0].url!; 91 | } 92 | }); 93 | 94 | watch( 95 | () => model.value, 96 | () => { 97 | emit('onUpdate:data', model.value); 98 | } 99 | ); 100 | 101 | return { 102 | model, 103 | hoverState, 104 | dropdownOptions, 105 | fileListCompute 106 | }; 107 | }, 108 | 109 | render () { 110 | const { 111 | type, 112 | model, 113 | hoverState, 114 | dropdownOptions 115 | } = this; 116 | 117 | return ( 118 | 119 | 120 | { 121 | type === 'image' ? : null 122 | } 123 | 124 | 125 | 标题 126 | 127 | 128 | 129 | 链接 130 | 131 | { 133 | console.log('over'); 134 | this.hoverState = true; 135 | }, 136 | onMouseoutCapture: () => { 137 | this.hoverState = false; 138 | } 139 | }}> 140 | 选择跳转到页面 141 | 142 | { hoverState ? : } 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | ); 151 | } 152 | }); 153 | -------------------------------------------------------------------------------- /src/components/free-nutui/image-nav/src/style.scss: -------------------------------------------------------------------------------- 1 | .image-nav { 2 | .nut-grid-item__content { 3 | background: transparent; 4 | background-color: transparent; 5 | border-color: var(--border-color); 6 | 7 | .nut-grid-item__text { 8 | color: inherit; 9 | } 10 | } 11 | } 12 | 13 | .image-nav-render { 14 | .nav-image-type { 15 | .n-icon { 16 | margin-top: 5px !important; 17 | } 18 | } 19 | 20 | .n-upload-file-list.n-upload-file-list--grid { 21 | grid-template-columns: repeat(auto-fill, 58px); 22 | } 23 | 24 | .n-upload-trigger.n-upload-trigger--image-card, 25 | .n-upload-file-list .n-upload-file.n-upload-file--image-card-type { 26 | width: 58px; 27 | height: 58px; 28 | } 29 | } 30 | 31 | .nav-container { 32 | .nav-add { 33 | width: 100%; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/free-nutui/navigation/assets/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eamesh/emesh/2b5163eddf1960f996a349fc9dbc416595e0205f/src/components/free-nutui/navigation/assets/thumb.png -------------------------------------------------------------------------------- /src/components/free-nutui/navigation/index.ts: -------------------------------------------------------------------------------- 1 | import { Widget } from 'free-core/lib/types/core/src/interface'; 2 | import Navigation from './src/Navigation'; 3 | import Thumb from './assets/thumb.png'; 4 | import { markRaw } from 'vue'; 5 | 6 | const NutuiNavigationWidget: Widget = { 7 | name: '电梯导航', 8 | key: 'navigation', 9 | thumb: Thumb, 10 | component: markRaw(Navigation), 11 | allowCount: 10 12 | }; 13 | 14 | export default NutuiNavigationWidget; 15 | -------------------------------------------------------------------------------- /src/components/free-nutui/navigation/src/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import { FreeActionTitle, widgetDataProps } from 'free-core'; 2 | import { NText } from 'naive-ui'; 3 | import { defineComponent, ref } from 'vue'; 4 | 5 | import './style.scss'; 6 | 7 | export interface NutuiNavigationProps { 8 | keyword: string; 9 | } 10 | 11 | const nutuiNavigationProps = widgetDataProps({ 12 | keyword: '' 13 | }); 14 | 15 | export default defineComponent({ 16 | name: 'Navigation', 17 | 18 | props: nutuiNavigationProps, 19 | 20 | setup () { 21 | const model = ref({ 22 | tableValue: '0' 23 | }); 24 | 25 | function renderAction () { 26 | return ( 27 | <> 28 | 29 |
30 | 开发中 31 |
32 | 33 | ); 34 | } 35 | 36 | return { 37 | model, 38 | renderAction 39 | }; 40 | }, 41 | 42 | render () { 43 | return ( 44 | 56 | ); 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /src/components/free-nutui/navigation/src/style.scss: -------------------------------------------------------------------------------- 1 | .navigation { 2 | .nut-tabs__content { 3 | display: none; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/components/free-nutui/notice-bar/assets/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eamesh/emesh/2b5163eddf1960f996a349fc9dbc416595e0205f/src/components/free-nutui/notice-bar/assets/thumb.png -------------------------------------------------------------------------------- /src/components/free-nutui/notice-bar/index.ts: -------------------------------------------------------------------------------- 1 | import { Widget } from 'free-core/lib/types/core/src/interface'; 2 | import NoticeBar, { NutuiNoticeBarProps } from './src/NoticeBar'; 3 | import Thumb from './assets/thumb.png'; 4 | import { markRaw } from 'vue'; 5 | 6 | const NutuiNoticeBarWidget: Widget = { 7 | name: '公告栏', 8 | key: 'notice-bar', 9 | thumb: Thumb, 10 | component: markRaw(NoticeBar), 11 | allowCount: 20, 12 | data: { 13 | title: '', 14 | color: '#D9500B', 15 | background: 'rgb(255, 248, 233)' 16 | } 17 | }; 18 | 19 | export default NutuiNoticeBarWidget; 20 | -------------------------------------------------------------------------------- /src/components/free-nutui/notice-bar/src/NoticeBar.tsx: -------------------------------------------------------------------------------- 1 | import { FreeActionTitle, widgetDataProps } from 'free-core'; 2 | import { NButton, NColorPicker, NForm, NFormItem, NInput, NSpace, NText } from 'naive-ui'; 3 | import { defineComponent, ref, unref } from 'vue'; 4 | 5 | import './style.scss'; 6 | 7 | export interface NutuiNoticeBarProps { 8 | title: string; 9 | color: string; 10 | background: string; 11 | } 12 | 13 | const nutuiNoticeBarProps = widgetDataProps({ 14 | title: '', 15 | color: '#D9500B', 16 | background: 'rgb(255, 248, 233)' 17 | }); 18 | 19 | export default defineComponent({ 20 | name: 'NoticeBar', 21 | 22 | props: nutuiNoticeBarProps, 23 | 24 | setup (props) { 25 | const model = ref(props.data); 26 | const modelUnref = unref(model); 27 | 28 | function renderAction () { 29 | return ( 30 | <> 31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | {modelUnref.background} 40 | 41 | modelUnref.background = 'rgb(255, 248, 233)'}>重置 42 | 43 | 44 | 45 | 46 | 47 | 48 | {modelUnref.color} 49 | 50 | modelUnref.color = '#D9500B'}>重置 51 | 52 | 53 | 54 | 55 | 56 |
57 | 58 | 59 | ); 60 | } 61 | 62 | return { 63 | model, 64 | renderAction 65 | }; 66 | }, 67 | 68 | render () { 69 | return ( 70 |
71 | 77 | 78 | 79 |
80 | ); 81 | } 82 | }); 83 | -------------------------------------------------------------------------------- /src/components/free-nutui/notice-bar/src/style.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eamesh/emesh/2b5163eddf1960f996a349fc9dbc416595e0205f/src/components/free-nutui/notice-bar/src/style.scss -------------------------------------------------------------------------------- /src/components/free-nutui/search-bar/assets/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eamesh/emesh/2b5163eddf1960f996a349fc9dbc416595e0205f/src/components/free-nutui/search-bar/assets/thumb.png -------------------------------------------------------------------------------- /src/components/free-nutui/search-bar/index.ts: -------------------------------------------------------------------------------- 1 | import { Widget } from 'free-core/lib/types/core/src/interface'; 2 | import Search, { NutuiSearchProps } from './src/SearchBar'; 3 | import Thumb from './assets/thumb.png'; 4 | import { markRaw } from 'vue'; 5 | 6 | const NutuiSearchWidget: Widget = { 7 | name: '搜索', 8 | key: 'search', 9 | thumb: Thumb, 10 | component: markRaw(Search), 11 | allowCount: 2, 12 | data: { 13 | text: '', 14 | scan: false, 15 | background: '#ffffff', 16 | inputBackground: '#f7f7f7', 17 | textColor: '#9f9f9f' 18 | } 19 | }; 20 | 21 | export default NutuiSearchWidget; 22 | -------------------------------------------------------------------------------- /src/components/free-nutui/search-bar/src/style.scss: -------------------------------------------------------------------------------- 1 | .search { 2 | .nut-searchbar__input-bar::placeholder { 3 | color: var(--search-text-color); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/components/free-nutui/video-player/assets/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eamesh/emesh/2b5163eddf1960f996a349fc9dbc416595e0205f/src/components/free-nutui/video-player/assets/thumb.png -------------------------------------------------------------------------------- /src/components/free-nutui/video-player/index.ts: -------------------------------------------------------------------------------- 1 | import { Widget } from 'free-core/lib/types/core/src/interface'; 2 | import VideoPlayer, { NutuiVideoPlayerProps } from './src/VideoPlayer'; 3 | import Thumb from './assets/thumb.png'; 4 | import { markRaw } from 'vue'; 5 | 6 | const NutuiVideoPlayerWidget: Widget = { 7 | name: '视频播放器', 8 | key: 'video-player', 9 | thumb: Thumb, 10 | component: markRaw(VideoPlayer), 11 | allowCount: 50, 12 | data: { 13 | type: 'resource', 14 | coverType: 'default', 15 | radioType: 'square', 16 | pagePadding: 0, 17 | resource: { 18 | src: '', 19 | type: 'video/mp4' 20 | }, 21 | network: { 22 | src: '', 23 | type: 'video/mp4' 24 | }, 25 | options: { 26 | controls: true, 27 | poster: '', 28 | autoplay: false, 29 | muted: true, 30 | loop: true 31 | } 32 | } 33 | }; 34 | 35 | export default NutuiVideoPlayerWidget; 36 | -------------------------------------------------------------------------------- /src/components/free-nutui/video-player/src/style.scss: -------------------------------------------------------------------------------- 1 | .video-layout { 2 | width: 344px; 3 | height: 204px; 4 | margin-top: 6px; 5 | position: relative; 6 | background-color: #000; 7 | margin-bottom: 14px; 8 | cursor: pointer; 9 | 10 | video { 11 | width: 100%; 12 | height: 100%; 13 | object-fit: contain; 14 | position: absolute; 15 | left: 50%; 16 | top: 50%; 17 | transform: translate(-50%, -50%); 18 | z-index: 1; 19 | } 20 | 21 | .play-btn { 22 | position: absolute; 23 | top: 0; 24 | bottom: 0; 25 | left: 0; 26 | right: 0; 27 | margin: auto; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/goods-type/GoodsType.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 38 | 39 | 55 | -------------------------------------------------------------------------------- /src/components/goods-type/index.ts: -------------------------------------------------------------------------------- 1 | import GoodsType from './GoodsType.vue'; 2 | 3 | export { GoodsType }; 4 | -------------------------------------------------------------------------------- /src/components/naive-ui/dynamic-tags/index.ts: -------------------------------------------------------------------------------- 1 | import DynamicTags from './DynamicTags'; 2 | 3 | export { 4 | DynamicTags 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/naive-ui/dynamic-tags/interface.d.ts: -------------------------------------------------------------------------------- 1 | export type OnUpdateValue = 2 | | ((value: string[]) => void) 3 | | ((value: DynamicTagsOption[]) => void) 4 | 5 | export type OnUpdateValueImpl = ( 6 | value: Array 7 | ) => void 8 | 9 | export type OnCreate = (label: string) => 10 | | { 11 | label: string 12 | value: string 13 | } 14 | | string 15 | 16 | export interface DynamicTagsOption { 17 | label: string 18 | value: string 19 | } 20 | -------------------------------------------------------------------------------- /src/components/naive-ui/form/index.ts: -------------------------------------------------------------------------------- 1 | import FormItem from './FormItem'; 2 | 3 | export { FormItem }; 4 | -------------------------------------------------------------------------------- /src/components/naive-ui/form/light.ts: -------------------------------------------------------------------------------- 1 | import { commonLight } from 'naive-ui/lib/_styles/common'; 2 | import type { ThemeCommonVars } from 'naive-ui/lib/_styles/common'; 3 | import type { Theme } from 'naive-ui/lib/_mixins'; 4 | import commonVariables from 'naive-ui/lib/form/styles/_common'; 5 | 6 | export const self = (vars: ThemeCommonVars) => { 7 | const { 8 | heightSmall, 9 | heightMedium, 10 | heightLarge, 11 | textColor1, 12 | errorColor, 13 | warningColor, 14 | lineHeight, 15 | textColor3 16 | } = vars; 17 | return { 18 | ...commonVariables, 19 | blankHeightSmall: heightSmall, 20 | blankHeightMedium: heightMedium, 21 | blankHeightLarge: heightLarge, 22 | lineHeight, 23 | labelTextColor: textColor1, 24 | asteriskColor: errorColor, 25 | feedbackTextColorError: errorColor, 26 | feedbackTextColorWarning: warningColor, 27 | feedbackTextColor: textColor3 28 | }; 29 | }; 30 | 31 | export type FormThemeVars = ReturnType 32 | 33 | const formLight: Theme<'Form', FormThemeVars> = { 34 | name: 'Form', 35 | common: commonLight, 36 | self 37 | }; 38 | 39 | export default formLight; 40 | export type FormTheme = typeof formLight 41 | -------------------------------------------------------------------------------- /src/components/naive-ui/form/styles/form-item.cssr.ts: -------------------------------------------------------------------------------- 1 | import { cB, cE, cM, c } from 'naive-ui/lib/_utils/cssr'; 2 | import fadeDownTransition from 'naive-ui/lib/_styles/transitions/fade-down.cssr'; 3 | 4 | // vars: 5 | // --n-line-height 6 | // --n-blank-height 7 | // --n-feedback-padding 8 | // --n-feedback-font-size 9 | // --n-label-font-size-left 10 | // --n-label-font-size-top 11 | // --n-label-height 12 | // --n-label-padding 13 | // --n-asterisk-color 14 | // --n-label-text-color 15 | // --n-bezier 16 | // --n-feedback-text-color 17 | // --n-feedback-text-color-warning 18 | // --n-feedback-text-color-error 19 | // --n-label-text-align 20 | // --n-label-padding 21 | // --n-border 22 | export default cB('form-item', { 23 | display: 'grid', 24 | lineHeight: 'var(--n-line-height)' 25 | }, [ 26 | cB('form-item-label', ` 27 | grid-area: label; 28 | align-items: center; 29 | line-height: 1.25; 30 | text-align: var(--n-label-text-align); 31 | font-size: var(--n-label-font-size); 32 | height: var(--n-label-height); 33 | padding: var(--n-label-padding); 34 | color: var(--n-label-text-color); 35 | transition: color .3s var(--n-bezier); 36 | box-sizing: border-box; 37 | `, [ 38 | cE('asterisk', ` 39 | color: var(--n-asterisk-color); 40 | transition: color .3s var(--n-bezier); 41 | `), 42 | cE('asterisk-placeholder', ` 43 | visibility: hidden; 44 | `) 45 | ]), 46 | cB('form-item-blank', { 47 | gridArea: 'blank', 48 | minHeight: 'var(--n-blank-height)' 49 | }), 50 | cM('left-labelled', ` 51 | grid-template-areas: 52 | "label blank" 53 | "label feedback"; 54 | grid-template-columns: auto minmax(0, 1fr); 55 | `, [ 56 | cB('form-item-label', ` 57 | height: var(--n-blank-height); 58 | line-height: var(--n-blank-height); 59 | box-sizing: border-box; 60 | white-space: nowrap; 61 | flex-shrink: 0; 62 | flex-grow: 0; 63 | `) 64 | ]), 65 | cM('top-labelled', ` 66 | grid-template-areas: 67 | "label" 68 | "blank" 69 | "feedback"; 70 | grid-template-rows: var(--n-label-height) 1fr; 71 | grid-template-columns: minmax(0, 100%); 72 | `, [ 73 | cM('no-label', ` 74 | grid-template-areas: 75 | "blank" 76 | "feedback"; 77 | grid-template-rows: 1fr; 78 | `), 79 | cB('form-item-label', { 80 | display: 'flex', 81 | alignItems: 'flex-end', 82 | justifyContent: 'var(--n-label-text-align)' 83 | }) 84 | ]), 85 | cB('form-item-blank', ` 86 | box-sizing: border-box; 87 | display: flex; 88 | align-items: center; 89 | position: relative; 90 | `), 91 | cB('form-item-feedback-wrapper', ` 92 | grid-area: feedback; 93 | box-sizing: border-box; 94 | min-height: var(--n-feedback-height); 95 | font-size: var(--n-feedback-font-size); 96 | line-height: 1.25; 97 | transform-origin: top left; 98 | `, [ 99 | c('&:not(:empty)', ` 100 | padding: var(--n-feedback-padding); 101 | `), 102 | cB('form-item-feedback', { 103 | transition: 'color .3s var(--n-bezier)', 104 | color: 'var(--n-feedback-text-color)' 105 | }, [ 106 | cM('warning', { 107 | color: 'var(--n-feedback-text-color-warning)' 108 | }), 109 | cM('error', { 110 | color: 'var(--n-feedback-text-color-error)' 111 | }), 112 | fadeDownTransition({ 113 | fromOffset: '-3px', 114 | enterDuration: '.3s', 115 | leaveDuration: '.2s' 116 | }) 117 | ]) 118 | ]), 119 | cB('form-item-help-wrapper', ` 120 | grid-area: 3/2; 121 | box-sizing: border-box; 122 | min-height: var(--n-feedback-height); 123 | font-size: var(--n-feedback-font-size); 124 | line-height: 1.25; 125 | transform-origin: top left; 126 | transition: color .3s var(--n-bezier); 127 | `, [ 128 | c('&:not(:empty)', ` 129 | padding: var(--n-feedback-padding); 130 | `), 131 | cB('form-item-help', ` 132 | font-size: 10px; 133 | line-height: 1.8; 134 | margin-bottom: calc(var(--n-feedback-height) / 2); 135 | `), 136 | ]) 137 | ]); 138 | -------------------------------------------------------------------------------- /src/components/naive-ui/tabs/index.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eamesh/emesh/2b5163eddf1960f996a349fc9dbc416595e0205f/src/components/naive-ui/tabs/index.tsx -------------------------------------------------------------------------------- /src/components/naive-ui/tabs/interface.ts: -------------------------------------------------------------------------------- 1 | import { Ref, CSSProperties } from 'vue'; 2 | import { createInjectionKey } from 'naive-ui/lib/_utils'; 3 | 4 | export type TabsType = 'line' | 'card' | 'bar' | 'segment' 5 | 6 | export type OnUpdateValue = (value: string & number) => void 7 | export type OnUpdateValueImpl = (value: string | number) => void 8 | 9 | export type OnClose = (name: string & number) => void 10 | export type OnCloseImpl = (name: string | number) => void 11 | 12 | export type OnBeforeLeave = ( 13 | name: string & number, 14 | oldName: string & number & null 15 | ) => boolean | Promise 16 | export type OnBeforeLeaveImpl = ( 17 | name: string | number, 18 | oldName: string | number | null 19 | ) => boolean | Promise 20 | 21 | export interface TabsInjection { 22 | mergedClsPrefixRef: Ref 23 | valueRef: Ref 24 | typeRef: Ref 25 | closableRef: Ref 26 | tabStyleRef: Ref 27 | paneClassRef: Ref 28 | paneStyleRef: Ref 29 | tabChangeIdRef: { id: number } 30 | onBeforeLeaveRef: Ref 31 | triggerRef: Ref<'click' | 'hover'> 32 | activateTab: (panelName: string | number) => void 33 | handleClose: (panelName: string | number) => void 34 | handleAdd: () => void 35 | } 36 | 37 | export type Addable = 38 | | boolean 39 | | { 40 | disabled?: boolean 41 | } 42 | 43 | export const tabsInjectionKey = createInjectionKey('n-tabs'); 44 | 45 | export interface TabsInst { 46 | syncBarPosition: () => void 47 | } 48 | -------------------------------------------------------------------------------- /src/components/router-button/RouterButton.tsx: -------------------------------------------------------------------------------- 1 | import { NA, NButton } from 'naive-ui'; 2 | import { Size, Type } from 'naive-ui/lib/button/src/interface'; 3 | import { defineComponent, PropType } from 'vue'; 4 | import { RouteLocationRaw, RouterLink } from 'vue-router'; 5 | 6 | const routerButtonProps = { 7 | to: { 8 | type: [String, Object] as PropType, 9 | required: true, 10 | }, 11 | type: { 12 | type: String as PropType, 13 | default: 'default', 14 | }, 15 | target: { 16 | type: String as PropType<'_blank' | '_self' | '_top' | '_parent'>, 17 | default: '_self' 18 | }, 19 | size: String as PropType 20 | }; 21 | 22 | export default defineComponent({ 23 | name: 'RouterButton', 24 | 25 | props: routerButtonProps, 26 | 27 | setup () { 28 | 29 | }, 30 | 31 | render () { 32 | const { 33 | target, 34 | to, 35 | type, 36 | size, 37 | $slots 38 | } = this; 39 | 40 | return ( 41 | 44 | {{ 45 | default: ({ href, navigate }: any) => { 46 | return ( 47 | {} : navigate 52 | }} 53 | > 54 | 55 | {$slots} 56 | 57 | 58 | ); 59 | } 60 | }} 61 | 62 | ); 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /src/components/router-button/index.ts: -------------------------------------------------------------------------------- 1 | import RouterButton from './RouterButton'; 2 | 3 | export { 4 | RouterButton 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/sku/Table.tsx: -------------------------------------------------------------------------------- 1 | import { SkuSchemas } from '@/components/sku/interface'; 2 | import { flatten, FlattenStock } from '@/components/sku/utils'; 3 | import { NDataTable } from 'naive-ui'; 4 | import { TableColumn, TableColumns } from 'naive-ui/lib/data-table/src/interface'; 5 | import { computed, defineComponent, PropType, ref, watch } from 'vue'; 6 | 7 | const tableProps = { 8 | sku: { 9 | type: Array as PropType, 10 | default: () => [] 11 | }, 12 | 13 | extraColumns: { 14 | type: Array as PropType>, 15 | default: () => [] 16 | }, 17 | 18 | flatten: { 19 | type: Array as PropType, 20 | default: () => [] 21 | } 22 | }; 23 | 24 | export default defineComponent({ 25 | name: 'Demo', 26 | 27 | props: tableProps, 28 | 29 | emits: ['changeData'], 30 | 31 | setup (props, { emit }) { 32 | const listsRef = ref([]); 33 | const rowspanRef = ref<(number[])[]>([]); 34 | const columns = ref([]); 35 | 36 | const computedFilter = computed(() => { 37 | return props.sku.filter(item => item.name && item.leaf.length); 38 | }); 39 | 40 | const handleColumns = function () { 41 | const { extraColumns } = props; 42 | columns.value = computedFilter.value.map((item, index): TableColumn => { 43 | return { 44 | title: item.name, 45 | key: index, 46 | render (row) { 47 | const { data } = row as FlattenStock; 48 | return data[index] ? data[index].v : null; 49 | }, 50 | // colSpan: (_, rowIndex) => { 51 | // return 1; 52 | // }, 53 | rowSpan: (_, rowIndex) => { 54 | return rowspanRef.value[index][rowIndex]; 55 | } 56 | }; 57 | }).concat(extraColumns as Array); 58 | }; 59 | 60 | watch( 61 | () => computedFilter.value, 62 | () => { 63 | handleColumns(); 64 | listsRef.value = flatten(computedFilter.value, props.flatten); 65 | handleRowspan(); 66 | }, 67 | { 68 | immediate: true 69 | } 70 | ); 71 | 72 | watch( 73 | () => listsRef.value, 74 | () => { 75 | console.log('update'); 76 | emit('changeData', listsRef.value); 77 | }, 78 | { 79 | deep: true, 80 | immediate: true, 81 | } 82 | ); 83 | 84 | function handleRowspan () { 85 | rowspanRef.value = []; 86 | 87 | computedFilter.value.map((_, index) => { 88 | const span: number[] = []; 89 | let dot = 0; 90 | 91 | listsRef.value.map((item: FlattenStock, indx: number) => { 92 | if (indx === 0) { 93 | span.push(1); 94 | } else { 95 | if (item.data[index].v === listsRef.value[indx - 1].data[index].v) { 96 | span[dot] += 1; 97 | span.push(0); 98 | } else { 99 | dot = indx; 100 | span.push(1); 101 | } 102 | } 103 | }); 104 | rowspanRef.value.push(span); 105 | }); 106 | } 107 | 108 | return { 109 | lists: listsRef, 110 | columns, 111 | computedFilter 112 | }; 113 | }, 114 | 115 | render () { 116 | const { 117 | lists, 118 | columns, 119 | } = this; 120 | 121 | return ( 122 |
123 | 128 |
129 | ); 130 | } 131 | }); 132 | -------------------------------------------------------------------------------- /src/components/sku/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { NButton, NSpace } from 'naive-ui'; 2 | import { MaybeArray } from 'naive-ui/lib/_utils'; 3 | import { defineComponent, PropType } from 'vue'; 4 | 5 | const skuButtonProps = { 6 | clsPrefix: String, 7 | onClick: [Function, Array] as PropType void>>, 8 | }; 9 | 10 | export default defineComponent({ 11 | name: 'SkuButton', 12 | 13 | props: skuButtonProps, 14 | 15 | render () { 16 | const { 17 | clsPrefix, 18 | onClick, 19 | } = this; 20 | 21 | return ( 22 |
25 | 31 | 34 | 添加规格 35 | 36 | 37 |
38 | ); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /src/components/sku/components/Image.tsx: -------------------------------------------------------------------------------- 1 | import { UploadImageMain } from '@/components/upload'; 2 | import { FileInfo } from 'naive-ui/lib/upload/src/interface'; 3 | import { computed, defineComponent, inject, PropType, toRef } from 'vue'; 4 | import { SkuInjection, skuInjectionKey, SkuSchema } from '../interface'; 5 | 6 | const skuImageProps = { 7 | groupIndex: Number, 8 | index: Number, 9 | group: { 10 | type: Object as PropType, 11 | default: () => {} 12 | } 13 | }; 14 | 15 | export default defineComponent({ 16 | name: 'SkuImage', 17 | 18 | props: skuImageProps, 19 | 20 | setup (props) { 21 | const skuMain = inject(skuInjectionKey); 22 | const { handleChangeSku } = skuMain as SkuInjection; 23 | const groupRef = toRef(props, 'group'); 24 | 25 | const defaultFileList = computed({ 26 | get () { 27 | const imgUrl = groupRef.value.leaf[props.index!].img_url; 28 | 29 | const files = imgUrl ? [ 30 | { 31 | id: props.index?.toString() as string, 32 | name: '', 33 | status: 'finished', 34 | url: imgUrl, 35 | thumbnailUrl: imgUrl 36 | } 37 | ] : []; 38 | 39 | return files as FileInfo[]; 40 | }, 41 | set (files: FileInfo[]) { 42 | console.log('update files'); 43 | handleSetImage(files); 44 | } 45 | }); 46 | 47 | function handleSetImage (files: FileInfo[]) { 48 | console.log('set files', files); 49 | const options = props.group.leaf; 50 | 51 | options[props.index as number].img_url = files.length ? files[0].url || files[0].thumbnailUrl! : ''; 52 | handleChangeSku(props.groupIndex as number, { 53 | ...props.group, 54 | leaf: [ 55 | ...options 56 | ] 57 | }); 58 | } 59 | 60 | return { 61 | groupRef, 62 | defaultFileList, 63 | handleSetImage 64 | }; 65 | }, 66 | 67 | render () { 68 | return ( 69 | 70 | ); 71 | } 72 | }); 73 | -------------------------------------------------------------------------------- /src/components/sku/index.ts: -------------------------------------------------------------------------------- 1 | import Sku from './Sku'; 2 | import SkuTable from './Table'; 3 | 4 | export { 5 | Sku, 6 | SkuTable, 7 | }; 8 | -------------------------------------------------------------------------------- /src/components/sku/interface.ts: -------------------------------------------------------------------------------- 1 | import { createInjectionKey } from 'naive-ui/lib/_utils'; 2 | import { Ref } from 'vue'; 3 | 4 | export interface SkuInjection { 5 | clsPrefixRef: Ref; 6 | handleChangeSku: (index: number, group: SkuSchema) => void; 7 | handleRemoveSku: (index: number) => void; 8 | createGroup: CreateGroup; 9 | createValue: CreateValue; 10 | getGroups: HandleGetGroups; 11 | getValues: HandleGetValues; 12 | groupsRef: Ref; 13 | valuesRef: Ref; 14 | skuData: Ref; 15 | } 16 | 17 | export const skuInjectionKey = createInjectionKey('sku'); 18 | 19 | export interface SkuBaseSchema { 20 | id: number; 21 | name: string, 22 | [key: string]: unknown 23 | } 24 | 25 | export interface SkuValueSchema extends SkuBaseSchema { 26 | img_url?: string; 27 | } 28 | 29 | export type SkuValueSchemas = Array 30 | 31 | export interface SkuSchema extends SkuBaseSchema { 32 | leaf: SkuValueSchemas, 33 | } 34 | 35 | export type SkuSchemas = Array 36 | 37 | export interface CreateGroup { 38 | (label: string): Promise; 39 | } 40 | 41 | export interface CreateValue { 42 | (values: string[]): Promise; 43 | } 44 | 45 | export interface GetGroups { 46 | (): Promise; 47 | } 48 | 49 | export interface GetValues { 50 | (): Promise; 51 | } 52 | 53 | export interface HandleGetData { 54 | (force: boolean): Promise 55 | } 56 | 57 | export type HandleGetGroups = HandleGetData; 58 | export type HandleGetValues = HandleGetData; 59 | -------------------------------------------------------------------------------- /src/components/sku/styles/light.ts: -------------------------------------------------------------------------------- 1 | import { commonLight } from 'naive-ui/lib/_styles/common'; 2 | import type { ThemeCommonVars } from 'naive-ui/lib/_styles/common'; 3 | import type { Theme } from 'naive-ui/lib/_mixins'; 4 | 5 | export const self = (vars: ThemeCommonVars) => { 6 | const { 7 | borderColor 8 | } = vars; 9 | return { 10 | borderColor 11 | }; 12 | }; 13 | 14 | export type SkuThemeVars = ReturnType 15 | 16 | const skuLight: Theme<'Sku', SkuThemeVars> = { 17 | name: 'Sku', 18 | common: commonLight, 19 | self 20 | }; 21 | 22 | export default skuLight; 23 | export type SkuTheme = typeof skuLight 24 | -------------------------------------------------------------------------------- /src/components/sku/styles/sku.cssr.ts: -------------------------------------------------------------------------------- 1 | import { cB, c } from 'naive-ui/lib/_utils/cssr'; 2 | 3 | // vars: 4 | // --n-border-color 5 | export default cB('card', ` 6 | padding: 14px 10px; 7 | box-sizing: border-box; 8 | `, [ 9 | cB('group', ` 10 | width: 100%; 11 | `, [ 12 | cB('group-section', ` 13 | position: relative; 14 | overflow: hidden; 15 | `), 16 | cB('group-section:hover', {}, [ 17 | cB('group-close', ` 18 | transform: translate(0, 0); 19 | `) 20 | ]), 21 | cB('group-close', ` 22 | cursor: pointer; 23 | transition: all .3s; 24 | transform: translate(50px, 0); 25 | display: flex; 26 | align-items: center; 27 | position: absolute; 28 | right: 20px; 29 | `), 30 | cB('group-header', ` 31 | background-color: var(--n-border-color); 32 | padding: 7px 10px; 33 | `, [ 34 | c('> div', ` 35 | display: flex; 36 | align-items: center; 37 | `) 38 | ]), 39 | cB('group-body', ` 40 | padding: 0 10px; 41 | `, []) 42 | ]) 43 | ]); 44 | -------------------------------------------------------------------------------- /src/components/sku/utils.ts: -------------------------------------------------------------------------------- 1 | import { SkuSchemas, SkuValueSchema } from './interface'; 2 | 3 | export interface FlattenOptions { 4 | optionValue: string; 5 | optionText: string; 6 | } 7 | 8 | interface FlattenStockMap { 9 | [key: string]: Omit; 10 | } 11 | 12 | export interface StockItem { 13 | k_id: string | number; 14 | k: string; 15 | v_id: string | number; 16 | v: string; 17 | } 18 | 19 | export interface FlattenStock { 20 | [key: string]: unknown; 21 | data: StockItem[]; 22 | } 23 | 24 | // 计算每个sku后面有多少项 25 | export function getLevels (tree: SkuSchemas): number[] { 26 | const level: number[] = []; 27 | for (let i = tree.length - 1; i >= 0; i--) { 28 | if (tree[i + 1] && tree[i + 1].leaf) { 29 | level[i] = tree[i + 1].leaf.length * level[i + 1] || 1; 30 | } else { 31 | level[i] = 1; 32 | } 33 | } 34 | return level; 35 | } 36 | 37 | /** 38 | * 笛卡尔积运算 39 | * @param {[type]} tree [description] 40 | * @param {Array} stocks [description] 41 | * @return {[type]} [description] 42 | */ 43 | export function flatten (tree: SkuSchemas, stocks: FlattenStock[] = [], options: FlattenOptions = { 44 | optionText: 'name', 45 | optionValue: 'id', 46 | }): FlattenStock[] { 47 | const { optionValue, optionText } = options || {}; 48 | const result: FlattenStock[] = []; 49 | let skuLen = 0; 50 | const stockMap: FlattenStockMap = {}; // 记录已存在的stock的数据 51 | const level = getLevels(tree); 52 | if (tree.length === 0) return result; 53 | tree.forEach(sku => { 54 | const { leaf } = sku; 55 | if (!leaf || leaf.length === 0) return true; 56 | skuLen = (skuLen || 1) * leaf.length; 57 | }); 58 | // 根据已有的stocks生成一个map 59 | stocks.forEach(stock => { 60 | const { data, ...attr } = stock; 61 | const key = data.map(item => { 62 | return `${item.k_id}_${item.v_id}`; 63 | }).join('|'); 64 | stockMap[key] = attr; 65 | }); 66 | for (let i = 0; i < skuLen; i++) { 67 | const data: StockItem[] = []; 68 | const mapKey: string []= []; 69 | tree.forEach((sku, column) => { 70 | const { leaf } = sku; 71 | let item = {} as SkuValueSchema; 72 | if (!leaf || leaf.length === 0) return true; 73 | if (leaf.length > 1) { 74 | const row = parseInt((i / level[column]).toString(), 10) % leaf.length; 75 | item = tree[column].leaf[row]; 76 | } else { 77 | item = tree[column].leaf[0]; 78 | } 79 | if (!sku[optionValue] || !item[optionValue]) return; 80 | mapKey.push(`${sku[optionValue]}_${item[optionValue]}`); 81 | data.push({ 82 | k_id: sku[optionValue] as string | number, 83 | k: sku[optionText] as string, 84 | v_id: item[optionValue] as string | number, 85 | v: item[optionText] as string 86 | }); 87 | }); 88 | const { ...dataMap } = stockMap[mapKey.join('|')] || {}; 89 | // 从map中找出存在的sku并保留其值 90 | result.push({ ...dataMap, data }); 91 | } 92 | return result; 93 | } 94 | 95 | export function randomNumber (max = 100000): number { 96 | return Math.floor(Math.random() * (max + 1)); 97 | } 98 | -------------------------------------------------------------------------------- /src/components/space-view/SpaceView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/components/space-view/index.ts: -------------------------------------------------------------------------------- 1 | import SpaceView from './SpaceView.vue'; 2 | 3 | export { SpaceView }; 4 | -------------------------------------------------------------------------------- /src/components/spin-view/SpinView.tsx: -------------------------------------------------------------------------------- 1 | import { NSpin } from 'naive-ui'; 2 | import { defineComponent } from 'vue'; 3 | 4 | import './style.scss'; 5 | 6 | export default defineComponent({ 7 | name: 'SpinView', 8 | 9 | render () { 10 | return ( 11 |
12 | 13 |
14 | ); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/spin-view/index.ts: -------------------------------------------------------------------------------- 1 | import SpinView from './SpinView'; 2 | 3 | export { 4 | SpinView, 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/spin-view/style.scss: -------------------------------------------------------------------------------- 1 | .spin-view { 2 | width: 100%; 3 | height: 100%; 4 | min-height: 400px; 5 | max-height: 800px; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/title-divider/TitleDivider.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | 29 | -------------------------------------------------------------------------------- /src/components/title-divider/index.ts: -------------------------------------------------------------------------------- 1 | import TitleDivider from './TitleDivider.vue'; 2 | 3 | export { TitleDivider }; 4 | -------------------------------------------------------------------------------- /src/components/upload/UploadMain.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, PropType, ref } from 'vue'; 2 | import { NButton, NGrid, NGridItem, NModal, NSpace } from 'naive-ui'; 3 | import { SelectListSchema } from './image/UploadLIst'; 4 | import { FileInfo } from 'naive-ui/lib/upload/src/interface'; 5 | 6 | import './styles/main.scss'; 7 | 8 | const uploadMainProps = { 9 | title: String, 10 | type: { 11 | type: String as PropType<'image' | 'video'>, 12 | default: 'image' 13 | }, 14 | max: { 15 | type: Number, 16 | default: 0, 17 | } 18 | }; 19 | 20 | export default defineComponent({ 21 | name: 'UploadMain', 22 | 23 | props: uploadMainProps, 24 | 25 | emits: ['selected'], 26 | 27 | setup (props, { emit }) { 28 | const showModal = ref(false); 29 | // const dataRef = ref(); 30 | const selectedItems = ref(); 31 | 32 | // dataRef.value = [ 33 | // { 34 | // id: 1, 35 | // path: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg' 36 | // }, 37 | // { 38 | // id: 2, 39 | // path: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg' 40 | // } 41 | // ]; 42 | 43 | const handleSelectChange = (selects: SelectListSchema) => { 44 | selectedItems.value = Object.values(selects); 45 | }; 46 | 47 | const handleClick = () => showModal.value = !showModal.value; 48 | 49 | const confirm = () => { 50 | emit('selected', selectedItems.value); 51 | reset(); 52 | }; 53 | 54 | const cancel = () => { 55 | reset(); 56 | }; 57 | 58 | const reset = () => { 59 | showModal.value = false; 60 | selectedItems.value = []; 61 | }; 62 | 63 | return { 64 | // data: dataRef, 65 | confirm, 66 | cancel, 67 | showModal, 68 | handleClick, 69 | handleSelectChange, 70 | style: { 71 | minWidth: '920px', 72 | width: '828px', 73 | }, 74 | headerStyle: { 75 | padding: '15px 16px', 76 | borderBottom: '1px solid var(--n-border-color)' 77 | }, 78 | contentStyle: { 79 | padding: '16px', 80 | }, 81 | footerStyle: { 82 | padding: '0 16px 16px', 83 | } 84 | }; 85 | }, 86 | 87 | render () { 88 | const { 89 | // data, 90 | title, 91 | confirm, 92 | cancel, 93 | handleClick, 94 | style, 95 | headerStyle, 96 | contentStyle, 97 | footerStyle, 98 | $slots 99 | } = this; 100 | 101 | return ( 102 | <> 103 | { 104 | $slots.default ? $slots.default({ toggle: handleClick }) : null 105 | } 106 | {}} 118 | onClose={() => this.showModal = false} 119 | > 120 | {{ 121 | default: () => ( 122 | 123 |
124 | {$slots.header ? $slots.header() : null} 125 |
126 | 127 | 128 | {$slots.sider ? $slots.sider() : null} 129 | 130 | 131 | {$slots.lists ? $slots.lists() : null} 132 | 133 | 134 |
135 | ), 136 | footer: () => ( 137 | 138 | 139 | 取消 140 | 确定 141 | 142 | 143 | ), 144 | }} 145 |
146 | 147 | ); 148 | } 149 | }); 150 | -------------------------------------------------------------------------------- /src/components/upload/a.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, computed, CSSProperties } from 'vue'; 2 | import { useConfig, useTheme, useThemeClass } from 'naive-ui/lib/_mixins'; 3 | import type { ThemeProps } from 'naive-ui/lib/_mixins'; 4 | import { typographyLight } from 'naive-ui/lib/typography/styles'; 5 | import type { TypographyTheme } from 'naive-ui/lib/typography/styles'; 6 | import style from 'naive-ui/lib/typography/src/styles/a.cssr'; 7 | import type { ExtractPublicPropTypes } from 'naive-ui/lib/_utils'; 8 | 9 | const aProps = { 10 | ...(useTheme.props as ThemeProps), 11 | href: String, 12 | target: String, 13 | } as const; 14 | 15 | export type AProps = ExtractPublicPropTypes 16 | 17 | export default defineComponent({ 18 | name: 'A', 19 | props: aProps, 20 | emits: ['click'], 21 | setup (props, { emit }) { 22 | const { mergedClsPrefixRef, inlineThemeDisabled } = useConfig(props); 23 | const themeRef = useTheme( 24 | 'Typography', 25 | '-a', 26 | style, 27 | typographyLight, 28 | props, 29 | mergedClsPrefixRef 30 | ); 31 | const cssVarsRef = computed(() => { 32 | const { 33 | common: { cubicBezierEaseInOut }, 34 | self: { aTextColor } 35 | } = themeRef.value; 36 | return { 37 | '--n-text-color': aTextColor, 38 | '--n-bezier': cubicBezierEaseInOut 39 | }; 40 | }); 41 | const themeClassHandle = inlineThemeDisabled 42 | ? useThemeClass('a', undefined, cssVarsRef, props) 43 | : undefined; 44 | 45 | function handleClick () { 46 | emit('click'); 47 | } 48 | return { 49 | mergedClsPrefix: mergedClsPrefixRef, 50 | cssVars: inlineThemeDisabled ? undefined : cssVarsRef, 51 | themeClass: themeClassHandle?.themeClass, 52 | onRender: themeClassHandle?.onRender, 53 | handleClick, 54 | }; 55 | }, 56 | render () { 57 | const { 58 | $slots, 59 | handleClick, 60 | } = this; 61 | this.onRender?.(); 62 | return ( 63 | 68 | {$slots.default ? $slots.default() : ''} 69 | 70 | ); 71 | } 72 | }); 73 | -------------------------------------------------------------------------------- /src/components/upload/hooks/main.ts: -------------------------------------------------------------------------------- 1 | import { computed, ref, toRef } from 'vue'; 2 | import { uploadLight } from 'naive-ui/lib/upload/styles'; 3 | import style from 'naive-ui/lib/upload/src/styles/index.cssr'; 4 | import { useConfig, useTheme } from 'naive-ui/lib/_mixins'; 5 | import { FileInfo } from 'naive-ui/lib/upload/src/interface'; 6 | import { SelectListSchema } from '../image/UploadLIst'; 7 | import { MenuOption } from 'naive-ui'; 8 | 9 | export const useMain = (props: any) => { 10 | const mainRef = ref(); 11 | const fileListRef = toRef<{ fileList: FileInfo[]}, 'fileList'>(props, 'fileList'); 12 | const { mergedClsPrefixRef } = useConfig(props); 13 | const themeRef = useTheme( 14 | 'Upload', 15 | '-upload-main', 16 | style, 17 | uploadLight, 18 | {}, 19 | mergedClsPrefixRef 20 | ); 21 | 22 | const cssVarsRef = computed(() => { 23 | const { 24 | common: { cubicBezierEaseInOut }, 25 | self: { 26 | draggerColor, 27 | draggerBorder, 28 | draggerBorderHover, 29 | itemColorHover, 30 | itemColorHoverError, 31 | itemTextColorError, 32 | itemTextColorSuccess, 33 | itemTextColor, 34 | itemIconColor, 35 | itemDisabledOpacity, 36 | lineHeight, 37 | borderRadius, 38 | fontSize, 39 | itemBorderImageCardError, 40 | itemBorderImageCard, 41 | } 42 | } = themeRef.value; 43 | return { 44 | '--n-bezier': cubicBezierEaseInOut, 45 | '--n-border-radius': borderRadius, 46 | '--n-dragger-border': draggerBorder, 47 | '--n-dragger-border-hover': draggerBorderHover, 48 | '--n-dragger-color': draggerColor, 49 | '--n-font-size': fontSize, 50 | '--n-item-color-hover': itemColorHover, 51 | '--n-item-color-hover-error': itemColorHoverError, 52 | '--n-item-disabled-opacity': itemDisabledOpacity, 53 | '--n-item-icon-color': itemIconColor, 54 | '--n-item-text-color': itemTextColor, 55 | '--n-item-text-color-error': itemTextColorError, 56 | '--n-item-text-color-success': itemTextColorSuccess, 57 | '--n-line-height': lineHeight, 58 | '--n-item-border-image-card-error': itemBorderImageCardError, 59 | '--n-item-border-image-card': itemBorderImageCard 60 | } as any; 61 | }); 62 | 63 | const handleSelect = (selects: FileInfo[]) => { 64 | // fileListRef.value = [ 65 | // ...fileListRef.value, 66 | // ...selects 67 | // ]; 68 | handleUpdate([ 69 | ...fileListRef.value, 70 | ...selects 71 | ]); 72 | 73 | // 新增 74 | const { onSelect } = props; 75 | if (onSelect) onSelect(selects); 76 | }; 77 | 78 | const handleSelectChange = (selects: SelectListSchema) => { 79 | mainRef.value.handleSelectChange(selects); 80 | }; 81 | 82 | const handleRemove = (index: number) => { 83 | console.log('remove image'); 84 | fileListRef.value.splice(index, 1); 85 | handleUpdate(fileListRef.value); 86 | }; 87 | 88 | const handleUpdate = (files: FileInfo[]) => { 89 | const { 'onUpdate:fileList': _onUpdateFileList, onUpdateFileList } = props; 90 | if (_onUpdateFileList) _onUpdateFileList(files); 91 | if (onUpdateFileList) onUpdateFileList(files); 92 | }; 93 | 94 | // watch( 95 | // () => fileListRef.value, 96 | // () => { 97 | // emit('update:fileList', fileListRef.value); 98 | // }, 99 | // { 100 | // deep: true, 101 | // immediate: true, 102 | // } 103 | // ); 104 | 105 | const groupOptions = ref([ 106 | { 107 | label: '未分组', 108 | key: 1, 109 | }, 110 | { 111 | label: '自定义分组', 112 | key: 2, 113 | } 114 | ]); 115 | 116 | const renderCount = (options: MenuOption) => { 117 | console.log(options); 118 | return '(0)'; 119 | }; 120 | 121 | const mainMax = computed(() => { 122 | console.log(fileListRef.value); 123 | return props.max ? props.max - fileListRef.value.length : 0; 124 | }); 125 | 126 | return { 127 | mainRef, 128 | fileListRef, 129 | cssVarsRef, 130 | mergedClsPrefixRef, 131 | mainMax, 132 | groupOptions, 133 | renderCount, 134 | handleSelect, 135 | handleSelectChange, 136 | handleRemove, 137 | }; 138 | }; 139 | -------------------------------------------------------------------------------- /src/components/upload/image/FileList.tsx: -------------------------------------------------------------------------------- 1 | import { NButton, NImage } from 'naive-ui'; 2 | import { ImageInst } from 'naive-ui/lib/image/src/Image'; 3 | import { FileInfo } from 'naive-ui/lib/upload/src/interface'; 4 | import { NBaseIcon, NIconSwitchTransition } from 'naive-ui/lib/_internal'; 5 | import { EyeIcon, TrashIcon } from 'naive-ui/lib/_internal/icons'; 6 | import { defineComponent, inject, PropType, ref } from 'vue'; 7 | import { uploadImageMainInjectionKey } from './UploadImageMain'; 8 | 9 | const fileListProps = { 10 | index: { 11 | type: Number, 12 | required: true, 13 | }, 14 | 15 | clsPrefix: { 16 | type: String, 17 | required: true, 18 | }, 19 | 20 | file: { 21 | type: Object as PropType, 22 | required: true 23 | }, 24 | }; 25 | 26 | export default defineComponent({ 27 | name: 'FileList', 28 | 29 | props: fileListProps, 30 | 31 | setup () { 32 | const uploadImageMain = inject(uploadImageMainInjectionKey); 33 | const imageRef = ref(null); 34 | 35 | const handlePreview = () => { 36 | const { value } = imageRef; 37 | if (!value) return; 38 | value.click(); 39 | }; 40 | 41 | return { 42 | handlePreview, 43 | handleRemove: uploadImageMain?.handleRemove, 44 | imageRef, 45 | }; 46 | }, 47 | 48 | render () { 49 | const { 50 | index, 51 | clsPrefix, 52 | file, 53 | handlePreview, 54 | handleRemove, 55 | } = this; 56 | 57 | return ( 58 |
65 |
70 | 75 | 81 | 82 |
83 | 84 |
85 |
91 | 96 | {{ 97 | icon: () => ( 98 | 99 | {{ default: () => }} 100 | 101 | ) 102 | }} 103 | 104 | handleRemove!(index as number)} 108 | > 109 | {{ 110 | icon: () => ( 111 | 112 | {{ 113 | default: () => ( 114 | 115 | {{ default: () => }} 116 | 117 | ) 118 | }} 119 | 120 | ) 121 | }} 122 | 123 |
124 |
125 |
126 | ); 127 | } 128 | }); 129 | -------------------------------------------------------------------------------- /src/components/upload/image/ImageItem.tsx: -------------------------------------------------------------------------------- 1 | import { NIcon, NImage, useThemeVars } from 'naive-ui'; 2 | import { computed, defineComponent, PropType, StyleValue } from 'vue'; 3 | import { CheckmarkDoneCircle } from '@vicons/ionicons5'; 4 | import { SelectExtraListSchema } from './UploadLIst'; 5 | 6 | export default defineComponent({ 7 | name: 'UploadImage', 8 | 9 | props: { 10 | index: { 11 | type: Number, 12 | default: 0, 13 | }, 14 | 15 | item: { 16 | type: Object as PropType, 17 | default: () => {} 18 | }, 19 | }, 20 | 21 | emits: ['select', 'unSelect',], 22 | 23 | setup (props, { emit }) { 24 | const themeRef = useThemeVars(); 25 | const cssVarsRef = computed(() => { 26 | return { 27 | '--n-selected-color': themeRef.value.primaryColorHover 28 | }; 29 | }); 30 | 31 | const handleClick = () => { 32 | const current = { 33 | index: props.index, 34 | item: props.item, 35 | }; 36 | props.item.select ? emit('unSelect', current) : emit('select', current); 37 | }; 38 | 39 | return { 40 | cssVars: cssVarsRef as StyleValue, 41 | handleClick, 42 | }; 43 | }, 44 | 45 | render () { 46 | const { 47 | item, 48 | handleClick, 49 | } = this; 50 | return ( 51 |
55 | 59 |
60 | 61 | 62 | 63 |
64 |
65 | ); 66 | } 67 | }); 68 | -------------------------------------------------------------------------------- /src/components/upload/index.ts: -------------------------------------------------------------------------------- 1 | import UploadImageMain from './image/UploadImageMain'; 2 | import UploadVideoMain from './video/UploadVideoMain'; 3 | import UploadImage from './image/UploadImage'; 4 | import UploadVideo from './video/UploadVideo'; 5 | import UploadMain from './UploadMain'; 6 | 7 | export { 8 | UploadImageMain, 9 | UploadVideoMain, 10 | UploadImage, 11 | UploadVideo, 12 | UploadMain, 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/upload/styles/main.scss: -------------------------------------------------------------------------------- 1 | .upload-layout { 2 | .upload-header { 3 | margin-bottom: 6px; 4 | } 5 | 6 | .upload-sider { 7 | padding-right: 12px; 8 | border-right: 1px solid var(--n-border-color); 9 | } 10 | 11 | .n-menu .n-menu-item-content::before { 12 | left: 0; 13 | right: 0; 14 | } 15 | 16 | .list-image { 17 | width: 100%; 18 | height: 98px; 19 | position: relative; 20 | cursor: pointer; 21 | 22 | &__selected { 23 | position: absolute; 24 | bottom: 0; 25 | right: 0; 26 | display: none; 27 | 28 | .n-icon { 29 | color: var(--n-selected-color); 30 | } 31 | } 32 | } 33 | 34 | .selected { 35 | .list-image { 36 | &__selected { 37 | display: block; 38 | } 39 | } 40 | } 41 | 42 | .n-image { 43 | width: 100%; 44 | height: 98px; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/upload/styles/upload.scss: -------------------------------------------------------------------------------- 1 | .upload-modal { 2 | .upload-main { 3 | min-height: 400px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/enums/page.ts: -------------------------------------------------------------------------------- 1 | export enum Page { 2 | // 首页 3 | DASH_NORMAL = '/dashboard', 4 | // 系统登录 5 | SYSTEM_AUTH_SIGIN = '/system/auth/sigin' 6 | } 7 | -------------------------------------------------------------------------------- /src/enums/theme.ts: -------------------------------------------------------------------------------- 1 | export enum Theme { 2 | // 黑暗 3 | THEME_DARK = 'THEME_DARK', 4 | } 5 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue'; 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any>; 7 | export default component; 8 | } 9 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | import { DialogApi, LoadingBarApi, MessageApi, NotificationApi } from 'naive-ui'; 2 | 3 | declare global { 4 | interface Window { 5 | $dialog: DialogApi; 6 | $message: MessageApi; 7 | $loading: LoadingBarApi, 8 | $notify: NotificationApi, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/hooks/form.ts: -------------------------------------------------------------------------------- 1 | import { FormInst } from 'naive-ui'; 2 | import { ref } from 'vue'; 3 | 4 | export const useForm = () => { 5 | const formRef = ref(null); 6 | const loadingRef = ref(false); 7 | 8 | const formValidate = (errorCallback?: (error: any) => void): Promise => { 9 | return new Promise((resolve, reject) => { 10 | formRef.value?.validate((errors) => { 11 | if (errors) { 12 | console.log(errors); 13 | errorCallback && errorCallback(errors); 14 | reject(true); 15 | return; 16 | } 17 | 18 | resolve(false); 19 | }); 20 | }); 21 | }; 22 | 23 | const formSubmit = async (request: Promise, errorCallback?: (error: any) => void): Promise => { 24 | if (await formValidate(errorCallback)) return; 25 | 26 | return new Promise(async (resolve, reject) => { 27 | loadingRef.value = true; 28 | try { 29 | const response = await request; 30 | resolve(response); 31 | } catch (error) { 32 | reject(error); 33 | } 34 | loadingRef.value = false; 35 | }); 36 | }; 37 | 38 | return { 39 | formRef, 40 | formValidate, 41 | formSubmit, 42 | loadingRef 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /src/layout/Base.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 79 | 80 | 89 | -------------------------------------------------------------------------------- /src/layout/Light.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue'; 2 | import { NConfigProvider, lightTheme } from 'naive-ui'; 3 | import { RouterView } from 'vue-router'; 4 | 5 | export default defineComponent({ 6 | name: 'Light', 7 | 8 | setup () { 9 | // 10 | }, 11 | 12 | render () { 13 | return ( 14 | 17 | 18 | 19 | ); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /src/layout/Normal.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/layout/Secondary.tsx: -------------------------------------------------------------------------------- 1 | import { NLayout, NLayoutContent } from 'naive-ui'; 2 | import { defineComponent } from 'vue'; 3 | import { RouterView } from 'vue-router'; 4 | import LayoutBase from './Base.vue'; 5 | import { Sider as LayoutSider } from './components/sider'; 6 | 7 | import $style from './style.module.scss'; 8 | 9 | export default defineComponent({ 10 | name: 'Secondary', 11 | 12 | setup () { 13 | // 14 | }, 15 | 16 | render () { 17 | return ( 18 | 19 |
20 | 21 | 22 | {/* 23 | 24 | */} 25 | 26 | 27 | 28 | 29 |
30 |
31 | ); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /src/layout/components/aside/Aside.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 123 | -------------------------------------------------------------------------------- /src/layout/components/aside/index.ts: -------------------------------------------------------------------------------- 1 | import Aside from './Aside.vue'; 2 | 3 | export { Aside }; 4 | -------------------------------------------------------------------------------- /src/layout/components/header/index.ts: -------------------------------------------------------------------------------- 1 | import Header from './Header.vue'; 2 | 3 | export { Header }; 4 | -------------------------------------------------------------------------------- /src/layout/components/sider/Sider.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 50 | -------------------------------------------------------------------------------- /src/layout/components/sider/index.ts: -------------------------------------------------------------------------------- 1 | import Sider from './Sider.vue'; 2 | 3 | export { Sider }; 4 | -------------------------------------------------------------------------------- /src/layout/style.module.scss: -------------------------------------------------------------------------------- 1 | .hiddenContainer, 2 | .hiddencontent { 3 | padding: 12px 24px; 4 | } 5 | 6 | // .hiddenMenus { 7 | // :global(.n-menu) { 8 | // margin-left: -8px; 9 | // margin-right: 8px; 10 | // } 11 | // } 12 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './styles/tailwind.css'; 2 | import { createApp } from 'vue'; 3 | import App from './App.vue'; 4 | import { setupStore } from './store'; 5 | import { setupRouter } from './router'; 6 | import { setMeta } from './utils/naive-ui'; 7 | import NutUI from '@nutui/nutui'; 8 | import '@nutui/nutui/dist/style.css'; 9 | 10 | const bootstrap = async (): Promise => { 11 | const app = createApp(App); 12 | 13 | // 挂载状态管理 14 | setupStore(app); 15 | 16 | // 挂载路由 17 | setupRouter(app); 18 | // await router.isReady(); 19 | 20 | setMeta(); 21 | 22 | app.use(NutUI); 23 | 24 | app.mount('#app', true); 25 | }; 26 | 27 | bootstrap(); 28 | -------------------------------------------------------------------------------- /src/router/constant.ts: -------------------------------------------------------------------------------- 1 | export const Layout = () => import('@/layout/Base.vue'); 2 | export const NormalLayout = () => import('@/layout/Normal.vue'); 3 | export const SecondaryLayout = () => import('@/layout/Secondary'); 4 | export const LightLayout = () => import('@/layout/Light'); 5 | -------------------------------------------------------------------------------- /src/router/guards.ts: -------------------------------------------------------------------------------- 1 | import { Page } from '@/enums/page'; 2 | import { useRouteStoreWhithout } from '@/store/modules/router'; 3 | import { ACCESS_TOKEN } from '@/store/types'; 4 | import { storage } from '@/utils/storage'; 5 | import { RouteLocationRaw, Router } from 'vue-router'; 6 | 7 | const SYSTEM_AUTH_SIGIN = Page.SYSTEM_AUTH_SIGIN; 8 | 9 | const whiteLists = [ 10 | SYSTEM_AUTH_SIGIN 11 | ]; 12 | 13 | export const routeGuards = (router: Router) => { 14 | const useRouterStore = useRouteStoreWhithout(); 15 | 16 | router.beforeEach(async (to, from, next) => { 17 | const Loading = window.$loading || null; 18 | Loading && Loading.start(); 19 | 20 | // 白名单控制 21 | if (whiteLists.includes(to.path as Page)) { 22 | next(); 23 | return; 24 | } 25 | 26 | const token = storage.get(ACCESS_TOKEN); 27 | if (!token) { 28 | // 开放路由 29 | if (to.meta.publish) { 30 | next(); 31 | return; 32 | } 33 | // 跳转登录页 34 | const redirectLogin: RouteLocationRaw = { 35 | path: SYSTEM_AUTH_SIGIN, 36 | replace: true 37 | }; 38 | 39 | if (to.path) { 40 | redirectLogin.query = { 41 | redirect: to.path 42 | }; 43 | } 44 | 45 | next(redirectLogin); 46 | return; 47 | } 48 | 49 | if (useRouterStore.getIsAdded) { 50 | next(); 51 | return; 52 | } 53 | 54 | // 获取路由 添加异步路由 55 | const routes = await useRouterStore.generateRoutes(); 56 | routes.forEach(route => { 57 | router.addRoute(route); 58 | }); 59 | 60 | const redirectPath = (from.query.redirect || to.path) as string; 61 | const redirect = decodeURIComponent(redirectPath); 62 | const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }; 63 | 64 | useRouterStore.setAdded(true); 65 | next(nextData); 66 | Loading && Loading.finish(); 67 | }); 68 | 69 | router.afterEach((to) => { 70 | document.title = (to?.meta?.title as string) || document.title; 71 | 72 | const Loading = window.$loading || null; 73 | Loading && Loading.finish(); 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /src/router/icons.ts: -------------------------------------------------------------------------------- 1 | import { NIcon } from 'naive-ui'; 2 | import { h, Component } from 'vue'; 3 | 4 | export const renderIcon = (icon: Component) => { 5 | return () => h(NIcon, null, { default: () => h(icon) }); 6 | }; 7 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { Page } from '@/enums/page'; 2 | import { App } from 'vue'; 3 | import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'; 4 | import { routeGuards } from './guards'; 5 | 6 | const moduleRoutes = () => { 7 | const modules = import.meta.globEager('./modules/**/*.ts'); 8 | const routeModules: RouteRecordRaw[] = []; 9 | 10 | Object.keys(modules).forEach((key) => { 11 | const routes = modules[key].default || {}; 12 | const list = Array.isArray(routes) ? [...routes] : [routes]; 13 | 14 | routeModules.push(...list); 15 | }); 16 | 17 | return routeModules; 18 | }; 19 | 20 | const baseRoute: RouteRecordRaw = { 21 | path: '/', 22 | name: 'root', 23 | redirect: Page.DASH_NORMAL 24 | }; 25 | 26 | const siginRoute: RouteRecordRaw = { 27 | path: '/system/auth/sigin', 28 | name: 'system-auth-sigin', 29 | component: () => import('@/views/system/auth/sigin.vue') 30 | }; 31 | 32 | export const asyncRoutes = [...moduleRoutes()]; 33 | 34 | export const constantRoutes = [ 35 | baseRoute, 36 | siginRoute 37 | ]; 38 | 39 | export const router = createRouter({ 40 | history: createWebHashHistory(''), 41 | routes: constantRoutes, 42 | strict: true, 43 | scrollBehavior: () => ({ left: 0, top: 0 }) 44 | }); 45 | 46 | export const setupRouter = (app: App) => { 47 | app.use(router); 48 | 49 | routeGuards(router); 50 | }; 51 | -------------------------------------------------------------------------------- /src/router/interface.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue'; 2 | import { RouteRecordRaw } from 'vue-router'; 3 | 4 | export type Component = 5 | | ReturnType 6 | | (() => Promise) 7 | | (() => Promise); 8 | 9 | export interface AppRouteRecordRaw extends Omit { 10 | children?: AppRouteRecordRaw[]; 11 | } 12 | -------------------------------------------------------------------------------- /src/router/modules/dashboard.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router'; 2 | import { Layout } from '../constant'; 3 | 4 | const routes: RouteRecordRaw[] = [ 5 | { 6 | path: '/dashboard', 7 | name: 'dashboard', 8 | meta: { 9 | title: '概览', 10 | sort: 0 11 | }, 12 | component: Layout, 13 | redirect: { 14 | path: '/dashboard/console' 15 | }, 16 | children: [ 17 | { 18 | path: '/dashboard/console', 19 | name: 'dashboard-console', 20 | meta: { 21 | title: '面板' 22 | }, 23 | component: () => import('@/views/dashboard/console') 24 | } 25 | ] 26 | } 27 | ]; 28 | 29 | export default routes; 30 | -------------------------------------------------------------------------------- /src/router/modules/system.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router'; 2 | import { Layout } from '../constant'; 3 | 4 | const routes: RouteRecordRaw[] = [ 5 | { 6 | path: '/system', 7 | name: 'system', 8 | meta: { 9 | title: '系统', 10 | sort: 1 11 | }, 12 | component: Layout, 13 | redirect: { 14 | path: '/system/settings' 15 | }, 16 | children: [ 17 | { 18 | path: '/system/settings', 19 | name: 'system-setting', 20 | meta: { 21 | title: '系统配置' 22 | }, 23 | component: () => import('@/views/system/settings/index.vue') 24 | } 25 | ] 26 | } 27 | ]; 28 | 29 | export default routes; 30 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia'; 2 | import { App } from 'vue'; 3 | 4 | export const store = createPinia(); 5 | 6 | export const setupStore = (app: App) => { 7 | app.use(store); 8 | }; 9 | -------------------------------------------------------------------------------- /src/store/modules/layout.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { Component } from 'vue'; 3 | 4 | type FooterComponent = Component | null; 5 | 6 | interface LayoutState { 7 | footer: FooterComponent; 8 | } 9 | 10 | export const useLayoutStore = defineStore({ 11 | id: 'layout', 12 | 13 | state: (): LayoutState => ({ 14 | footer: null 15 | }), 16 | 17 | actions: { 18 | setFooter (render: FooterComponent | null) { 19 | this.footer = render; 20 | } 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /src/store/modules/router.ts: -------------------------------------------------------------------------------- 1 | import { constantRoutes } from '@/router'; 2 | import { generateRoutes } from '@/router/generator'; 3 | import { defineStore } from 'pinia'; 4 | import { toRaw } from 'vue'; 5 | import { RouteRecordRaw } from 'vue-router'; 6 | 7 | export interface IRouterState { 8 | menus: RouteRecordRaw[], 9 | routes: any[], 10 | addRoutes: any[], 11 | isAdded: boolean, 12 | } 13 | 14 | export const useRouterStore = defineStore({ 15 | id: 'router', 16 | 17 | state: (): IRouterState => ({ 18 | menus: [], 19 | routes: constantRoutes, 20 | addRoutes: [], 21 | isAdded: false 22 | }), 23 | 24 | getters: { 25 | getMenus (): RouteRecordRaw[] { 26 | return this.menus; 27 | }, 28 | getIsAdded (): boolean { 29 | return this.isAdded; 30 | } 31 | }, 32 | 33 | actions: { 34 | setRoutes (routes: any[]) { 35 | this.addRoutes = routes; 36 | this.routes = constantRoutes.concat(routes); 37 | }, 38 | setMenus (menus: RouteRecordRaw[]) { 39 | this.menus = menus; 40 | }, 41 | setAdded (added: boolean) { 42 | this.isAdded = added; 43 | }, 44 | async generateRoutes () { 45 | let asyncRoutes: RouteRecordRaw[] = []; 46 | // 异步获取路由 47 | try { 48 | asyncRoutes = await generateRoutes(); 49 | } catch (error) { 50 | console.log(error); 51 | } 52 | 53 | this.setRoutes(asyncRoutes); 54 | this.setMenus(asyncRoutes); 55 | return toRaw(asyncRoutes); 56 | } 57 | } 58 | }); 59 | 60 | export const useRouteStoreWhithout = () => { 61 | return useRouterStore(); 62 | }; 63 | -------------------------------------------------------------------------------- /src/store/modules/theme.ts: -------------------------------------------------------------------------------- 1 | import { storage } from '@/utils/storage'; 2 | import { defineStore } from 'pinia'; 3 | import { Theme } from '@/enums/theme'; 4 | 5 | interface ThemeSettingSate { 6 | dark: boolean; 7 | fontSize: string; 8 | } 9 | 10 | export const useThemeStore = defineStore({ 11 | id: 'theme', 12 | state: (): ThemeSettingSate => ({ 13 | dark: storage.get(Theme.THEME_DARK, false), 14 | fontSize: '12px' 15 | }), 16 | getters: { 17 | getDark (): boolean { 18 | return this.dark; 19 | }, 20 | getFontSize (): string { 21 | return this.fontSize; 22 | } 23 | }, 24 | actions: { 25 | toggleDark () { 26 | const dark = !this.dark; 27 | this.$patch({ 28 | dark 29 | }); 30 | storage.set(Theme.THEME_DARK, dark); 31 | }, 32 | 33 | setFontSize (size: string) { 34 | this.fontSize = size; 35 | } 36 | } 37 | }); 38 | 39 | // 外部调用 40 | export const useThemeStoreWithout = () => { 41 | return useThemeStore(); 42 | }; 43 | -------------------------------------------------------------------------------- /src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { profile, sigin, signout } from '@/api/system/user'; 2 | import { storage } from '@/utils/storage'; 3 | import { defineStore } from 'pinia'; 4 | import { ACCESS_TOKEN, CURRENT_USER, USER } from '../types'; 5 | import { useRouteStoreWhithout } from './router'; 6 | 7 | interface IUserState { 8 | token: string; 9 | username: string; 10 | avatar: string; 11 | info: unknown; 12 | } 13 | 14 | export const useUserStore = defineStore({ 15 | id: 'user', 16 | 17 | state: (): IUserState => ({ 18 | token: storage.get(ACCESS_TOKEN, ''), 19 | username: '', 20 | avatar: '', 21 | info: storage.get(USER, {}) 22 | }), 23 | 24 | getters: { 25 | getToken (): string { 26 | return this.token; 27 | }, 28 | getProfile (): unknown { 29 | return this.info; 30 | } 31 | }, 32 | 33 | actions: { 34 | setToken (token: string) { 35 | this.token = token; 36 | }, 37 | setUserInfo (info: unknown) { 38 | storage.set(CURRENT_USER, info); 39 | this.info = info; 40 | }, 41 | // 登录 42 | async login (data: any) { 43 | return new Promise(async (resolve, reject) => { 44 | try { 45 | const { token } = await sigin(data); 46 | console.log(token); 47 | storage.set(ACCESS_TOKEN, token); 48 | this.setToken(token); 49 | 50 | // 重载路由 51 | const useRouterStore = useRouteStoreWhithout(); 52 | useRouterStore.setAdded(false); 53 | 54 | await this.setProfile(); 55 | 56 | resolve(token); 57 | } catch (error) { 58 | reject(error); 59 | } 60 | }); 61 | }, 62 | async setProfile () { 63 | return new Promise(async (resolve, reject) => { 64 | try { 65 | const info = await profile(); 66 | this.setUserInfo(info); 67 | 68 | resolve(info); 69 | } catch (error) { 70 | reject(error); 71 | } 72 | }); 73 | }, 74 | // 登出 75 | async logout () { 76 | // token destroy 77 | await signout(); 78 | this.logoutRemove(); 79 | return Promise.resolve(true); 80 | }, 81 | 82 | logoutRemove () { 83 | storage.remove(CURRENT_USER); 84 | storage.remove(ACCESS_TOKEN); 85 | this.setUserInfo({}); 86 | this.setToken(''); 87 | 88 | return Promise.resolve(); 89 | } 90 | } 91 | }); 92 | 93 | // 外部调用 94 | export const useUserStoreWithout = () => { 95 | return useUserStore(); 96 | }; 97 | -------------------------------------------------------------------------------- /src/store/types.ts: -------------------------------------------------------------------------------- 1 | export const ACCESS_TOKEN = 'ACCESS_TOKEN'; 2 | export const CURRENT_USER = 'CURRENT_USER'; 3 | export const USER = 'USER'; 4 | -------------------------------------------------------------------------------- /src/styles/common.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #app { 4 | width: 100vw; 5 | height: 100vh; 6 | position: relative; 7 | min-width: 1400px; 8 | font-family: Avenir, Helvetica, Arial, sans-serif; 9 | -webkit-font-smoothing: antialiased; 10 | } 11 | 12 | a { 13 | background: transparent; 14 | text-decoration: none; 15 | outline: none; 16 | cursor: pointer; 17 | transition: color .2s ease; 18 | } 19 | 20 | a:active, 21 | a:hover { 22 | outline-width: 0; 23 | } 24 | 25 | a:active, 26 | a:hover { 27 | outline: 0; 28 | text-decoration: none; 29 | } 30 | 31 | .title-card { 32 | .n-card-header { 33 | padding: 0 10px; 34 | } 35 | 36 | .n-card-header__main span { 37 | position: relative; 38 | padding: 0 10px; 39 | 40 | &::before { 41 | position: absolute; 42 | content: " "; 43 | width: 3px; 44 | height: 100%; 45 | left: 0; 46 | top: 0; 47 | background-color: var(--primary-color); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/styles/core.scss: -------------------------------------------------------------------------------- 1 | @import "./common"; 2 | -------------------------------------------------------------------------------- /src/styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/theme.ts: -------------------------------------------------------------------------------- 1 | import { GlobalThemeOverrides } from 'naive-ui'; 2 | 3 | export const globalTheme = (theme: any): GlobalThemeOverrides => { 4 | return { 5 | common: { 6 | baseColor: '#fff', 7 | fontSize: theme.getFontSize, 8 | fontSizeMedium: theme.getFontSize, 9 | fontSizeLarge: theme.getFontSize, 10 | fontSizeHuge: theme.getFontSize, 11 | fontSizeSmall: theme.getFontSize, 12 | primaryColor: '#5156be', 13 | primaryColorHover: '#4549a2', 14 | primaryColorPressed: '#4549a2', 15 | primaryColorSuppl: '#4549a2', 16 | }, 17 | Button: { 18 | fontSizeMedium: theme.getFontSize, 19 | }, 20 | // DataTable: { 21 | // fontSizeMedium: theme.getFontSize, 22 | // }, 23 | // Input: { 24 | // fontSizeLarge: theme.getFontSize, 25 | // fontSizeMedium: theme.getFontSize, 26 | // }, 27 | Form: { 28 | labelFontSizeLeftMedium: theme.getFontSize, 29 | feedbackFontSizeMedium: theme.getFontSize, 30 | feedbackFontSizeLarge: theme.getFontSize, 31 | feedbackFontSizeSmall: theme.getFontSize 32 | } 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /src/utils/http/http.ts: -------------------------------------------------------------------------------- 1 | import { useUserStoreWithout } from '@/store/modules/user'; 2 | import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; 3 | import { CreateAxiosOptions, RequestOptions, Result } from './interface'; 4 | import { checkStatus } from './status'; 5 | 6 | export class Http { 7 | private instance: AxiosInstance; 8 | 9 | constructor (options: CreateAxiosOptions) { 10 | this.instance = axios.create(options); 11 | this.interceptors(); 12 | } 13 | 14 | interceptors () { 15 | // 请求拦截器 16 | this.instance.interceptors.request.use((config: CreateAxiosOptions) => { 17 | console.log('axios reponse'); 18 | const userStore = useUserStoreWithout(); 19 | const token = userStore.getToken; 20 | 21 | // 设置token 22 | if (!!token && config.requestOptions?.token !== false) { 23 | config.headers!.Authorization = `Bearer ${token}`; 24 | } 25 | 26 | return config; 27 | }, undefined); 28 | 29 | // 请求拦截器错误处理 30 | this.instance.interceptors.response.use(undefined, (error: any) => { 31 | console.log('axios reponse status error', error); 32 | const $dialog = window.$dialog; 33 | const $message = window.$message; 34 | const { response, code, message } = error || {}; 35 | // TODO 此处要根据后端接口返回格式修改 36 | const msg: string = 37 | response && response.data && response.data.message ? response.data.message : ''; 38 | const err: string = error.toString(); 39 | try { 40 | if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) { 41 | $message.error('接口请求超时,请刷新页面重试!'); 42 | return; 43 | } 44 | if (err && err.includes('Network Error')) { 45 | $dialog.info({ 46 | title: '网络异常', 47 | content: '请检查您的网络连接是否正常', 48 | positiveText: '确定', 49 | // negativeText: '取消', 50 | closable: false, 51 | maskClosable: false, 52 | onPositiveClick: () => {}, 53 | onNegativeClick: () => {} 54 | }); 55 | return Promise.reject(error); 56 | } 57 | } catch (error) { 58 | throw new Error(error as any); 59 | } 60 | 61 | checkStatus(error.response && error.response.status, msg); 62 | return Promise.reject(response?.data); 63 | }); 64 | } 65 | 66 | request (config: AxiosRequestConfig, options: RequestOptions = {}): Promise { 67 | const configure = Object.assign({}, config, { 68 | requestOptions: options 69 | }); 70 | 71 | return new Promise((resolve, reject) => { 72 | this.instance 73 | .request(configure) 74 | .then((response: AxiosResponse) => { 75 | const { data } = response; 76 | resolve(data as unknown as T); 77 | }) 78 | .catch((e: Error) => { 79 | reject(e); 80 | }); 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/utils/http/index.ts: -------------------------------------------------------------------------------- 1 | import { Http } from './http'; 2 | 3 | const { 4 | VITE_HOST, 5 | VITE_API_PREFIX, 6 | VITE_API_VERSION 7 | } = import.meta.env; 8 | 9 | export const http = new Http({ 10 | timeout: 10 * 1000, 11 | headers: { 12 | 'Content-Type': 'application/json;charset=UTF-8' 13 | }, 14 | baseURL: `${VITE_HOST}/${VITE_API_PREFIX}/${VITE_API_VERSION}`, 15 | requestOptions: { 16 | token: true 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/utils/http/interface.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from 'axios'; 2 | 3 | export interface CreateAxiosOptions extends AxiosRequestConfig { 4 | requestOptions?: RequestOptions; 5 | } 6 | 7 | export interface RequestOptions { 8 | token?: boolean; 9 | } 10 | 11 | export interface Result { 12 | [key: string]: T 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/http/status.ts: -------------------------------------------------------------------------------- 1 | import { useUserStoreWithout } from '@/store/modules/user'; 2 | import { useRoute, useRouter } from 'vue-router'; 3 | 4 | export const checkStatus = async (status: number, msg: string): Promise => { 5 | const $message = window.$message; 6 | switch (status) { 7 | case 400: 8 | $message.error(msg); 9 | break; 10 | // 401: 未登录 11 | // 未登录则跳转登录页面,并携带当前页面的路径 12 | // 在登录成功后返回当前页面,这一步需要在登录页操作。 13 | case 401: { 14 | $message.error('用户没有权限(令牌、用户名、密码错误)!'); 15 | 16 | // 执行注销操作 17 | const userStore = useUserStoreWithout(); 18 | await userStore.logoutRemove(); 19 | const router = useRouter(); 20 | const route = useRoute(); 21 | router.push({ 22 | name: 'system-auth-sigin', 23 | query: { 24 | redirect: route.fullPath 25 | } 26 | }); 27 | break; 28 | } 29 | case 403: 30 | $message.error('用户得到授权,但是访问是被禁止的。!'); 31 | break; 32 | // 404请求不存在 33 | case 404: 34 | $message.error('网络请求错误,未找到该资源!'); 35 | break; 36 | case 405: 37 | $message.error('网络请求错误,请求方法未允许!'); 38 | break; 39 | case 408: 40 | $message.error('网络请求超时'); 41 | break; 42 | case 500: 43 | $message.error('服务器错误,请联系管理员!'); 44 | break; 45 | case 501: 46 | $message.error('网络未实现'); 47 | break; 48 | case 502: 49 | $message.error('网络错误'); 50 | break; 51 | case 503: 52 | $message.error('服务不可用,服务器暂时过载或维护!'); 53 | break; 54 | case 504: 55 | $message.error('网络超时'); 56 | break; 57 | case 505: 58 | $message.error('http版本不支持该请求!'); 59 | break; 60 | default: 61 | $message.error(msg); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { cloneDeep } from 'lodash-es'; 2 | 3 | /** 4 | * 混合菜单 5 | * */ 6 | export function generatorMenuMix (routerMap: Array, routerName: string, location: string) { 7 | const cloneRouterMap = cloneDeep(routerMap); 8 | const newRouter = filterRouter(cloneRouterMap); 9 | 10 | if (location === 'header') { 11 | const firstRouter: any[] = []; 12 | newRouter.forEach((item) => { 13 | const isRoot = isRootRouter(item); 14 | const info = isRoot ? item.children[0] : item; 15 | info.children = undefined; 16 | const currentMenu = { 17 | ...info, 18 | ...info.meta, 19 | label: info.meta?.title, 20 | key: info.name 21 | }; 22 | firstRouter.push(currentMenu); 23 | }); 24 | return firstRouter; 25 | } else { 26 | const routes = getChildrenRouter(newRouter.filter((item) => item.name === routerName)); 27 | 28 | // 无子菜单隐藏 29 | return routes.length && routes[0].children ? routes[0].children : []; 30 | } 31 | } 32 | 33 | /** 34 | * 递归组装子菜单 35 | * */ 36 | export function getChildrenRouter (routerMap: Array) { 37 | return filterRouter(routerMap).map((item) => { 38 | const isRoot = isRootRouter(item); 39 | console.log(isRoot); 40 | const info = isRoot ? item.children[0] : item; 41 | console.log(info); 42 | const currentMenu = { 43 | ...info, 44 | ...info.meta, 45 | label: info.meta?.title, 46 | key: info.name 47 | }; 48 | // 是否有子菜单,并递归处理 49 | if (info.children && info.children.length > 0) { 50 | // Recursion 51 | currentMenu.children = getChildrenRouter(info.children); 52 | currentMenu.children.length === 0 && delete currentMenu.children; 53 | } 54 | return currentMenu; 55 | }); 56 | } 57 | 58 | /** 59 | * 递归组装菜单格式 60 | */ 61 | export function generatorMenu (routerMap: Array) { 62 | return filterRouter(routerMap).map((item) => { 63 | const isRoot = isRootRouter(item); 64 | const info = isRoot ? item.children[0] : item; 65 | const currentMenu = { 66 | ...info, 67 | ...info.meta, 68 | label: info.meta?.title, 69 | key: info.name, 70 | icon: isRoot ? item.meta?.icon : info.meta?.icon 71 | }; 72 | // 是否有子菜单,并递归处理 73 | if (info.children && info.children.length > 0) { 74 | // Recursion 75 | currentMenu.children = generatorMenu(info.children); 76 | } 77 | return currentMenu; 78 | }); 79 | } 80 | 81 | /** 82 | * 判断根路由 Router 83 | * */ 84 | export function isRootRouter (item: any) { 85 | return item.meta?.alwaysShow != true && item.children?.length === 1; 86 | } 87 | 88 | /** 89 | * 排除Router 90 | * */ 91 | export function filterRouter (routerMap: Array) { 92 | return routerMap.filter((item) => { 93 | return ( 94 | (item.meta?.hidden || false) != true && 95 | !['/:path(.*)*', '/'].includes(item.path) 96 | ); 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /src/utils/naive-ui.ts: -------------------------------------------------------------------------------- 1 | export const setMeta = () => { 2 | const meta = document.createElement('meta'); 3 | meta.name = 'naive-ui-style'; 4 | document.head.appendChild(meta); 5 | }; 6 | -------------------------------------------------------------------------------- /src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | import store from 'store2'; 2 | 3 | export const storage = store.namespace('eamesh'); 4 | -------------------------------------------------------------------------------- /src/views/article/category/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 67 | -------------------------------------------------------------------------------- /src/views/article/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 79 | -------------------------------------------------------------------------------- /src/views/assets/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { SpaceView } from '@/components/space-view'; 2 | import { defineComponent } from 'vue'; 3 | 4 | export default defineComponent({ 5 | name: 'AssetsDashboard', 6 | 7 | setup () { 8 | 9 | }, 10 | 11 | render () { 12 | return ( 13 | 14 | 资产总览 15 | 16 | ); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/views/assets/reconciliation.tsx: -------------------------------------------------------------------------------- 1 | import { SpaceView } from '@/components/space-view'; 2 | import { defineComponent } from 'vue'; 3 | 4 | export default defineComponent({ 5 | name: 'AssetsReconciliation', 6 | 7 | setup () { 8 | 9 | }, 10 | 11 | render () { 12 | return ( 13 | 14 | 对账单 15 | 16 | ); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/views/dashboard/components/Line.tsx: -------------------------------------------------------------------------------- 1 | import { ApexOptions } from 'apexcharts'; 2 | import { NCard } from 'naive-ui'; 3 | import { defineComponent, h, reactive } from 'vue'; 4 | import VueApexCharts from 'vue3-apexcharts'; 5 | import { dataSeries } from './dataSeries'; 6 | 7 | export default defineComponent({ 8 | name: 'LineChart', 9 | 10 | setup () { 11 | let ts2 = 1484418600000; 12 | const dates = []; 13 | for (let i = 0; i < 120; i++) { 14 | ts2 = ts2 + 86400000; 15 | const innerArr = [ts2, dataSeries[1][i].value]; 16 | dates.push(innerArr); 17 | } 18 | 19 | const data = reactive<{ 20 | series: any, 21 | chartOptions: ApexOptions 22 | }>({ 23 | series: [{ 24 | name: 'Session Duration', 25 | data: [45, 52, 38, 24, 33, 26, 21, 20, 6, 8, 15, 10] 26 | }, { 27 | name: 'Page Views', 28 | data: [36, 42, 60, 42, 13, 18, 29, 37, 36, 51, 32, 35] 29 | }, { 30 | name: 'Total Visits', 31 | data: [89, 56, 74, 98, 72, 38, 64, 46, 84, 58, 46, 49] 32 | }], 33 | chartOptions: { 34 | chart: { 35 | type: 'line', 36 | zoom: { 37 | enabled: !1 38 | }, 39 | toolbar: { 40 | show: !1 41 | } 42 | }, 43 | colors: ['#5156be', '#2ab57d'], 44 | dataLabels: { 45 | enabled: !1 46 | }, 47 | stroke: { 48 | width: [3, 4, 3], 49 | curve: 'straight', 50 | dashArray: [0, 8, 5] 51 | }, 52 | 53 | title: { 54 | text: '访客', 55 | align: 'left', 56 | style: { 57 | fontWeight: '500' 58 | } 59 | }, 60 | markers: { 61 | size: 0, 62 | hover: { 63 | sizeOffset: 6 64 | } 65 | }, 66 | xaxis: { 67 | categories: ['01 Jan', '02 Jan', '03 Jan', '04 Jan', '05 Jan', '06 Jan', '07 Jan', '08 Jan', '09 Jan', '10 Jan', '11 Jan', '12 Jan'] 68 | }, 69 | tooltip: { 70 | y: [{ 71 | title: { 72 | formatter: function (e) { 73 | return e + ' (mins)'; 74 | } 75 | } 76 | }, { 77 | title: { 78 | formatter: function (e) { 79 | return e + ' per session'; 80 | } 81 | } 82 | }, { 83 | title: { 84 | formatter: function (e) { 85 | return e; 86 | } 87 | } 88 | }] 89 | }, 90 | grid: { 91 | borderColor: '#f1f1f1' 92 | }, 93 | responsive: [ 94 | { 95 | breakpoint: 1760, 96 | options: { 97 | chart: { 98 | height: 338 99 | } 100 | } 101 | }, 102 | { 103 | breakpoint: 1620, 104 | options: { 105 | chart: { 106 | height: 301 107 | } 108 | } 109 | } 110 | ] 111 | }, 112 | }); 113 | 114 | return { 115 | data 116 | }; 117 | }, 118 | 119 | render () { 120 | const { 121 | data 122 | } = this; 123 | return ( 124 | 125 | { 126 | h(VueApexCharts, { 127 | height: 387, 128 | options: data.chartOptions, 129 | series: data.series, 130 | }) 131 | } 132 | 133 | ); 134 | } 135 | }); 136 | -------------------------------------------------------------------------------- /src/views/dashboard/components/Pie.tsx: -------------------------------------------------------------------------------- 1 | import { ApexOptions } from 'apexcharts'; 2 | import { NCard, NNumberAnimation, NSpace, NStatistic } from 'naive-ui'; 3 | import { defineComponent, h, reactive } from 'vue'; 4 | import VueApexCharts from 'vue3-apexcharts'; 5 | 6 | export default defineComponent({ 7 | name: 'PieChart', 8 | 9 | setup () { 10 | const data = reactive<{ 11 | series: any, 12 | chartOptions: ApexOptions 13 | }>({ 14 | series: [100, 250], 15 | 16 | chartOptions: { 17 | chart: { 18 | width: 227, 19 | height: 227, 20 | type: 'pie' 21 | }, 22 | labels: ['Ethereum', 'Bitcoin'], 23 | colors: ['#777aca', '#5156be'], 24 | stroke: { 25 | width: 0 26 | }, 27 | legend: { 28 | show: !1 29 | }, 30 | dataLabels: { 31 | formatter (val: number, opts) { 32 | console.log(opts); 33 | return val.toFixed(0); 34 | } 35 | }, 36 | responsive: [ 37 | { 38 | breakpoint: 1760, 39 | options: { 40 | chart: { 41 | width: 200 42 | } 43 | } 44 | }, 45 | { 46 | breakpoint: 1660, 47 | options: { 48 | chart: { 49 | width: 150 50 | } 51 | } 52 | } 53 | ] 54 | } 55 | }); 56 | 57 | return { 58 | data 59 | }; 60 | }, 61 | 62 | render () { 63 | const { 64 | data 65 | } = this; 66 | return ( 67 | 68 | 69 | { 70 | h(VueApexCharts, { 71 | width: 230, 72 | type: 'pie', 73 | options: data.chartOptions, 74 | series: data.series 75 | }) 76 | } 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | ); 88 | } 89 | }); 90 | -------------------------------------------------------------------------------- /src/views/dashboard/console.tsx: -------------------------------------------------------------------------------- 1 | import { SpaceView } from '@/components/space-view'; 2 | import { NGi, NGrid, NNumberAnimation, NSpace, NStatistic } from 'naive-ui'; 3 | import { defineComponent } from 'vue'; 4 | import PieChart from './components/Pie'; 5 | import Linehart from './components/Line'; 6 | 7 | 8 | export default defineComponent({ 9 | name: 'Console', 10 | 11 | render () { 12 | return ( 13 |
16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
69 | ); 70 | } 71 | }); 72 | -------------------------------------------------------------------------------- /src/views/dashboard/workspace.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/views/data/analysis.tsx: -------------------------------------------------------------------------------- 1 | import { SpaceView } from '@/components/space-view'; 2 | import { defineComponent } from 'vue'; 3 | 4 | export default defineComponent({ 5 | name: 'DataAnalysis', 6 | 7 | setup () { 8 | 9 | }, 10 | 11 | render () { 12 | return ( 13 | 14 | 实时分析 15 | 16 | ); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/views/data/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { SpaceView } from '@/components/space-view'; 2 | import { defineComponent } from 'vue'; 3 | 4 | export default defineComponent({ 5 | name: 'DataIndex', 6 | 7 | setup () { 8 | 9 | }, 10 | 11 | render () { 12 | return ( 13 | 14 | 数据概况 15 | 16 | ); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/views/diy/style.scss: -------------------------------------------------------------------------------- 1 | @import "free-core/lib/style.css"; 2 | 3 | .free-action-render { 4 | padding: 16px; 5 | 6 | .action-item-label { 7 | font-size: 14px; 8 | } 9 | 10 | .help-text { 11 | font-size: 10px; 12 | } 13 | } 14 | 15 | .secondary-container { 16 | padding: 12px 16px; 17 | background-color: #f7f8fa; 18 | } 19 | -------------------------------------------------------------------------------- /src/views/diy/widgets.ts: -------------------------------------------------------------------------------- 1 | import NutuiGoodsCardWidget from '@/components/free-nutui/goods-card'; 2 | import NutuiImageAdWidget from '@/components/free-nutui/image-ad'; 3 | import NutuiImageNavWidget from '@/components/free-nutui/image-nav'; 4 | import NutuiNoticeBarWidget from '@/components/free-nutui/notice-bar'; 5 | import NutuiSearchWidget from '@/components/free-nutui/search-bar'; 6 | import NutuiVideoPlayerWidget from '@/components/free-nutui/video-player'; 7 | import { FreeFooterWidget, FreeHeaderWidget, FreePageWidget, FreeTitleTextWidget, FreeWhiteHeightWidget } from 'free-core'; 8 | import { CoreWidget, Widget } from 'free-core/lib/types/core/src/interface'; 9 | 10 | const widgets: { 11 | [key: string]: Widget | CoreWidget 12 | } = { 13 | 'free-page': FreePageWidget, 14 | 'free-footer': FreeFooterWidget, 15 | 'free-header': FreeHeaderWidget, 16 | 'white-height': FreeWhiteHeightWidget, 17 | 'title-text': FreeTitleTextWidget, 18 | 'search': NutuiSearchWidget, 19 | 'notice-bar': NutuiNoticeBarWidget, 20 | 'goods-card': NutuiGoodsCardWidget, 21 | 'image-ad': NutuiImageAdWidget, 22 | 'image-nav': NutuiImageNavWidget, 23 | 'video-player': NutuiVideoPlayerWidget, 24 | }; 25 | 26 | export default widgets; 27 | -------------------------------------------------------------------------------- /src/views/mall/goods/action.tsx: -------------------------------------------------------------------------------- 1 | import { computed, defineComponent, provide, ref } from 'vue'; 2 | import { SpaceView } from '@/components/space-view'; 3 | import BaseAction from './base.vue'; 4 | import { NButton, NLayoutFooter, NSpace } from 'naive-ui'; 5 | import { goodsDetail } from '@/api/mall/goods'; 6 | import { goodsActionInjectionKey } from './interface'; 7 | import { useRoute } from 'vue-router'; 8 | import { SpinView } from '@/components/spin-view'; 9 | 10 | import './style.scss'; 11 | 12 | export default defineComponent({ 13 | name: 'Goods', 14 | 15 | setup () { 16 | const route = useRoute(); 17 | const baseRef = ref(); 18 | // submit loading 19 | const loadingRef = ref(false); 20 | // global loading 21 | const showSpin = ref(false); 22 | const goodsModelRef = ref({}); 23 | 24 | const next = () => { 25 | const nextTab = parseInt(baseRef.value.tabRef) + 1; 26 | baseRef.value.tabRef = (nextTab > 1 ? 0 : nextTab).toString(); 27 | console.log(baseRef.value.tabRef); 28 | }; 29 | 30 | // 编辑 31 | const { 32 | params: { 33 | id 34 | } 35 | } = route; 36 | 37 | function getGoodsDetail () { 38 | return new Promise(async (resolve, reject) => { 39 | try { 40 | goodsModelRef.value = await goodsDetail(id as string); 41 | console.log(goodsModelRef.value); 42 | 43 | // 设置Type 44 | console.log('设置商品Type'); 45 | baseRef.value.handleTypeTrigger(goodsModelRef.value.type); 46 | 47 | resolve(true); 48 | } catch (error) { 49 | console.log(error); 50 | reject(error); 51 | } 52 | }); 53 | } 54 | 55 | const isAction = computed(() => { 56 | console.log(id); 57 | return !!id; 58 | }); 59 | 60 | async function submit () { 61 | loadingRef.value = true; 62 | try { 63 | await baseRef.value.submit(); 64 | } catch (error) { 65 | console.log(error); 66 | } 67 | loadingRef.value = false; 68 | } 69 | 70 | async function action () { 71 | showSpin.value = true; 72 | loadingRef.value = true; 73 | console.log('获取商品id:', id); 74 | 75 | await getGoodsDetail(); 76 | 77 | showSpin.value = false; 78 | loadingRef.value = false; 79 | } 80 | 81 | id && action(); 82 | 83 | provide(goodsActionInjectionKey, { 84 | goodsModelRef, 85 | isAction 86 | }); 87 | 88 | return { 89 | baseRef, 90 | submit, 91 | next, 92 | loading: loadingRef, 93 | showSpin 94 | }; 95 | }, 96 | 97 | render () { 98 | const { 99 | submit, 100 | next, 101 | loading 102 | } = this; 103 | 104 | return ( 105 |
106 | 107 | { this.showSpin && } 108 |
109 | 110 | 114 | 120 | 121 |
122 |
123 |
124 | ); 125 | } 126 | }); 127 | -------------------------------------------------------------------------------- /src/views/mall/goods/base.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 80 | -------------------------------------------------------------------------------- /src/views/mall/goods/components/card/index.ts: -------------------------------------------------------------------------------- 1 | import Card from './card.vue'; 2 | 3 | export { Card }; 4 | -------------------------------------------------------------------------------- /src/views/mall/goods/components/entity/entity.tsx: -------------------------------------------------------------------------------- 1 | import { createGoodsFormInjectionKey, FormRefs, FormRef } from './interface'; 2 | // import { watch } from 'vue'; 3 | import { FormInst, useMessage } from 'naive-ui'; 4 | import { defineComponent, onMounted, provide, ref } from 'vue'; 5 | import { FormBase, FormData, FormDelivery, FormOther } from './form'; 6 | import { create as goodsCreate, update as goodsUpdate } from '@/api/mall/goods'; 7 | 8 | import './style.scss'; 9 | import { useGoods } from '../../hooks/goods'; 10 | import { useRouter } from 'vue-router'; 11 | 12 | export default defineComponent({ 13 | name: 'GoodsEntity', 14 | 15 | setup () { 16 | const { 17 | isAction, 18 | goodsModel 19 | } = useGoods(); 20 | 21 | const formRefs = ref({}); 22 | 23 | const components = [ 24 | FormBase, FormData, FormDelivery, FormOther 25 | ]; 26 | 27 | const message = useMessage(); 28 | const router = useRouter(); 29 | 30 | // 子表单ref缓存 31 | function setForm (name: string, form: FormInst, model: () => any) { 32 | formRefs.value[name] = { 33 | form, 34 | model 35 | }; 36 | } 37 | 38 | // 验证子表单 39 | function promiseValidate (data: FormRef) { 40 | return new Promise((resolve, reject) => { 41 | data.form.validate(errors => { 42 | errors ? reject(errors) : resolve(data.model); 43 | }); 44 | }); 45 | } 46 | 47 | // 新建操作 48 | async function handleCreate (formModel: any) { 49 | try { 50 | await goodsCreate(formModel); 51 | message.success('新建成功'); 52 | router.replace('/mall/goods'); 53 | return Promise.resolve(); 54 | } catch (e) { 55 | console.log(e); 56 | message.error('新建失败'); 57 | return Promise.reject(e); 58 | } 59 | } 60 | 61 | // 编辑操作 62 | async function handleUpdate (formModel: any) { 63 | try { 64 | await goodsUpdate(goodsModel.value.id, formModel); 65 | message.success('更新成功'); 66 | router.replace('/mall/goods'); 67 | return Promise.resolve(); 68 | } catch (e) { 69 | console.log(e); 70 | message.error('更新失败'); 71 | return Promise.reject(e); 72 | } 73 | } 74 | 75 | // 提交操作 76 | async function submit (content = '') { 77 | try { 78 | const models = await Promise.all( 79 | Object.values(formRefs.value).map(data => promiseValidate(data)) 80 | ) as Array; 81 | 82 | let formModel: any = {}; 83 | models.map(value => formModel = Object.assign(formModel, value())); 84 | console.log(formModel); 85 | 86 | content && (formModel.content = content); 87 | 88 | // 更新 编辑 89 | isAction.value ? await handleUpdate(formModel) : await handleCreate(formModel); 90 | return Promise.resolve(); 91 | } catch (error) { 92 | return Promise.reject(error); 93 | } 94 | } 95 | 96 | provide(createGoodsFormInjectionKey, { 97 | setForm 98 | }); 99 | 100 | onMounted(() => { 101 | console.log(formRefs.value); 102 | }); 103 | 104 | return { 105 | components, 106 | submit 107 | }; 108 | }, 109 | 110 | render () { 111 | const { 112 | components 113 | } = this; 114 | 115 | return ( 116 |
117 | { 118 | components.map((component, index) => ) 119 | } 120 |
121 | ); 122 | } 123 | }); 124 | -------------------------------------------------------------------------------- /src/views/mall/goods/components/entity/form/index.ts: -------------------------------------------------------------------------------- 1 | import FormBase from './base'; 2 | import FormData from './data'; 3 | import FormDelivery from './delivery'; 4 | import FormOther from './other'; 5 | 6 | export { 7 | FormBase, 8 | FormData, 9 | FormDelivery, 10 | FormOther 11 | }; 12 | -------------------------------------------------------------------------------- /src/views/mall/goods/components/entity/index.ts: -------------------------------------------------------------------------------- 1 | import Entity from './entity'; 2 | 3 | export { Entity }; 4 | -------------------------------------------------------------------------------- /src/views/mall/goods/components/entity/interface.ts: -------------------------------------------------------------------------------- 1 | import { FormInst } from 'naive-ui'; 2 | import { createInjectionKey } from 'naive-ui/lib/_utils'; 3 | 4 | export interface GoodsData { 5 | name: string, 6 | description: string; 7 | group_ids: number[]; 8 | images: string[]; 9 | video: string; 10 | } 11 | 12 | export interface FormRef { 13 | form: FormInst; 14 | model: () => any 15 | } 16 | 17 | export interface FormRefs { 18 | [key: string]: FormRef; 19 | } 20 | 21 | interface GoodsFormInjection { 22 | setForm: (name: string, form: FormInst, model: unknown) => void; 23 | } 24 | 25 | export const createGoodsFormInjectionKey = createInjectionKey('upload-image-main'); 26 | -------------------------------------------------------------------------------- /src/views/mall/goods/components/entity/style.scss: -------------------------------------------------------------------------------- 1 | .goods-form { 2 | :deep(.form-short) { 3 | .form-item-blank { 4 | width: 160px; 5 | } 6 | } 7 | 8 | :deep(.form-mid) { 9 | .form-item-blank { 10 | width: 460px; 11 | } 12 | } 13 | 14 | :deep(.form-long) { 15 | .form-item-blank { 16 | width: 800px; 17 | } 18 | } 19 | 20 | // :deep(.n-form-item-label) { 21 | // font-size: 10px; 22 | // } 23 | 24 | :deep(.n-checkbox), 25 | :deep(.n-radio) { 26 | display: flex; 27 | align-items: center; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/views/mall/goods/components/interface.d.ts: -------------------------------------------------------------------------------- 1 | import { Component } from 'vue'; 2 | 3 | export type TypeKeys = 'entity' | 'virtual' | 'card'; 4 | 5 | export interface TypeSchema { 6 | title: string; 7 | delivery: boolean; 8 | name: Component; 9 | key: TypeKeys; 10 | } 11 | -------------------------------------------------------------------------------- /src/views/mall/goods/components/types.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, PropType, toRef, watch } from 'vue'; 2 | import { GoodsType } from '@/components/goods-type'; 3 | import { Entity } from './entity'; 4 | import { Virtual } from './virtual'; 5 | import { Card } from './card'; 6 | import { TypeKeys, TypeSchema } from './interface'; 7 | import { useGoods } from '../hooks/goods'; 8 | 9 | const goodsTypeProps = { 10 | typeKey: { 11 | type: String as PropType, 12 | default: 'entity' 13 | } 14 | }; 15 | 16 | export default defineComponent({ 17 | name: 'GoodsType', 18 | 19 | props: goodsTypeProps, 20 | 21 | emits: ['change'], 22 | 23 | setup (props, { emit }) { 24 | const { 25 | isAction 26 | } = useGoods(); 27 | const activeKey = toRef<{typeKey: TypeKeys}, 'typeKey'>(props, 'typeKey'); 28 | 29 | const types: TypeSchema[] = [ 30 | { 31 | title: '实物商品', 32 | delivery: true, 33 | key: 'entity', 34 | name: Entity 35 | }, 36 | { 37 | title: '虚拟商品', 38 | delivery: false, 39 | key: 'virtual', 40 | name: Virtual 41 | }, 42 | { 43 | title: '电子卡密', 44 | delivery: false, 45 | key: 'card', 46 | name: Card 47 | } 48 | ]; 49 | 50 | function getCurrent (key: TypeKeys) { 51 | return types.find(type => type.key === key); 52 | } 53 | 54 | const handleTrigger = (key: TypeKeys) => { 55 | if (activeKey.value === key || isAction.value) return; 56 | 57 | emit('change', getCurrent(key)); 58 | }; 59 | 60 | watch( 61 | () => activeKey.value, 62 | () => { 63 | emit('change', getCurrent(activeKey.value)); 64 | }, 65 | { 66 | immediate: true 67 | } 68 | ); 69 | 70 | return { 71 | types, 72 | activeKey, 73 | isAction, 74 | handleTrigger 75 | }; 76 | }, 77 | 78 | render () { 79 | const { 80 | types, 81 | handleTrigger 82 | } = this; 83 | 84 | return ( 85 |
93 | { 94 | types.map((item, index) => ( 95 | handleTrigger(item.key) 102 | }} 103 | /> 104 | )) 105 | } 106 |
107 | ); 108 | } 109 | }); 110 | -------------------------------------------------------------------------------- /src/views/mall/goods/components/virtual/index.ts: -------------------------------------------------------------------------------- 1 | import Virtual from './virtual.vue'; 2 | 3 | export { Virtual }; 4 | -------------------------------------------------------------------------------- /src/views/mall/goods/hooks/goods.ts: -------------------------------------------------------------------------------- 1 | import { computed, inject } from 'vue'; 2 | import { GoodsActionInjection, goodsActionInjectionKey } from '../interface'; 3 | 4 | export const useGoods = () => { 5 | const goodsMain = inject(goodsActionInjectionKey); 6 | 7 | const { 8 | goodsModelRef, 9 | isAction 10 | } = goodsMain as GoodsActionInjection; 11 | 12 | // 商品详情 13 | const goodsModel = computed(() => { 14 | return goodsModelRef.value; 15 | }); 16 | 17 | return { 18 | goodsModel, 19 | isAction 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/views/mall/goods/hooks/sku.ts: -------------------------------------------------------------------------------- 1 | import { create as createSpec, getLists as getSpecLists } from '@/api/mall/spec'; 2 | import { create as createValue, getLists as getValueLists } from '@/api/mall/value'; 3 | import { SkuBaseSchema, SkuSchema, SkuValueSchemas } from '@/components/sku/interface'; 4 | 5 | export const useSku = () => { 6 | function handleCreateSkuGroup (label: string): Promise { 7 | return new Promise(async (resolve, reject) => { 8 | try { 9 | const response = await createSpec(label); 10 | resolve({ 11 | ...response, 12 | leaf: [] 13 | }); 14 | } catch (error) { 15 | reject(error); 16 | } 17 | }); 18 | } 19 | 20 | function handleCreateSkuValues (labels: string[]): Promise { 21 | return new Promise(async (resolve, reject) => { 22 | try { 23 | const response = await createValue(labels); 24 | resolve(response); 25 | } catch (error) { 26 | reject(error); 27 | } 28 | }); 29 | } 30 | 31 | function handleGetGroups (): Promise { 32 | return new Promise(async (resolve, reject) => { 33 | try { 34 | const response = await getSpecLists(); 35 | 36 | resolve(response); 37 | } catch (error) { 38 | reject(error); 39 | } 40 | }); 41 | } 42 | 43 | function handleGetValues (): Promise { 44 | return new Promise(async (resolve, reject) => { 45 | try { 46 | const response = await getValueLists(); 47 | resolve(response); 48 | } catch (error) { 49 | reject(error); 50 | } 51 | }); 52 | } 53 | 54 | return { 55 | handleCreateSkuGroup, 56 | handleCreateSkuValues, 57 | handleGetGroups, 58 | handleGetValues 59 | }; 60 | }; 61 | -------------------------------------------------------------------------------- /src/views/mall/goods/interface.ts: -------------------------------------------------------------------------------- 1 | import { createInjectionKey } from 'naive-ui/lib/_utils'; 2 | import { Ref } from 'vue'; 3 | 4 | export interface GoodsActionInjection { 5 | goodsModelRef: Ref; 6 | isAction: Ref 7 | } 8 | 9 | export const goodsActionInjectionKey = createInjectionKey('goods-action'); 10 | 11 | export interface GoodsType { 12 | type: 'entity' | 'virtual' | 'card'; 13 | } 14 | 15 | export type DeliveryType = 'express' | 'pickup'; 16 | export type ExpressType = 'price' | 'template'; 17 | -------------------------------------------------------------------------------- /src/views/mall/goods/style.scss: -------------------------------------------------------------------------------- 1 | .goods-action { 2 | display: flex; 3 | flex-direction: column; 4 | padding-bottom: 60px; 5 | 6 | &__footer { 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | height: 60px; 11 | } 12 | 13 | .types-disabled { 14 | & > .goods-type { 15 | cursor: not-allowed; 16 | cursor: no-drop; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/views/mall/group/action.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 117 | 118 | 131 | -------------------------------------------------------------------------------- /src/views/mall/group/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 104 | 105 | 110 | -------------------------------------------------------------------------------- /src/views/mall/page/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import { NTab, NTabs } from 'naive-ui'; 2 | import { computed, defineComponent } from 'vue'; 3 | import { useRouter } from 'vue-router'; 4 | 5 | const mallTabProps = { 6 | defaultValue: { 7 | type: String 8 | } 9 | }; 10 | 11 | export default defineComponent({ 12 | name: 'MallTabs', 13 | 14 | props: mallTabProps, 15 | 16 | setup () { 17 | const router = useRouter(); 18 | const tabOptions = [ 19 | { 20 | name: 'page', 21 | title: '微页面', 22 | path: '/mall/page' 23 | }, 24 | { 25 | name: 'category', 26 | title: '微页面分类', 27 | path: '/mall/page/category' 28 | }, 29 | { 30 | name: 'draft', 31 | title: '微页面草稿', 32 | path: '/mall/page/draft' 33 | } 34 | ]; 35 | 36 | const pathMap = computed(() => { 37 | const maps = new Map(); 38 | tabOptions.map(item => maps.set(item.name, item.path)); 39 | 40 | return maps; 41 | }); 42 | 43 | function handleChangeRoute (value: string | number) { 44 | router.push({ 45 | path: pathMap.value.get(value) 46 | }); 47 | } 48 | 49 | return { 50 | tabOptions, 51 | handleChangeRoute 52 | }; 53 | }, 54 | 55 | render () { 56 | const { 57 | tabOptions, 58 | defaultValue, 59 | handleChangeRoute 60 | } = this; 61 | 62 | return ( 63 | 64 | {tabOptions.map(item => { 65 | return ( 66 | 67 | {item.title} 68 | 69 | ); 70 | })} 71 | 72 | ); 73 | } 74 | }); 75 | -------------------------------------------------------------------------------- /src/views/mall/page/category.tsx: -------------------------------------------------------------------------------- 1 | import { SpaceView } from '@/components/space-view'; 2 | import { DataTableColumn, MenuOption, NA, NButton, NCard, NDataTable, NDivider, NForm, NFormItem, NInput, NSpace } from 'naive-ui'; 3 | import { defineComponent, h } from 'vue'; 4 | import { RouterLink } from 'vue-router'; 5 | import StoreTabs from './Tabs'; 6 | 7 | export default defineComponent({ 8 | name: 'StoreCategory', 9 | 10 | setup () { 11 | const menuOptions: MenuOption[] = [ 12 | { 13 | label: '微页面', 14 | key: 'page', 15 | } 16 | ]; 17 | 18 | const columns: DataTableColumn[] = [ 19 | { 20 | type: 'selection' 21 | }, 22 | { 23 | title: '标题', 24 | key: 'title', 25 | }, 26 | { 27 | title: '发布状态', 28 | key: 'status', 29 | }, 30 | { 31 | title: '创建时间', 32 | key: 'created_at', 33 | }, 34 | { 35 | title: '商品数', 36 | key: 'count', 37 | }, 38 | { 39 | title: '访客数/浏览量', 40 | key: 'views', 41 | }, 42 | { 43 | title: '商品访客数/商品浏览量', 44 | key: 'goods_views', 45 | }, 46 | { 47 | title: '操作', 48 | key: 'action', 49 | width: '120px', 50 | render (row: any) { 51 | return h(NSpace, {}, { 52 | default: () => [ 53 | h(RouterLink, { 54 | to: `/mall/goods/${row.id}/edit` 55 | }, { 56 | default: () => h(NA, {}, { 57 | default: () => '编辑' 58 | }) 59 | }), 60 | h(NDivider, { 61 | vertical: true, 62 | style: { 63 | margin: 0 64 | } 65 | }), 66 | h(NA, {}, { 67 | default: () => '删除' 68 | }) 69 | ] 70 | }); 71 | } 72 | } 73 | ]; 74 | 75 | return { 76 | columns, 77 | menuOptions 78 | }; 79 | }, 80 | 81 | render () { 82 | const { 83 | columns 84 | } = this; 85 | 86 | return ( 87 | 88 | 89 | 90 | 91 | 新建微页面分类 92 | 93 | 94 | 95 | 96 | 97 | 98 | 101 | 102 | 103 | 104 | 108 | 筛选 109 | 110 | 111 | 重置筛选条件 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | ); 120 | } 121 | }); 122 | -------------------------------------------------------------------------------- /src/views/mall/page/draft.tsx: -------------------------------------------------------------------------------- 1 | import { RouterButton } from '@/components/router-button'; 2 | import { SpaceView } from '@/components/space-view'; 3 | import { DataTableColumn, MenuOption, NA, NButton, NCard, NDataTable, NDivider, NForm, NFormItem, NInput, NSelect, NSpace } from 'naive-ui'; 4 | import { defineComponent, h } from 'vue'; 5 | import { RouterLink } from 'vue-router'; 6 | import StoreTabs from './Tabs'; 7 | 8 | export default defineComponent({ 9 | name: 'StoreDraft', 10 | 11 | setup () { 12 | const menuOptions: MenuOption[] = [ 13 | { 14 | label: '微页面', 15 | key: 'page', 16 | } 17 | ]; 18 | 19 | const columns: DataTableColumn[] = [ 20 | { 21 | type: 'selection' 22 | }, 23 | { 24 | title: '标题', 25 | key: 'title', 26 | }, 27 | { 28 | title: '发布状态', 29 | key: 'status', 30 | }, 31 | { 32 | title: '创建时间', 33 | key: 'created_at', 34 | }, 35 | { 36 | title: '商品数', 37 | key: 'count', 38 | }, 39 | { 40 | title: '访客数/浏览量', 41 | key: 'views', 42 | }, 43 | { 44 | title: '商品访客数/商品浏览量', 45 | key: 'goods_views', 46 | }, 47 | { 48 | title: '操作', 49 | key: 'action', 50 | width: '120px', 51 | render (row: any) { 52 | return h(NSpace, {}, { 53 | default: () => [ 54 | h(RouterLink, { 55 | to: `/mall/goods/${row.id}/edit` 56 | }, { 57 | default: () => h(NA, {}, { 58 | default: () => '编辑' 59 | }) 60 | }), 61 | h(NDivider, { 62 | vertical: true, 63 | style: { 64 | margin: 0 65 | } 66 | }), 67 | h(NA, {}, { 68 | default: () => '删除' 69 | }) 70 | ] 71 | }); 72 | } 73 | } 74 | ]; 75 | 76 | return { 77 | columns, 78 | menuOptions 79 | }; 80 | }, 81 | 82 | render () { 83 | const { 84 | columns 85 | } = this; 86 | 87 | return ( 88 | 89 | 90 | 91 | 92 | 93 | 新建微页面 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 104 | 105 | 106 | 107 | 110 | 111 | 112 | 113 | 114 | 118 | 筛选 119 | 120 | 121 | 重置筛选条件 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | ); 130 | } 131 | }); 132 | -------------------------------------------------------------------------------- /src/views/member/action/index.tsx: -------------------------------------------------------------------------------- 1 | import { SpaceView } from '@/components/space-view'; 2 | import { defineComponent } from 'vue'; 3 | 4 | export default defineComponent({ 5 | name: 'MemberAction', 6 | 7 | setup () { 8 | 9 | }, 10 | 11 | render () { 12 | return ( 13 | 14 | 客户Action 15 | 16 | ); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/views/member/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { SpaceView } from '@/components/space-view'; 2 | import { defineComponent } from 'vue'; 3 | 4 | export default defineComponent({ 5 | name: 'MemberDashboard', 6 | 7 | setup () { 8 | 9 | }, 10 | 11 | render () { 12 | return ( 13 | 14 | 客户概况 15 | 16 | ); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/views/member/index.tsx: -------------------------------------------------------------------------------- 1 | import { FormItem } from '@/components/naive-ui/form'; 2 | import { RouterButton } from '@/components/router-button'; 3 | import { SpaceView } from '@/components/space-view'; 4 | import { DataTableColumn, NA, NButton, NCard, NDataTable, NDatePicker, NDivider, NForm, NInput, NSpace } from 'naive-ui'; 5 | import { defineComponent, h } from 'vue'; 6 | import { RouterLink } from 'vue-router'; 7 | 8 | export default defineComponent({ 9 | name: 'MemberIndex', 10 | 11 | setup () { 12 | const columns: DataTableColumn[] = [ 13 | { 14 | type: 'selection' 15 | }, 16 | { 17 | title: '标题', 18 | key: 'title', 19 | }, 20 | { 21 | title: '发布状态', 22 | key: 'status', 23 | }, 24 | { 25 | title: '创建时间', 26 | key: 'created_at', 27 | }, 28 | { 29 | title: '商品数', 30 | key: 'count', 31 | }, 32 | { 33 | title: '访客数/浏览量', 34 | key: 'views', 35 | }, 36 | { 37 | title: '商品访客数/商品浏览量', 38 | key: 'goods_views', 39 | }, 40 | { 41 | title: '操作', 42 | key: 'action', 43 | width: '120px', 44 | render (row: any) { 45 | return h(NSpace, {}, { 46 | default: () => [ 47 | h(RouterLink, { 48 | to: `/mall/goods/${row.id}/edit` 49 | }, { 50 | default: () => h(NA, {}, { 51 | default: () => '编辑' 52 | }) 53 | }), 54 | h(NDivider, { 55 | vertical: true, 56 | style: { 57 | margin: 0 58 | } 59 | }), 60 | h(NA, {}, { 61 | default: () => '删除' 62 | }) 63 | ] 64 | }); 65 | } 66 | } 67 | ]; 68 | 69 | return { 70 | columns 71 | }; 72 | }, 73 | 74 | render () { 75 | const { 76 | columns 77 | } = this; 78 | 79 | return ( 80 | 81 | 82 | 83 | 84 | 添加客户 85 | 86 | 87 | 91 | 95 | 99 | 104 | 105 | 106 | 107 | 108 | 113 | 114 | 115 | 116 | 117 | 122 | 123 | 124 | 125 | 126 | 131 | 132 | 133 | 134 | 138 | 筛选 139 | 140 | 141 | 导出 142 | 143 | 查看已导出列表 144 | 重置筛选条件 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | ); 156 | } 157 | }); 158 | -------------------------------------------------------------------------------- /src/views/resource/index.tsx: -------------------------------------------------------------------------------- 1 | import { SpaceView } from '@/components/space-view'; 2 | import { defineComponent } from 'vue'; 3 | 4 | export default defineComponent({ 5 | name: 'ResourceIndex', 6 | 7 | setup () { 8 | 9 | }, 10 | 11 | render () { 12 | return ( 13 | 14 | 素材中心 15 | 16 | ); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/views/settings/base/components/card.tsx: -------------------------------------------------------------------------------- 1 | import { computed, defineComponent } from 'vue'; 2 | import { cB, commonLight, NCard, NText } from 'naive-ui'; 3 | import { createTheme, useTheme } from 'naive-ui/lib/_mixins'; 4 | 5 | export default defineComponent({ 6 | name: 'FormCard', 7 | 8 | props: { 9 | title: { 10 | type: String, 11 | required: true 12 | } 13 | }, 14 | 15 | setup () { 16 | const themeRef = useTheme( 17 | 'Card', 18 | '-form-card', 19 | cB('card', ''), 20 | createTheme({ 21 | name: 'FormCard', 22 | common: commonLight, 23 | }), 24 | {}, 25 | ); 26 | 27 | const cssVarsRef = computed(() => { 28 | const { 29 | common: { 30 | primaryColor, 31 | } 32 | } = themeRef.value; 33 | return { 34 | '--primary-color': primaryColor, 35 | }; 36 | }); 37 | 38 | return { 39 | cssVarsRef 40 | }; 41 | }, 42 | 43 | render () { 44 | const { 45 | title, 46 | $slots, 47 | cssVarsRef 48 | } = this; 49 | 50 | return ( 51 | 52 | {{ 53 | header: () => { 54 | return ( 55 | 56 | {{ 57 | header: () => { 58 | return {title}; 59 | }, 60 | }} 61 | 62 | ); 63 | }, 64 | default: $slots.default 65 | }} 66 | 67 | ); 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /src/views/settings/base/components/payment.tsx: -------------------------------------------------------------------------------- 1 | import { FormItem } from '@/components/naive-ui/form'; 2 | import { NForm, NRadio, NRadioGroup, NSpace } from 'naive-ui'; 3 | import { defineComponent } from 'vue'; 4 | import FormCard from './card'; 5 | 6 | export default defineComponent({ 7 | name: 'PaymentForm', 8 | 9 | setup () { 10 | // 11 | }, 12 | 13 | render () { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 开启 22 | 关闭 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /src/views/settings/base/components/section.tsx: -------------------------------------------------------------------------------- 1 | import { NSpace, NText } from 'naive-ui'; 2 | import { defineComponent } from 'vue'; 3 | 4 | export default defineComponent({ 5 | name: 'FormSection', 6 | 7 | props: { 8 | title: { 9 | type: String, 10 | required: true 11 | }, 12 | description: { 13 | type: String, 14 | default: '' 15 | } 16 | }, 17 | 18 | setup () { 19 | // 20 | }, 21 | 22 | render () { 23 | const { 24 | title, 25 | description 26 | } = this; 27 | return ( 28 | 29 |
30 | {title} 31 |
32 | {description} 33 |
34 | ); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /src/views/settings/base/index.tsx: -------------------------------------------------------------------------------- 1 | import { SpaceView } from '@/components/space-view'; 2 | import { defineComponent, ref } from 'vue'; 3 | import GoodsForm from './components/goods'; 4 | import StoreForm from './components/store'; 5 | import PaymentForm from './components/payment'; 6 | import OrderForm from './components/order'; 7 | import { NButton, NLayoutFooter, NSpace } from 'naive-ui'; 8 | 9 | import $style from './style.module.scss'; 10 | 11 | interface FormRefs { 12 | [key: string]: any; 13 | } 14 | 15 | export default defineComponent({ 16 | name: 'SettingIndex', 17 | 18 | setup () { 19 | const formRefs = ref({}); 20 | const formComponents = [ 21 | StoreForm, 22 | GoodsForm, 23 | OrderForm, 24 | PaymentForm, 25 | ]; 26 | 27 | function handleSetFormRefs (key: string, formRef: any) { 28 | formRefs.value[key] = formRef; 29 | } 30 | 31 | return { 32 | formComponents, 33 | handleSetFormRefs 34 | }; 35 | }, 36 | 37 | render () { 38 | const { 39 | formComponents, 40 | handleSetFormRefs 41 | } = this; 42 | return ( 43 |
44 | 45 | 46 | {formComponents.map(component => { 47 | return handleSetFormRefs(component.name, e)} />; 48 | })} 49 | 50 | 54 | 59 | 60 | 61 |
62 | ); 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /src/views/settings/base/style.module.scss: -------------------------------------------------------------------------------- 1 | .setting-form { 2 | padding-bottom: 60px; 3 | 4 | :global { 5 | .setting-footer { 6 | height: 60px; 7 | } 8 | 9 | .store-section { 10 | &__title { 11 | width: 130px; 12 | text-align: right; 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/views/settings/contact.tsx: -------------------------------------------------------------------------------- 1 | import { SpaceView } from '@/components/space-view'; 2 | import { NButton, NCard, NCascader, NForm, NFormItem, NInput, NInputNumber, NRadio, NRadioGroup, NSpace } from 'naive-ui'; 3 | import { defineComponent } from 'vue'; 4 | 5 | export default defineComponent({ 6 | name: 'SettingIndex', 7 | 8 | setup () { 9 | 10 | }, 11 | 12 | render () { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 座机号 21 | 手机号 22 | 23 | 24 | 25 | 26 | - 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 搜索地图 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /src/views/settings/info.tsx: -------------------------------------------------------------------------------- 1 | import { SpaceView } from '@/components/space-view'; 2 | import { defineComponent } from 'vue'; 3 | 4 | export default defineComponent({ 5 | name: 'SettingInfo', 6 | 7 | setup () { 8 | 9 | }, 10 | 11 | render () { 12 | return ( 13 | 14 | 店铺信息 15 | 16 | ); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /src/views/settings/refund.tsx: -------------------------------------------------------------------------------- 1 | import { FormItem } from '@/components/naive-ui/form'; 2 | import { SpaceView } from '@/components/space-view'; 3 | import { NForm, NInputNumber, NText } from 'naive-ui'; 4 | import { defineComponent } from 'vue'; 5 | import FormCard from './base/components/card'; 6 | 7 | export default defineComponent({ 8 | name: 'SettingIndex', 9 | 10 | setup () { 11 | 12 | }, 13 | 14 | render () { 15 | return ( 16 | 17 | 18 | 19 | 20 | {{ 21 | default: () => { 22 | return ; 23 | }, 24 | help: () => { 25 | return 手机号可用于接收买家维权咨询、维权通知提醒,如不填写,将收不到消息提醒; 26 | } 27 | }} 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /src/views/system/auth/index.tsx: -------------------------------------------------------------------------------- 1 | import { RouterButton } from '@/components/router-button'; 2 | import { SpaceView } from '@/components/space-view'; 3 | import { DataTableColumns, NButton, NCard, NDataTable, NSpace } from 'naive-ui'; 4 | import { defineComponent, h } from 'vue'; 5 | 6 | export default defineComponent({ 7 | name: 'AuthRole', 8 | 9 | setup () { 10 | const tableColumns: DataTableColumns = [ 11 | { 12 | title: '角色', 13 | key: 'name' 14 | }, 15 | { 16 | title: '标识', 17 | key: 'slug' 18 | }, 19 | { 20 | title: '描述', 21 | key: 'description' 22 | }, 23 | { 24 | title: '操作', 25 | key: 'actions', 26 | render () { 27 | return h( 28 | NButton, 29 | { 30 | text: true, 31 | }, 32 | { default: () => '查看' } 33 | ); 34 | } 35 | } 36 | ]; 37 | 38 | return { 39 | tableColumns 40 | }; 41 | }, 42 | 43 | render () { 44 | const { 45 | tableColumns 46 | } = this; 47 | 48 | return ( 49 | 50 | 51 | 52 | 53 | 新建角色 54 | 55 | 56 | 57 | 58 | 59 | 60 | ); 61 | } 62 | }); 63 | -------------------------------------------------------------------------------- /src/views/system/auth/role/action.tsx: -------------------------------------------------------------------------------- 1 | import { SpaceView } from '@/components/space-view'; 2 | import { NCard } from 'naive-ui'; 3 | import { defineComponent} from 'vue'; 4 | 5 | export default defineComponent({ 6 | name: 'AuthRole', 7 | 8 | setup () { 9 | // 10 | }, 11 | 12 | render () { 13 | return ( 14 | 15 | 16 | Action 17 | 18 | 19 | ); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /src/views/system/auth/sigin.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 115 | 116 | 128 | -------------------------------------------------------------------------------- /src/views/system/settings/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'jit', 3 | important: true, 4 | content: [ 5 | './index.html', 6 | './src/**/*.{vue,js,ts,jsx,tsx}' 7 | ], 8 | theme: { 9 | extend: {} 10 | }, 11 | plugins: [] 12 | }; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": false, 12 | "esModuleInterop": true, 13 | "lib": ["esnext", "dom"], 14 | "paths": { 15 | "@/*":[ 16 | "./src/*" 17 | ] 18 | } 19 | }, 20 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 21 | "references": [{ "path": "./tsconfig.node.json" }] 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, PluginOption } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | import vueJsx from '@vitejs/plugin-vue-jsx'; 4 | import Components from 'unplugin-vue-components/vite'; 5 | import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'; 6 | import { createStyleImportPlugin, NutuiResolve } from 'vite-plugin-style-import'; 7 | import { resolve } from 'path'; 8 | 9 | const plugins: PluginOption[] = [ 10 | vue(), 11 | vueJsx(), 12 | Components({ 13 | dts: false, 14 | resolvers: [ 15 | NaiveUiResolver() 16 | ] 17 | }), 18 | createStyleImportPlugin({ 19 | resolves: [ 20 | NutuiResolve(), 21 | ] 22 | }), 23 | ]; 24 | 25 | // https://vitejs.dev/config/ 26 | export default defineConfig({ 27 | plugins, 28 | resolve: { 29 | alias: { 30 | '@': resolve(__dirname, 'src') 31 | } 32 | }, 33 | css: { 34 | preprocessorOptions: { 35 | scss: { 36 | // 配置 nutui 全局 scss 变量 37 | additionalData: '@import "@/styles/nutui/variables.scss";' 38 | } 39 | } 40 | } 41 | }); 42 | --------------------------------------------------------------------------------