├── .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 |
2 |
8 |
9 |
10 |
11 |
12 |
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 |
2 |
11 |
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 |
7 |
--------------------------------------------------------------------------------
/src/components/free-nutui/image-ad/assets/carousel-dot.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/src/components/free-nutui/image-ad/assets/carousel-rectangle.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/src/components/free-nutui/image-ad/assets/carousel-small.svg:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/src/components/free-nutui/image-ad/assets/carousel.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
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 |
2 |
5 | {{ title }}
6 | ({{ deliveryText }})
7 |
8 |
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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 | {{ title }}
5 |
6 |
7 |
8 |
9 |
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 |
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 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
17 |
21 |
22 |
23 | {{ item.meta.title }}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
35 |
36 | Eamesh Mall · Made by Easeava
37 |
38 |
39 |
40 |
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 |
2 |
3 |
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 |
2 |
13 |
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 |
2 |
14 |
21 |
22 |
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 |
2 |
3 |
7 |
11 | 新建内容分类
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
67 |
--------------------------------------------------------------------------------
/src/views/article/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
11 | 新建内容
12 |
13 |
14 |
15 |
16 |
17 |
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 |
2 | workspace
3 |
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 |
2 |
5 |
11 |
16 |
17 |
22 |
23 |
27 |
28 |
33 |
34 |
35 |
42 |
43 |
44 |
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 |
2 |
3 |
4 |
13 |
17 |
18 |
19 |
23 |
24 |
25 |
28 |
33 | 提交
34 |
35 |
36 | 返回
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
117 |
118 |
131 |
--------------------------------------------------------------------------------
/src/views/mall/group/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
13 | 新建商品分组
14 |
15 |
16 |
17 |
25 |
26 |
27 |
28 |
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 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
17 |
20 |
25 |
26 |
29 |
35 |
36 |
37 |
44 | 登录
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
56 | @
57 |
58 |
59 |
60 |
61 |
62 |
115 |
116 |
128 |
--------------------------------------------------------------------------------
/src/views/system/settings/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | 系统设置
4 |
5 |
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 |
--------------------------------------------------------------------------------