Compare commits

...

36 Commits
v0.9.6 ... main

Author SHA1 Message Date
chansee97
a5c3697a6b chore: bump version to 0.9.12 and update dependencies 2025-04-05 17:43:46 +08:00
chansee97
d97306d5b5 chore: bump version to 0.9.11 2025-03-27 22:08:06 +08:00
chenxin
99840ed604 style: add cursor pointer style to user avatar 2025-03-14 14:52:43 +08:00
chansee97
75dd7b0c83 chore: update dependencies to latest versions 2025-03-05 21:28:54 +08:00
chansee97
2f2d8726d4 Merge branch 'main' of https://github.com/chansee97/nova-admin 2025-01-20 22:47:46 +08:00
chansee97
30f0ac0904 feat: remove support single role permission 2025-01-20 22:47:43 +08:00
Rock Chen
5cb0ca39dd feat: add draggable tab bar items and refactor tab rendering 2025-01-16 13:12:24 +00:00
chansee97
e5222bbbc6 docs: update WeChat group information and clarify adding purpose 2025-01-08 17:07:52 +08:00
chansee97
52614819e8 chore: update version to 0.9.10 2025-01-06 11:50:26 +08:00
chansee97
4f31476cf8 chore: updata deps 2025-01-05 12:35:41 +08:00
chansee97
85b0826eea feat: file collpse 2024-12-24 10:59:19 +08:00
chansee97
5975d05437 fix: copy i18n 2024-12-24 10:45:44 +08:00
AnsonCar
aa9aece0ca
feat: Add docker support to enhance project scalability and deployment efficiency (#43)
* docs: update readme Install and use

* feat: add docker

* feat: add nginx.conf

* fix: change Traditional Chinese to Simplified Chinese
2024-11-23 20:47:56 +08:00
chansee97
a487141cfb chore: updata deps 2024-11-06 22:56:31 +08:00
chansee97
e7148081a9 modify: support multi tabs 2024-10-08 12:37:27 +08:00
chansee97
308e0b4bf9 fix: rich editor error type 2024-09-26 22:44:44 +08:00
Ray.D.Song
367406258c
fix: Sass legacy JS API warn (#39)
* feat: draggable list & useTableDrag hook

* fix: sass deprecated legacy JS API
2024-09-25 12:33:57 +08:00
Ray.D.Song
aee3e52f15
feat: draggable list & useTableDrag hook (#38) 2024-09-24 11:23:52 +08:00
chansee97
89d78b7ec7 fix: useEcharts updata error 2024-09-22 23:23:15 +08:00
chansee97
242c94723b fix: dict function error 2024-09-19 00:50:10 +08:00
chansee97
dc3563969b fix: import error 2024-09-16 00:03:06 +08:00
chansee97
7da454563b fix: import order 2024-09-14 08:09:20 +08:00
chenxin
e450a029ac feat: adapt vue3.5 feat 2024-09-05 17:05:55 +08:00
chenxin
c14ebc2343 refactor: organize file dir 2024-09-05 16:02:24 +08:00
chansee97
5e88b8d01f fix: map name error 2024-08-31 00:35:39 +08:00
chansee97
5fb8881763 fix: modify echart bg color 2024-08-24 00:43:31 +08:00
chansee97
efc1ccbc9a fix: remove DemoList type 2024-08-21 00:21:57 +08:00
chansee97
f20d2a5fb6 fix:typo 2024-08-21 00:07:17 +08:00
chansee97
fad054e8df fix: chart & qrcode demo error 2024-08-19 08:26:37 +08:00
chansee97
df0cf9f72b chore: update deps 2024-08-14 22:33:01 +08:00
chansee97
d69bd796a4 refactor: env var 2024-08-09 00:49:55 +08:00
TianQian
a9c7708119
fix: 为 createServerTokenAuthentication 添加 statesHook 类型 (#32) 2024-08-08 23:48:09 +08:00
chansee97
de4cd17548 chore: update alova version 2024-08-03 07:02:28 +08:00
chansee97
70c43a276c fix: type error 2024-07-29 22:31:48 +08:00
chansee97
5cc410c7b4 fix: type error & local icon error 2024-07-25 22:40:12 +08:00
chansee97
5c24fa1502 fix: readme error mock url 2024-07-20 01:04:51 +08:00
103 changed files with 1035 additions and 624 deletions

8
.dockerignore Normal file
View File

@ -0,0 +1,8 @@
/node_modules
/.git
/.gitignore
/.vscode
/.DS_Store
/*.md
/dist

22
.env
View File

@ -1,20 +1,26 @@
# 项目根目录
VITE_BASE_URL=/
VITE_BASE_URL = /
# 项目名称
VITE_APP_NAME=Nova - Admin
# 路由模式
VITE_APP_NAME = Nova - Admin
# 路由模式 web hash
VITE_ROUTE_MODE = web
# 权限路由模式: static dynamic
VITE_AUTH_ROUTE_MODE=dynamic
# 路由加载模式 static dynamic
VITE_ROUTE_LOAD_MODE = static
# 设置登陆后跳转地址
VITE_HOME_PATH = /dashboard/workbench
# 本地存储前缀
VITE_STORAGE_PREFIX=
VITE_STORAGE_PREFIX =
# 版权信息
VITE_COPYRIGHT_INFO= Copyright © 2024 chansee97
VITE_COPYRIGHT_INFO = Copyright © 2024 chansee97
# 自动刷新token
VITE_AUTO_REFRESH_TOKEN=Y
VITE_AUTO_REFRESH_TOKEN = Y
# 默认多语言 enUS | zhCN
VITE_DEFAULT_LANG = enUS

1
.github/FUNDING.yml vendored
View File

@ -1 +0,0 @@
custom: ['https://cdn.jsdelivr.net/gh/chansee97/static/sponsor-wechat.png', 'https://cdn.jsdelivr.net/gh/chansee97/static/sponsor-alipay.png']

14
.vscode/settings.json vendored
View File

@ -50,5 +50,17 @@
"i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": [
"locales"
]
],
// File collapse
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
"*.ts": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx, $(capture).d.ts",
"*.tsx": "$(capture).test.ts, $(capture).test.tsx, $(capture).spec.ts, $(capture).spec.tsx,$(capture).d.ts",
"*.env": "$(capture).env.*",
"README.md": "README*,CHANGELOG*,LICENSE,CNAME",
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
"eslint.config.js": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json",
"docker-compose.product.yml": ".dockerignore,nginx.conf"
}
}

View File

@ -51,7 +51,7 @@
## Interface document
This project uses ApiFox for interface mock, check the online documentation for more interface details
[online aipfox docs](https://apifox.com/apidoc/shared-2b1abeb5-6e78-425e-a4ff-d7277ae83bf0)
[online aipfox docs](https://nova-admin.apifox.cn)
## Install and use
@ -71,6 +71,13 @@ pnpm build
```
You can deploy **nova-admin** in a production environment using docker-compose.
```bash
# Build product
docker compose -f docker-compose.product.yml up --build -d
```
> The nginx.conf provided is for reference only. You can adjust it according to your own needs.
## Related projects
- [Nova-admin-nest](https://github.com/chansee97/nove-admin-nest) (under development) Nova-Admin supporting background project based on TS, NestJs, typeorm
@ -79,11 +86,11 @@ pnpm build
Nova-Admin is a completely open-source and free project. It is still being optimized and iterated. It is designed to help developers more conveniently develop medium and large management systems. If you have any questions, please ask questions in the QQ exchange group.
| Q-Group | wechat-Group |wechat |
| :--: |:--: |:--: |
| <img src="https://cdn.jsdelivr.net/gh/chansee97/static/nova-admin/q-group.png" width=170> | <img src="https://cdn.jsdelivr.net/gh/chansee97/static/nova-admin/wx-group.png" width=170>|<img src="https://cdn.jsdelivr.net/gh/chansee97/static/wechat.png" width=170>|
| Q-Group | wechat-Group |
| :--: |:--: |
| <img src="https://cdn.jsdelivr.net/gh/chansee97/static/nova-admin/q-group.png" width=170> |<img src="https://cdn.jsdelivr.net/gh/chansee97/static/wechat.png" width=170>|
> The WeChat group QR code is invalid, please add me as a friend.
> Please indicate the purpose of adding WeChat.
## Contribution

View File

@ -51,7 +51,7 @@
## 接口文档
本项目使用ApiFox进行接口mock查看在线文档以了解更多接口详情
[在线apifox文档](https://apifox.com/apidoc/shared-2b1abeb5-6e78-425e-a4ff-d7277ae83bf0)
[在线apifox文档](https://nova-admin.apifox.cn)
## 安装使用
@ -71,6 +71,13 @@ pnpm build
```
在生产环境也可以使用 docker-compose 部署 **nova-admin**
```bash
# Build product
docker compose -f docker-compose.product.yml up --build -d
```
> 关于 nginx.conf 只供参考,你可以根据自己的需求进行调整。
## 相关项目
- [Nova-admin-nest](https://github.com/chansee97/nove-admin-nest) (开发中)基于TS, NestJs, typeorm的Nova-Admin配套后台项目
@ -79,11 +86,11 @@ pnpm build
Nova-Admin 是完全开源免费的项目,目前仍然在优化迭代中,旨在帮助开发者更方便地进行中大型管理系统开发,有使用问题欢迎在交流群内提问。
| Q群 | 微信群 | 个人微信 |
| :--: |:--: |:--: |
| <img src="https://cdn.jsdelivr.net/gh/chansee97/static/nova-admin/q-group.png" width=170> | <img src="https://cdn.jsdelivr.net/gh/chansee97/static/nova-admin/wx-group.png" width=170>|<img src="https://cdn.jsdelivr.net/gh/chansee97/static/wechat.png" width=170>|
| Q群 | 微信群 |
| :--: |:--: |
| <img src="https://cdn.jsdelivr.net/gh/chansee97/static/nova-admin/q-group.png" width=170> |<img src="https://cdn.jsdelivr.net/gh/chansee97/static/wechat.png" width=170>|
> 微信群二维码失效请加我为好友
> 添加微信请注明来意
## 贡献

View File

@ -1,16 +1,16 @@
import UnoCSS from '@unocss/vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import VueDevTools from 'vite-plugin-vue-devtools'
import Components from 'unplugin-vue-components/vite'
import AutoImport from 'unplugin-auto-import/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
import viteCompression from 'vite-plugin-compression'
import Icons from 'unplugin-icons/vite'
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
// https://github.com/antfu/unplugin-icons
import IconsResolver from 'unplugin-icons/resolver'
import Icons from 'unplugin-icons/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
import Components from 'unplugin-vue-components/vite'
import viteCompression from 'vite-plugin-compression'
import VueDevTools from 'vite-plugin-vue-devtools'
/**
* @description: vite插件配置

View File

@ -0,0 +1,8 @@
services:
nove-admin:
build:
context: .
dockerfile: ./docker/dockerfile.product
container_name: nove-admin
ports:
- 80:80

23
docker/dockerfile.product Normal file
View File

@ -0,0 +1,23 @@
FROM node:20-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
COPY . /app
WORKDIR /app
FROM base AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
FROM base AS builder
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm run build
FROM nginx:1.23.1-alpine
WORKDIR /www
COPY --from=builder /app/dist/ .
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

View File

@ -6,8 +6,17 @@ export default antfu(
{
typescript: {
overrides: {
'perfectionist/sort-exports': 'off',
'perfectionist/sort-imports': 'off',
'ts/no-unused-expressions': ['error', { allowShortCircuit: true }],
},
},
vue: {
overrides: {
'vue/no-unused-refs': 'off', // 暂时关闭等待vue-lint的分支合并
'vue/no-reserved-component-names': 'off',
'vue/component-definition-name-casing': 'off',
},
},
},
)

View File

@ -14,7 +14,7 @@
"loginOutContent": "Confirm to log out of current account?",
"loginOutTitle": "Sign out",
"userCenter": "Personal center",
"lignt": "Light",
"light": "Light",
"dark": "Dark",
"system": "System",
"backTop": "Back to top",
@ -89,17 +89,18 @@
"route": {
"appRoot": "Home",
"cardList": "Card list",
"draggableList": "Draggable list",
"commonList": "Common list",
"dashboard": "Dashboard",
"demo": "Function example",
"fetch": "Request example",
"list": "List",
"monitor": "Monitoring",
"test": "Multi-level menu",
"test2": "Multi-level menu subpage",
"test2Detail": "Details page of multi-level menu",
"test3": "multi-level menu",
"test4": "Multi-level menu 3-1",
"multi": "Multi-level menu",
"multi2": "Multi-level menu subpage",
"multi2Detail": "Details page of multi-level menu",
"multi3": "multi-level menu",
"multi4": "Multi-level menu 3-1",
"workbench": "Workbench",
"QRCode": "QR code",
"about": "About",
@ -112,6 +113,7 @@
"documentsVite": "Vite",
"documentsVue": "Vue",
"documentsVueuse": "VueUse (external link)",
"documentsNova": "Nova docs",
"echarts": "Echarts",
"editor": "Editor",
"editorMd": "MarkDown editor",
@ -153,7 +155,9 @@
},
"copyText": {
"message": "Copied successfully",
"tooltip": "Copy"
"tooltip": "Copy",
"unsupportedError": "Your browser does not support Clipboard API",
"unpermittedError": "Crrently not permitted to use Clipboard API"
}
}
}

View File

@ -14,7 +14,7 @@
"loginOutTitle": "退出登录",
"loginOutContent": "确认退出当前账号?",
"userCenter": "个人中心",
"lignt": "浅色",
"light": "浅色",
"dark": "深色",
"system": "跟随系统",
"backTop": "返回顶部",
@ -87,7 +87,9 @@
},
"copyText": {
"tooltip": "复制",
"message": "复制成功"
"message": "复制成功",
"unsupportedError": "您的浏览器不支持剪贴板API",
"unpermittedError": "目前不允许使用剪贴板API"
}
},
"login": {
@ -118,14 +120,15 @@
"dashboard": "仪表盘",
"workbench": "工作台",
"monitor": "监控页",
"test": "多级菜单演示",
"test2": "多级菜单子页",
"test2Detail": "多级菜单的详情页",
"test3": "多级菜单",
"test4": "多级菜单3-1",
"multi": "多级菜单演示",
"multi2": "多级菜单子页",
"multi2Detail": "多级菜单的详情页",
"multi3": "多级菜单",
"multi4": "多级菜单3-1",
"list": "列表页",
"commonList": "常用列表",
"cardList": "卡片列表",
"draggableList": "拖拽列表",
"demo": "功能示例",
"fetch": "请求示例",
"echarts": "Echarts示例",
@ -140,6 +143,7 @@
"documentsVue": "Vue",
"documentsVite": "Vite",
"documentsVueuse": "VueUse外链",
"documentsNova": "Nova 文档",
"permission": "权限",
"permissionDemo": "权限示例",
"justSuper": "super可见",

66
nginx.conf Normal file
View File

@ -0,0 +1,66 @@
server {
listen 80;
listen [::]:80;
# 启用 gzip 压缩
gzip on;
gzip_vary on;
gzip_min_length 10240;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/javascript;
gzip_disable "MSIE [1-6]\.";
# 设定 MIME types
include /etc/nginx/mime.types;
# 基本安全设定
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
# 增加伺服器效能的配置
client_max_body_size 100M;
client_body_buffer_size 128k;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
location / {
root /www;
index index.html;
try_files $uri $uri/ /index.html;
# 设定快取控制
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
# 动态内容不快取
location = /index.html {
add_header Cache-Control "no-store, no-cache, must-revalidate";
add_header Pragma "no-cache";
expires -1;
}
# 错误处理
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_intercept_errors on;
# 基本的代理设定
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 禁止访问隐藏文件
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}

View File

@ -1,7 +1,7 @@
{
"name": "nova-admin",
"type": "module",
"version": "0.9.6",
"version": "0.9.12",
"private": true,
"description": "a clean and concise back-end management template based on Vue3, Vite5, Typescript, and Naive UI.",
"author": {
@ -40,52 +40,52 @@
"dev": "vite --mode dev --port 9980",
"dev:test": "vite --mode test",
"dev:prod": "vite --mode prod",
"build": "vue-tsc --noEmit && vite build --mode prod",
"build:dev": "vue-tsc --noEmit && vite build --mode dev",
"build:test": "vue-tsc --noEmit && vite build --mode test",
"build": "vite build --mode prod",
"build:dev": "vite build --mode dev",
"build:test": "vite build --mode test",
"preview": "vite preview --port 9981",
"lint": "eslint .",
"lint": "eslint . && vue-tsc --noEmit",
"lint:fix": "eslint . --fix",
"lint:check": "npx @eslint/config-inspector",
"sizecheck": "npx vite-bundle-visualizer"
},
"dependencies": {
"@alova/scene-vue": "^1.6.0",
"@vueuse/core": "^10.11.0",
"alova": "^2.21.3",
"@vueuse/core": "^13.0.0",
"alova": "^3.2.10",
"colord": "^2.9.3",
"echarts": "^5.5.1",
"md-editor-v3": "^4.15.2",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"quill": "^2.0.2",
"echarts": "^5.6.0",
"md-editor-v3": "^5.4.5",
"pinia": "^3.0.1",
"pinia-plugin-persistedstate": "^4.2.0",
"quill": "^2.0.3",
"radash": "^12.1.0",
"vue": "^3.4.33",
"vue-i18n": "^9.13.1",
"vue-router": "^4.4.0"
"vue": "^3.5.13",
"vue-draggable-plus": "^0.6.0",
"vue-i18n": "^11.1.2",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@antfu/eslint-config": "^2.23.0",
"@iconify-json/icon-park-outline": "^1.1.15",
"@iconify/vue": "^4.1.2",
"@types/node": "^20.14.11",
"@vitejs/plugin-vue": "^5.0.5",
"@vitejs/plugin-vue-jsx": "^4.0.0",
"eslint": "^9.7.0",
"lint-staged": "^15.2.7",
"naive-ui": "^2.39.0",
"sass": "^1.77.8",
"simple-git-hooks": "^2.11.1",
"typescript": "^5.5.3",
"unocss": "^0.61.5",
"unplugin-auto-import": "^0.18.0",
"unplugin-icons": "^0.19.0",
"unplugin-vue-components": "^0.27.3",
"vite": "^5.3.4",
"@antfu/eslint-config": "^4.11.0",
"@iconify-json/icon-park-outline": "^1.2.2",
"@iconify/vue": "^4.3.0",
"@types/node": "^22.14.0",
"@vitejs/plugin-vue": "^5.2.3",
"@vitejs/plugin-vue-jsx": "^4.1.2",
"eslint": "^9.24.0",
"lint-staged": "^15.5.0",
"naive-ui": "^2.41.0",
"sass": "^1.86.3",
"simple-git-hooks": "^2.12.1",
"typescript": "^5.8.3",
"unocss": "^0.65.4",
"unplugin-auto-import": "^19.1.2",
"unplugin-icons": "^22.1.0",
"unplugin-vue-components": "^28.4.1",
"vite": "^6.2.5",
"vite-bundle-visualizer": "^1.2.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-devtools": "7.3.6",
"vue-tsc": "^2.0.26"
"vite-plugin-vue-devtools": "7.7.2",
"vue-tsc": "^2.2.8"
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { naiveI18nOptions } from '@/utils'
import { darkTheme } from 'naive-ui'
import { useAppStore } from './store'
import { naiveI18nOptions } from '@/utils'
const appStore = useAppStore()

View File

@ -1,9 +1,9 @@
<script setup lang="ts">
import { NFlex } from 'naive-ui'
import { useAppStore } from '@/store'
import IconSun from '~icons/icon-park-outline/sun-one'
import IconMoon from '~icons/icon-park-outline/moon'
import IconAuto from '~icons/icon-park-outline/laptop-computer'
import IconMoon from '~icons/icon-park-outline/moon'
import IconSun from '~icons/icon-park-outline/sun-one'
import { NFlex } from 'naive-ui'
const { t } = useI18n()
@ -12,7 +12,7 @@ const appStore = useAppStore()
const options = computed(() => {
return [
{
label: t('app.lignt'),
label: t('app.light'),
value: 'light',
icon: IconSun,
},

View File

@ -3,7 +3,7 @@ interface Props {
message: string
}
const props = defineProps<Props>()
const { message } = defineProps<Props>()
</script>
<template>
@ -11,6 +11,6 @@ const props = defineProps<Props>()
<template #trigger>
<icon-park-outline-help class="op-50 cursor-help" />
</template>
{{ props.message }}
{{ message }}
</n-tooltip>
</template>

View File

@ -5,9 +5,9 @@ interface Props {
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
disabled: false,
})
const {
disabled = false,
} = defineProps<Props>()
interface IconList {
prefix: string
@ -39,6 +39,16 @@ async function fetchIconAllList(nameList: string[]) {
return i
})
}
// svg
function getSvgName(path: string) {
const regex = /\/([^/]+)\.svg$/
const match = path.match(regex)
if (match) {
const fileName = match[1]
return fileName
}
return path
}
//
function generateLocalIconList() {
@ -48,15 +58,6 @@ function generateLocalIconList() {
eager: true,
})
function getSvgName(path: string) {
const regex = /\/([^/]+)\.svg$/
const match = path.match(regex)
if (match) {
const fileName = match[1]
return fileName
}
return path
}
return mapEntries(localSvgList, (key, value) => {
return [getSvgName(key), value]
})
@ -103,9 +104,8 @@ const icons = computed(() => {
//
const visibleIcons = computed(() => {
return icons.value
?.filter(i => i.includes(searchValue.value))
?.slice((currentPage.value - 1) * 200, (currentPage.value) * 200)
return icons.value?.filter(i => i
.includes(searchValue.value))?.slice((currentPage.value - 1) * 200, (currentPage.value) * 200)
})
const showModal = ref(false)
@ -125,13 +125,13 @@ function clearIcon() {
<template>
<n-input-group disabled>
<n-button v-if="value" :disabled="props.disabled" type="primary">
<n-button v-if="value" :disabled="disabled" type="primary">
<template #icon>
<nova-icon :icon="value" />
</template>
</n-button>
<n-input :value="value" readonly :placeholder="$t('components.iconSelector.inputPlaceholder')" />
<n-button type="primary" ghost :disabled="props.disabled" @click="showModal = true">
<n-button type="primary" ghost :disabled="disabled" @click="showModal = true">
{{ $t('common.choose') }}
</n-button>
</n-input-group>

View File

@ -11,13 +11,10 @@ interface iconPorps {
/* 图标深度 */
depth?: 1 | 2 | 3 | 4 | 5
}
const props = withDefaults(defineProps<iconPorps>(), {
size: 18,
isLocal: false,
})
const { size = 18, icon } = defineProps<iconPorps>()
const isLocal = computed(() => {
return props.icon && props.icon.startsWith('local:')
return icon && icon.startsWith('local:')
})
function getLocalIcon(icon: string) {
@ -28,25 +25,22 @@ function getLocalIcon(icon: string) {
eager: true,
})
const domparser = new DOMParser()
return domparser.parseFromString(svg[`/src/assets/svg-icons/${svgName}.svg`], 'image/svg+xml')
return svg[`/src/assets/svg-icons/${svgName}.svg`]
}
</script>
<template>
<n-icon
v-if="icon && !isLocal"
v-if="icon"
:size="size"
:depth="depth"
:color="color"
>
<template v-if="isLocal">
{{ getLocalIcon(icon) }}
<i v-html="getLocalIcon(icon)" />
</template>
<template v-else>
<Icon :icon="icon" />
</template>
</n-icon>
</template>
<style scoped></style>

View File

@ -1,10 +1,10 @@
<script setup lang="ts">
const props = defineProps({
count: {
type: Number,
default: 0,
},
})
interface Props {
count?: number
}
const {
count = 0,
} = defineProps<Props>()
const emit = defineEmits<{
change: [page: number, pageSize: number] //
@ -21,11 +21,11 @@ function changePage() {
<template>
<n-pagination
v-if="props.count > 0"
v-if="count > 0"
v-model:page="page"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30, 50]"
:item-count="props.count"
:item-count="count"
:display-order="displayOrder"
show-size-picker
@update-page="changePage"

View File

@ -3,16 +3,15 @@ interface Props {
showWatermark: boolean
text?: string
}
const props = withDefaults(defineProps<Props>(), {
showWatermark: false,
text: 'Watermark',
})
const {
text = 'Watermark',
} = defineProps<Props>()
</script>
<template>
<n-watermark
v-if="props.showWatermark"
:content="props.text"
v-if="showWatermark"
:content="text"
cross
fullscreen
:font-size="16"

View File

@ -1,13 +1,14 @@
<script setup lang="ts">
const props = defineProps<{
interface Props {
maxLength?: string
}>()
}
const { maxLength } = defineProps<Props>()
const modelValue = defineModel<string>('value')
</script>
<template>
<div v-if="modelValue" class="inline-flex items-center gap-0.5em">
<n-ellipsis :style="{ 'max-width': props.maxLength || '12em' }">
<n-ellipsis :style="{ 'max-width': maxLength || '12em' }">
{{ modelValue }}
</n-ellipsis>
<n-tooltip trigger="hover">

View File

@ -1,26 +1,16 @@
<script setup lang="ts">
import type { ToolbarNames } from 'md-editor-v3'
import { MdEditor } from 'md-editor-v3'
// https://imzbf.github.io/md-editor-v3/zh-CN/docs
import 'md-editor-v3/lib/style.css'
import { useAppStore } from '@/store'
const props = defineProps<{
modelValue: string
}>()
import { MdEditor } from 'md-editor-v3'
// https://imzbf.github.io/md-editor-v3/zh-CN/docs
import 'md-editor-v3/lib/style.css'
const emit = defineEmits(['update:modelValue'])
const model = defineModel<string>()
const appStore = useAppStore()
const data = useVModel(props, 'modelValue', emit)
const theme = computed(() => {
return appStore.colorMode ? 'dark' : 'light'
})
const toolbarsExclude: ToolbarNames[] = [
'mermaid',
'katex',
@ -32,7 +22,7 @@ const toolbarsExclude: ToolbarNames[] = [
<template>
<MdEditor
v-model="data" :theme="theme" :toolbars-exclude="toolbarsExclude"
v-model="model" :theme="appStore.colorMode" :toolbars-exclude="toolbarsExclude"
/>
</template>

View File

@ -1,15 +1,16 @@
<script setup lang="ts">
import Quill from 'quill'
import { useTemplateRef } from 'vue'
import 'quill/dist/quill.snow.css'
defineOptions({
name: 'RichTextEditor',
})
const props = defineProps<{
const { disabled } = defineProps<Props>()
interface Props {
disabled?: boolean
}>()
}
const model = defineModel<string>()
let editorInst = null
@ -20,7 +21,7 @@ onMounted(() => {
initEditor()
})
const editorRef = ref()
const editorRef = useTemplateRef<HTMLElement>('editorRef')
function initEditor() {
const options = {
modules: {
@ -52,13 +53,13 @@ function initEditor() {
placeholder: 'Insert text here ...',
theme: 'snow',
}
const quill = new Quill(editorRef.value, options)
const quill = new Quill(editorRef.value!, options)
quill.on('text-change', (_delta, _oldDelta, _source) => {
editorModel.value = quill.getSemanticHTML()
})
if (props.disabled)
if (disabled)
quill.enable(false)
editorInst = quill
@ -92,7 +93,7 @@ watch(editorModel, (newValue, oldValue) => {
})
watch(
() => props.disabled,
() => disabled,
(newValue, _oldValue) => {
editorInst!.enable(!newValue)
},

View File

@ -11,12 +11,12 @@ export function install(app: App) {
function clipboardEnable() {
if (!isSupported.value) {
window.$message.error('Your browser does not support Clipboard API')
window.$message.error($t('components.copyText.unsupportedError'))
return false
}
if (permissionWrite.value !== 'granted') {
window.$message.error('Currently not permitted to use Clipboard API')
window.$message.error($t('components.copyText.unpermittedError'))
return false
}
return true

View File

@ -1,6 +1,3 @@
import * as echarts from 'echarts/core'
import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts'
// 系列类型的定义后缀都为 SeriesOption
import type {
BarSeriesOption,
@ -8,7 +5,6 @@ import type {
PieSeriesOption,
RadarSeriesOption,
} from 'echarts/charts'
// 组件类型的定义后缀都为 ComponentOption
import type {
DatasetComponentOption,
@ -18,6 +14,9 @@ import type {
ToolboxComponentOption,
TooltipComponentOption,
} from 'echarts/components'
import { useAppStore } from '@/store'
import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts'
import {
DatasetComponent, // 数据集组件
GridComponent,
@ -27,10 +26,11 @@ import {
TooltipComponent,
TransformComponent, // 内置数据转换器组件 (filter, sort)
} from 'echarts/components'
import * as echarts from 'echarts/core'
import { LabelLayout, UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'
import { useAppStore } from '@/store'
import { useTemplateRef } from 'vue'
// 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型
export type ECOption = echarts.ComposeOption<
@ -68,14 +68,16 @@ echarts.use([
* Echarts hooks函数
* @description
*/
export function useEcharts(el: Ref<HTMLElement | null>, chartOptions: Ref<ECOption>) {
export function useEcharts(ref: string, chartOptions: Ref<ECOption>) {
const el = useTemplateRef<HTMLLIElement>(ref)
const appStore = useAppStore()
let chart: echarts.ECharts | null = null
const { width, height } = useElementSize(el)
const isRendered = computed(() => Boolean(el && chart))
const isRendered = () => Boolean(el && chart)
async function render() {
// 宽或高不存在时不渲染
@ -90,9 +92,10 @@ export function useEcharts(el: Ref<HTMLElement | null>, chartOptions: Ref<ECOpti
}
}
function update(updateOptions: ECOption) {
if (isRendered.value)
chart!.setOption({ ...updateOptions, backgroundColor: 'transparent' })
async function update(updateOptions: ECOption) {
if (isRendered()) {
chart!.setOption({ backgroundColor: 'transparent', ...updateOptions })
}
}
function destroy() {
@ -101,7 +104,7 @@ export function useEcharts(el: Ref<HTMLElement | null>, chartOptions: Ref<ECOpti
}
watch([width, height], async ([newWidth, newHeight]) => {
if (isRendered.value && newWidth && newHeight)
if (isRendered() && newWidth && newHeight)
chart?.resize()
})

View File

@ -1,4 +1,3 @@
import { isArray, isString } from 'radash'
import { useAuthStore } from '@/store'
/** 权限判断 */
@ -6,7 +5,7 @@ export function usePermission() {
const authStore = useAuthStore()
function hasPermission(
permission: Entity.RoleType | Entity.RoleType[] | undefined,
permission?: Entity.RoleType[],
) {
if (!permission)
return true
@ -15,13 +14,9 @@ export function usePermission() {
return false
const { role } = authStore.userInfo
let has = role === 'super'
let has = role.includes('super')
if (!has) {
if (isArray(permission))
has = permission.includes(role)
if (isString(permission))
has = permission === role
has = permission.every(i => role.includes(i))
}
return has
}

35
src/hooks/useTableDrag.ts Normal file
View File

@ -0,0 +1,35 @@
import type { NDataTable } from 'naive-ui'
import { useDraggable } from 'vue-draggable-plus'
export function useTableDrag<T = unknown>(params: {
tableRef: Ref<InstanceType<typeof NDataTable> | undefined>
data: Ref<T[]>
onRowDrag: (rows: T[]) => void
}) {
const tableEl = computed(() => params.tableRef?.value?.$el as HTMLElement)
const tableBodyRef = ref<HTMLElement | undefined>(undefined)
const { start } = useDraggable(tableBodyRef, params.data, {
immediate: false,
animation: 150,
handle: '.drag-handle',
onEnd: (event) => {
const { oldIndex, newIndex } = event
const start = Math.min(oldIndex!, newIndex!)
const end = Math.max(oldIndex!, newIndex!) - start + 1
const changedRows = [...params.data.value].splice(start, end)
params.onRowDrag(unref([...changedRows]))
},
})
onMounted(async () => {
while (!tableBodyRef.value) {
tableBodyRef.value = tableEl.value?.querySelector('tbody') || undefined
await new Promise(resolve => setTimeout(resolve, 100))
}
})
watchOnce(() => tableBodyRef.value, (el) => {
el && start()
})
}

View File

@ -2,7 +2,7 @@
interface Props {
list?: Entity.Message[]
}
const props = defineProps<Props>()
const { list } = defineProps<Props>()
const emit = defineEmits<Emits>()
interface Emits {
@ -13,7 +13,7 @@ interface Emits {
<template>
<n-scrollbar style="height: 400px">
<n-list hoverable clickable>
<n-list-item v-for="(item) in props.list" :key="item.id" @click="emit('read', item.id)">
<n-list-item v-for="(item) in list" :key="item.id" @click="emit('read', item.id)">
<n-thing content-indented :class="{ 'opacity-30': item.isRead }">
<template #header>
<n-ellipsis :line-clamp="1">

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import LayoutSelector from './LayoutSelector.vue'
import { useAppStore } from '@/store'
import LayoutSelector from './LayoutSelector.vue'
const appStore = useAppStore()

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { useRouteStore } from '@/store'
import { useBoolean } from '@/hooks'
import { useRouteStore } from '@/store'
const routeStore = useRouteStore()

View File

@ -1,10 +1,10 @@
<script setup lang="ts">
import { renderIcon } from '@/utils/icon'
import { useAuthStore } from '@/store'
import IconGithub from '~icons/icon-park-outline/github'
import IconUser from '~icons/icon-park-outline/user'
import IconLogout from '~icons/icon-park-outline/logout'
import { renderIcon } from '@/utils/icon'
import IconBookOpen from '~icons/icon-park-outline/book-open'
import IconGithub from '~icons/icon-park-outline/github'
import IconLogout from '~icons/icon-park-outline/logout'
import IconUser from '~icons/icon-park-outline/user'
const { t } = useI18n()
@ -82,7 +82,7 @@ function handleSelect(key: string | number) {
>
<n-avatar
round
class="cursor-pointer"
:src="userInfo?.avatar"
>
<template #fallback>

View File

@ -1,30 +1,30 @@
import Logo from './sider/Logo.vue'
import Menu from './sider/Menu.vue'
import BackTop from './common/BackTop.vue'
import Setting from './common/Setting.vue'
import SettingDrawer from './common/SettingDrawer.vue'
import Breadcrumb from './header/Breadcrumb.vue'
import CollapaseButton from './header/CollapaseButton.vue'
import FullScreen from './header/FullScreen.vue'
import Notices from './header/Notices.vue'
import UserCenter from './header/UserCenter.vue'
import Search from './header/Search.vue'
import UserCenter from './header/UserCenter.vue'
import Logo from './sider/Logo.vue'
import Menu from './sider/Menu.vue'
import TabBar from './tab/TabBar.vue'
import BackTop from './common/BackTop.vue'
import Setting from './common/Setting.vue'
import SettingDrawer from './common/SettingDrawer.vue'
export {
BackTop,
Breadcrumb,
CollapaseButton,
Menu,
Logo,
FullScreen,
Logo,
Menu,
Notices,
Search,
Setting,
SettingDrawer,
Notices,
UserCenter,
Search,
TabBar,
BackTop,
UserCenter,
}

View File

@ -1,25 +1,24 @@
<script setup lang="ts">
import type { RouteLocationNormalized } from 'vue-router'
import Reload from './Reload.vue'
import DropTabs from './DropTabs.vue'
import ContentFullScreen from './ContentFullScreen.vue'
import { useAppStore, useTabStore } from '@/store'
import IconRedo from '~icons/icon-park-outline/redo'
import { useDraggable } from 'vue-draggable-plus'
import IconClose from '~icons/icon-park-outline/close'
import IconDelete from '~icons/icon-park-outline/delete-four'
import IconFullwith from '~icons/icon-park-outline/fullwidth'
import IconRedo from '~icons/icon-park-outline/redo'
import IconLeft from '~icons/icon-park-outline/to-left'
import IconRight from '~icons/icon-park-outline/to-right'
import IconFullwith from '~icons/icon-park-outline/fullwidth'
import ContentFullScreen from './ContentFullScreen.vue'
import DropTabs from './DropTabs.vue'
import Reload from './Reload.vue'
import TabBarItem from './TabBarItem.vue'
const tabStore = useTabStore()
const appStore = useAppStore()
const router = useRouter()
function handleTab(route: RouteLocationNormalized) {
router.push(route.path)
}
function handleClose(path: string) {
tabStore.closeTab(path)
router.push(route.fullPath)
}
const { t } = useI18n()
const options = computed(() => {
@ -71,16 +70,16 @@ function handleSelect(key: string) {
appStore.reloadPage()
},
closeCurrent() {
tabStore.closeTab(currentRoute.value.path)
tabStore.closeTab(currentRoute.value.fullPath)
},
closeOther() {
tabStore.closeOtherTabs(currentRoute.value.path)
tabStore.closeOtherTabs(currentRoute.value.fullPath)
},
closeLeft() {
tabStore.closeLeftTabs(currentRoute.value.path)
tabStore.closeLeftTabs(currentRoute.value.fullPath)
},
closeRight() {
tabStore.closeRightTabs(currentRoute.value.path)
tabStore.closeRightTabs(currentRoute.value.fullPath)
},
closeAll() {
tabStore.closeAllTabs()
@ -101,54 +100,49 @@ function handleContextMenu(e: MouseEvent, route: RouteLocationNormalized) {
function onClickoutside() {
showDropdown.value = false
}
// const [DefineTabItem, ReuseTabItem] = createReusableTemplate<{ route: RouteLocationNormalized }>()
const el = ref()
useDraggable(el, tabStore.tabs, {
animation: 150,
ghostClass: 'ghost',
})
</script>
<template>
<div class="wh-full flex items-end">
<n-tabs
type="card"
size="small"
:tabs-padding="10"
:value="tabStore.currentTabPath"
@close="handleClose"
>
<n-tab
v-for="item in tabStore.pinTabs"
:key="item.path"
:name="item.path"
@click="router.push(item.path)"
>
<div class="flex-x-center gap-2">
<nova-icon :icon="item.meta.icon" /> {{ $t(`route.${String(item.name)}`, item.meta.title) }}
</div>
</n-tab>
<n-tab
v-for="item in tabStore.tabs"
:key="item.path"
closable
:name="item.path"
<div class="p-l-2 flex w-full relative">
<div class="flex items-end">
<TabBarItem
v-for="item in tabStore.pinTabs" :key="item.fullPath" :value="tabStore.currentTabPath" :route="item"
@click="handleTab(item)"
/>
</div>
<div ref="el" class="flex items-end flex-1">
<TabBarItem
v-for="item in tabStore.tabs" :key="item.fullPath" :value="tabStore.currentTabPath" :route="item" closable
@close="tabStore.closeTab"
@click="handleTab(item)"
@contextmenu="handleContextMenu($event, item)"
>
<div class="flex-x-center gap-2">
<nova-icon :icon="item.meta.icon" /> {{ $t(`route.${String(item.name)}`, item.meta.title) }}
</div>
</n-tab>
<template #suffix>
<Reload />
<ContentFullScreen />
<DropTabs />
</template>
</n-tabs>
<n-dropdown
placement="bottom-start"
trigger="manual"
:x="x"
:y="y"
:options="options"
:show="showDropdown"
:on-clickoutside="onClickoutside"
@select="handleSelect"
/>
/>
<n-dropdown
placement="bottom-start" trigger="manual" :x="x" :y="y" :options="options" :show="showDropdown"
:on-clickoutside="onClickoutside" @select="handleSelect"
/>
</div>
<!-- <span class="m-l-auto" /> -->
<n-el class="absolute right-0 flex items-center gap-1 bg-[var(--base-color)] h-full">
<Reload />
<ContentFullScreen />
<DropTabs />
</n-el>
</div>
</template>
<style scoped>
.ghost {
opacity: 0.5;
background: #c4f6d5;
}
</style>

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import type { RouteLocationNormalized } from 'vue-router'
const { route, value, closable = false } = defineProps<{
route: RouteLocationNormalized
value: string
closable?: boolean
}>()
const emit = defineEmits<{
close: [string]
}>()
</script>
<template>
<n-el
class="cursor-pointer p-x-4 p-y-2 m-x-2px b b-[--divider-color] b-b-[#0000] rounded-[--border-radius]"
:class="[
value === route.fullPath ? 'c-[--primary-color]' : 'c-[--text-color-2]',
value === route.fullPath ? 'bg-[#0000]' : 'bg-[--tab-color]',
closable && 'p-r-2',
]"
style="transition: box-shadow .3s var(--n-bezier), color .3s var(--n-bezier), background-color .3s var(--n-bezier), border-color .3s var(--n-bezier);"
>
<div class="flex-center gap-2 text-nowrap">
<nova-icon :icon="route.meta.icon" />
<span>{{ $t(`route.${String(route.name)}`, route.meta.title) }}</span>
<button
v-if="closable"
type="button"
class="bg-transparent h-18px w-18px flex-center text-[var(--close-icon-color)] hover:bg-[var(--close-color-hover)] rounded-3px"
style="transition: background-color .3s var(--n-bezier), color .3s var(--n-bezier);"
@click.stop="emit('close', route.fullPath)"
>
<n-icon size="14">
<svg viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g fill="currentColor" fill-rule="nonzero"><path d="M2.08859116,2.2156945 L2.14644661,2.14644661 C2.32001296,1.97288026 2.58943736,1.95359511 2.7843055,2.08859116 L2.85355339,2.14644661 L6,5.293 L9.14644661,2.14644661 C9.34170876,1.95118446 9.65829124,1.95118446 9.85355339,2.14644661 C10.0488155,2.34170876 10.0488155,2.65829124 9.85355339,2.85355339 L6.707,6 L9.85355339,9.14644661 C10.0271197,9.32001296 10.0464049,9.58943736 9.91140884,9.7843055 L9.85355339,9.85355339 C9.67998704,10.0271197 9.41056264,10.0464049 9.2156945,9.91140884 L9.14644661,9.85355339 L6,6.707 L2.85355339,9.85355339 C2.65829124,10.0488155 2.34170876,10.0488155 2.14644661,9.85355339 C1.95118446,9.65829124 1.95118446,9.34170876 2.14644661,9.14644661 L5.293,6 L2.14644661,2.85355339 C1.97288026,2.67998704 1.95359511,2.41056264 2.08859116,2.2156945 L2.14644661,2.14644661 L2.08859116,2.2156945 Z" /></g></g></svg>
</n-icon>
</button>
</div>
</n-el>
</template>

View File

@ -1,9 +1,9 @@
<script setup lang="ts">
import leftMenu from './leftMenu.layout.vue'
import topMenu from './topMenu.layout.vue'
import mixMenu from './mixMenu.layout.vue'
import { SettingDrawer } from './components'
import { useAppStore } from '@/store/app'
import { SettingDrawer } from './components'
import leftMenu from './leftMenu.layout.vue'
import mixMenu from './mixMenu.layout.vue'
import topMenu from './topMenu.layout.vue'
const appStore = useAppStore()
const layoutMap = {

View File

@ -1,4 +1,5 @@
<script lang="ts" setup>
import { useAppStore, useRouteStore } from '@/store'
import {
BackTop,
Breadcrumb,
@ -12,7 +13,6 @@ import {
TabBar,
UserCenter,
} from './components'
import { useAppStore, useRouteStore } from '@/store'
const routeStore = useRouteStore()
const appStore = useAppStore()

View File

@ -1,5 +1,6 @@
<script lang="ts" setup>
import type { MenuInst, MenuOption } from 'naive-ui'
import { useAppStore, useRouteStore } from '@/store'
import {
BackTop,
CollapaseButton,
@ -11,7 +12,6 @@ import {
TabBar,
UserCenter,
} from './components'
import { useAppStore, useRouteStore } from '@/store'
const routeStore = useRouteStore()
const appStore = useAppStore()
@ -52,8 +52,8 @@ onMounted(() => {
const sideMenu = ref<MenuOption[]>([])
function handleSideMenu(key: string) {
// @ts-expect-error no error at here
const targetMenu = routeStore.menus.find(i => i.key === key)
const routeMenu = routeStore.menus as MenuOption[]
const targetMenu = routeMenu.find(i => i.key === key)
if (targetMenu) {
sideMenu.value = targetMenu.children ? targetMenu.children : [targetMenu]
}

View File

@ -1,4 +1,5 @@
<script lang="ts" setup>
import { useAppStore, useRouteStore } from '@/store'
import {
BackTop,
FullScreen,
@ -10,7 +11,6 @@ import {
TabBar,
UserCenter,
} from './components'
import { useAppStore, useRouteStore } from '@/store'
const routeStore = useRouteStore()
const appStore = useAppStore()

View File

@ -1,8 +1,8 @@
import type { App } from 'vue'
import AppVue from './App.vue'
import AppLoading from './components/common/AppLoading.vue'
import { installRouter } from '@/router'
import { installPinia } from '@/store'
import AppVue from './App.vue'
import AppLoading from './components/common/AppLoading.vue'
async function setupApp() {
// 载入全局loading加载状态

View File

@ -1,13 +1,15 @@
import { createI18n } from 'vue-i18n'
import type { App } from 'vue'
import { local } from '@/utils'
import { createI18n } from 'vue-i18n'
import enUS from '../../locales/en_US.json'
import zhCN from '../../locales/zh_CN.json'
import { local } from '@/utils'
const { VITE_DEFAULT_LANG } = import.meta.env
export const i18n = createI18n({
legacy: false,
locale: local.get('lang') || 'enUS', // 默认显示语言
fallbackLocale: 'enUS',
locale: local.get('lang') || VITE_DEFAULT_LANG, // 默认显示语言
fallbackLocale: VITE_DEFAULT_LANG,
messages: {
zhCN,
enUS,

View File

@ -61,7 +61,7 @@ export function setupRouterGuard(router: Router) {
// 添加tabs
tabStore.addTab(to)
// 设置高亮标签;
tabStore.setCurrentTab(to.path as string)
tabStore.setCurrentTab(to.fullPath as string)
})
router.afterEach((to) => {

View File

@ -1,7 +1,7 @@
import type { App } from 'vue'
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
import { routes } from './routes.inner'
import { setupRouterGuard } from './guard'
import { routes } from './routes.inner'
const { VITE_ROUTE_MODE = 'hash', VITE_BASE_URL } = import.meta.env
export const router = createRouter({

View File

@ -34,8 +34,8 @@ export const staticRoutes: AppRoute.RowRoute[] = [
pid: 1,
},
{
name: 'test',
path: '/test',
name: 'multi',
path: '/multi',
title: '多级菜单演示',
requiresAuth: true,
icon: 'icon-park-outline:list',
@ -45,32 +45,32 @@ export const staticRoutes: AppRoute.RowRoute[] = [
pid: null,
},
{
name: 'test2',
path: '/test/test2',
name: 'multi2',
path: '/multi/multi2',
title: '多级菜单子页',
requiresAuth: true,
icon: 'icon-park-outline:list',
menuType: 'page',
componentPath: '/test/test2/index.vue',
componentPath: '/demo/multi/multi2/index.vue',
id: 6,
pid: 4,
},
{
name: 'test2Detail',
path: '/test/test2/detail',
title: '多级菜单详情页',
name: 'multi2Detail',
path: '/multi/multi2/detail',
title: '菜单详情页',
requiresAuth: true,
icon: 'icon-park-outline:list',
hide: true,
activeMenu: '/test/test2',
activeMenu: '/multi/multi2',
menuType: 'page',
componentPath: '/test/test2/detail/index.vue',
componentPath: '/demo/multi/multi2/detail/index.vue',
id: 7,
pid: 4,
},
{
name: 'test3',
path: '/test/test3',
name: 'multi3',
path: '/multi/multi3',
title: '多级菜单',
requiresAuth: true,
icon: 'icon-park-outline:list',
@ -80,12 +80,12 @@ export const staticRoutes: AppRoute.RowRoute[] = [
pid: 4,
},
{
name: 'test4',
path: '/test/test3/test4',
name: 'multi4',
path: '/multi/multi3/multi4',
title: '多级菜单3-1',
requiresAuth: true,
icon: 'icon-park-outline:list',
componentPath: '/test/test3/test4/index.vue',
componentPath: '/demo/multi/multi3/multi4/index.vue',
id: 9,
pid: 8,
},
@ -106,7 +106,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
title: '常用列表',
requiresAuth: true,
icon: 'icon-park-outline:list-view',
componentPath: '/list/commonList/index.vue',
componentPath: '/demo/list/commonList/index.vue',
id: 11,
pid: 10,
},
@ -116,7 +116,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
title: '卡片列表',
requiresAuth: true,
icon: 'icon-park-outline:view-grid-list',
componentPath: '/list/cardList/index.vue',
componentPath: '/demo/list/cardList/index.vue',
id: 12,
pid: 10,
},
@ -240,7 +240,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
title: 'Vue',
requiresAuth: true,
icon: 'logos:vue',
componentPath: '/documents/vue/index.vue',
componentPath: '/demo/documents/vue/index.vue',
id: 25,
pid: 24,
},
@ -250,7 +250,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
title: 'Vite',
requiresAuth: true,
icon: 'logos:vitejs',
componentPath: '/documents/vite/index.vue',
componentPath: '/demo/documents/vite/index.vue',
id: 26,
pid: 24,
},
@ -282,7 +282,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
title: '权限示例',
requiresAuth: true,
icon: 'icon-park-outline:right-user',
componentPath: '/permission/permission/index.vue',
componentPath: '/demo/permission/permission/index.vue',
id: 29,
pid: 28,
},
@ -295,7 +295,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
'super',
],
icon: 'icon-park-outline:wrong-user',
componentPath: '/permission/justSuper/index.vue',
componentPath: '/demo/permission/justSuper/index.vue',
id: 30,
pid: 28,
},
@ -390,7 +390,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
title: '个人中心',
requiresAuth: true,
icon: 'carbon:user-avatar-filled-alt',
componentPath: '/userCenter/index.vue',
componentPath: '/demo/userCenter/index.vue',
id: 39,
pid: null,
},
@ -400,7 +400,7 @@ export const staticRoutes: AppRoute.RowRoute[] = [
title: '关于',
requiresAuth: true,
icon: 'icon-park-outline:info',
componentPath: '/about/index.vue',
componentPath: '/demo/about/index.vue',
id: 40,
pid: null,
},
@ -435,4 +435,14 @@ export const staticRoutes: AppRoute.RowRoute[] = [
id: 43,
pid: 13,
},
{
name: 'draggableList',
path: '/list/draggableList',
title: '拖拽列表',
requiresAuth: true,
icon: 'icon-park-outline:menu-fold',
componentPath: '/demo/list/draggableList/index.vue',
id: 44,
pid: 10,
},
]

View File

@ -36,7 +36,7 @@ export function withoutToken() {
/* 接口数据转换 */
export function dictData() {
return request.Get('/getDictData', {
transformData(rawData, _headers) {
transform(rawData, _headers) {
const response = rawData as any
return {
...response,
@ -61,10 +61,7 @@ export function getBlob(url: string) {
/* 带进度的下载文件 */
export function downloadFile(url: string) {
const methodInstance = blankInstance.Get<Blob>(url, {
// 开启下载进度
enableDownload: true,
})
const methodInstance = blankInstance.Get<Blob>(url)
methodInstance.meta = {
// 标识为blob数据
isBlob: true,

View File

@ -1,20 +1,20 @@
import { local } from '@/utils'
import { createAlova } from 'alova'
import VueHook from 'alova/vue'
import GlobalFetch from 'alova/GlobalFetch'
import { createServerTokenAuthentication } from '@alova/scene-vue'
import { createServerTokenAuthentication } from 'alova/client'
import adapterFetch from 'alova/fetch'
import VueHook, { type VueHookType } from 'alova/vue'
import {
DEFAULT_ALOVA_OPTIONS,
DEFAULT_BACKEND_OPTIONS,
} from './config'
import {
handleBusinessError,
handleRefreshToken,
handleResponseError,
handleServiceResult,
} from './handle'
import {
DEFAULT_ALOVA_OPTIONS,
DEFAULT_BACKEND_OPTIONS,
} from './config'
import { local } from '@/utils'
const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication({
const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication<VueHookType>({
// 服务端判定token过期
refreshTokenOnSuccess: {
// 当服务端返回401时表示token过期
@ -50,8 +50,8 @@ export function createAlovaInstance(
return createAlova({
statesHook: VueHook,
requestAdapter: GlobalFetch(),
localCache: null,
requestAdapter: adapterFetch(),
cacheFor: null,
baseURL: _alovaConfig.baseURL,
timeout: _alovaConfig.timeout,

View File

@ -1,10 +1,10 @@
import { fetchUpdateToken } from '@/service'
import { useAuthStore } from '@/store'
import { local } from '@/utils'
import {
ERROR_NO_TIP_STATUS,
ERROR_STATUS,
} from './config'
import { useAuthStore } from '@/store'
import { fetchUpdateToken } from '@/service'
import { local } from '@/utils'
type ErrorStatus = keyof typeof ERROR_STATUS

View File

@ -1,6 +1,6 @@
import { createAlovaInstance } from './alova'
import { serviceConfig } from '@/../service.config'
import { generateProxyPattern } from '@/../build/proxy'
import { serviceConfig } from '@/../service.config'
import { createAlovaInstance } from './alova'
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y'

View File

@ -1,4 +1,4 @@
export * from './api/system'
export * from './api/login'
export * from './api/list'
export * from './api/login'
export * from './api/system'
export * from './api/test'

View File

@ -1,12 +1,14 @@
import type { GlobalThemeOverrides } from 'naive-ui'
import { local, setLocale } from '@/utils'
import { colord } from 'colord'
import { set } from 'radash'
import themeConfig from './theme.json'
import { local, setLocale } from '@/utils'
export type TransitionAnimation = '' | 'fade-slide' | 'fade-bottom' | 'fade-scale' | 'zoom-fade' | 'zoom-out'
export type LayoutMode = 'leftMenu' | 'topMenu' | 'mixMenu'
const { VITE_DEFAULT_LANG, VITE_COPYRIGHT_INFO } = import.meta.env
const docEle = ref(document.documentElement)
const { isFullscreen, toggle } = useFullscreen(docEle)
@ -18,8 +20,8 @@ const { system, store } = useColorMode({
export const useAppStore = defineStore('app-store', {
state: () => {
return {
footerText: import.meta.env.VITE_COPYRIGHT_INFO,
lang: 'enUS' as App.lang,
footerText: VITE_COPYRIGHT_INFO,
lang: VITE_DEFAULT_LANG,
theme: themeConfig as GlobalThemeOverrides,
primaryColor: themeConfig.common.primaryColor,
collapsed: false,

View File

@ -1,8 +1,8 @@
import { router } from '@/router'
import { fetchLogin } from '@/service'
import { local } from '@/utils'
import { useRouteStore } from './router'
import { useTabStore } from './tab'
import { fetchLogin } from '@/service'
import { router } from '@/router'
import { local } from '@/utils'
interface AuthStatus {
userInfo: Api.Login.Info | null

58
src/store/dict.ts Normal file
View File

@ -0,0 +1,58 @@
import { fetchDictList } from '@/service'
import { session } from '@/utils'
export const useDictStore = defineStore('dict-store', {
state: () => {
return {
dictMap: {} as DictMap,
isInitDict: false,
}
},
actions: {
async dict(code: string) {
// 调用前初始化
if (!this.dictMap) {
this.initDict()
}
const targetDict = await this.getDict(code)
return {
data: () => targetDict,
enum: () => Object.fromEntries(targetDict.map(({ value, label }) => [value, label])),
valueMap: () => Object.fromEntries(targetDict.map(({ value, ...data }) => [value, data])),
labelMap: () => Object.fromEntries(targetDict.map(({ label, ...data }) => [label, data])),
}
},
async getDict(code: string) {
const isExist = Reflect.has(this.dictMap, code)
if (isExist) {
return this.dictMap[code]
}
else {
return await this.getDictByNet(code)
}
},
async getDictByNet(code: string) {
const { data, isSuccess } = await fetchDictList(code)
if (isSuccess) {
Reflect.set(this.dictMap, code, data)
// 同步至session
session.set('dict', this.dictMap)
return data
}
else {
throw new Error(`Failed to get ${code} dictionary from network, check ${code} field or network`)
}
},
initDict() {
const dict = session.get('dict')
if (dict) {
Object.assign(this.dictMap, dict)
}
this.isInitDict = true
},
},
})

View File

@ -3,6 +3,7 @@ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
export * from './app/index'
export * from './auth'
export * from './dict'
export * from './router'
export * from './tab'

View File

@ -1,10 +1,10 @@
import { clone, min, omit, pick } from 'radash'
import type { RouteRecordRaw } from 'vue-router'
import type { MenuOption } from 'naive-ui'
import { RouterLink } from 'vue-router'
import Layout from '@/layouts/index.vue'
import type { RouteRecordRaw } from 'vue-router'
import { usePermission } from '@/hooks'
import Layout from '@/layouts/index.vue'
import { $t, arrayToTree, renderIcon } from '@/utils'
import { clone, min, omit, pick } from 'radash'
import { RouterLink } from 'vue-router'
const metaFields: AppRoute.MetaKeys[]
= ['title', 'icon', 'requiresAuth', 'roles', 'keepAlive', 'hide', 'order', 'href', 'activeMenu', 'withoutTab', 'pinTab', 'menuType']
@ -104,8 +104,9 @@ export function createMenus(userRoutes: AppRoute.RowRoute[]) {
// render the returned routing table as a sidebar
function transformAuthRoutesToMenus(userRoutes: AppRoute.Route[]) {
const { hasPermission } = usePermission()
// Filter out side menus without permission
return userRoutes.filter(i => hasPermission(i.meta.roles))
return userRoutes
// Filter out side menus without permission
.filter(i => hasPermission(i.meta.roles))
// Sort the menu according to the order size
.sort((a, b) => {
if (a.meta && a.meta.order && b.meta && b.meta.order)
@ -116,7 +117,6 @@ function transformAuthRoutesToMenus(userRoutes: AppRoute.Route[]) {
return 1
else return 0
})
// Convert to side menu data structure
.map((item) => {
const target: MenuOption = {

View File

@ -1,10 +1,10 @@
import type { MenuOption } from 'naive-ui'
import { createMenus, createRoutes, generateCacheRoutes } from './helper'
import { $t, local } from '@/utils'
import { router } from '@/router'
import { fetchUserRoutes } from '@/service'
import { staticRoutes } from '@/router/routes.static'
import { fetchUserRoutes } from '@/service'
import { useAuthStore } from '@/store/auth'
import { $t, local } from '@/utils'
import { createMenus, createRoutes, generateCacheRoutes } from './helper'
interface RoutesStatus {
isInitAuthRoute: boolean
@ -38,7 +38,7 @@ export const useRouteStore = defineStore('route-store', {
},
async initRouteInfo() {
if (import.meta.env.VITE_AUTH_ROUTE_MODE === 'dynamic') {
if (import.meta.env.VITE_ROUTE_LOAD_MODE === 'dynamic') {
const userInfo = local.get('userInfo')
if (!userInfo || !userInfo.id) {

View File

@ -24,7 +24,7 @@ export const useTabStore = defineStore('tab-store', {
return
// 如果标签名称已存在则不添加
if (this.hasExistTab(route.path as string))
if (this.hasExistTab(route.fullPath as string))
return
// 根据meta.pinTab传递到不同的分组中
@ -33,42 +33,42 @@ export const useTabStore = defineStore('tab-store', {
else
this.tabs.push(route)
},
async closeTab(path: string) {
async closeTab(fullPath: string) {
const tabsLength = this.tabs.length
// 如果动态标签大于一个,才会标签跳转
if (this.tabs.length > 1) {
// 获取关闭的标签索引
const index = this.getTabIndex(path)
const index = this.getTabIndex(fullPath)
const isLast = index + 1 === tabsLength
// 如果是关闭的当前页面,路由跳转到原先标签的后一个标签
if (this.currentTabPath === path && !isLast) {
if (this.currentTabPath === fullPath && !isLast) {
// 跳转到后一个标签
router.push(this.tabs[index + 1].path)
router.push(this.tabs[index + 1].fullPath)
}
else if (this.currentTabPath === path && isLast) {
else if (this.currentTabPath === fullPath && isLast) {
// 已经是最后一个了,就跳转前一个
router.push(this.tabs[index - 1].path)
router.push(this.tabs[index - 1].fullPath)
}
}
// 删除标签
this.tabs = this.tabs.filter((item) => {
return item.path !== path
return item.fullPath !== fullPath
})
// 删除后如果清空了,就跳转到默认首页
if (tabsLength - 1 === 0)
router.push('/')
},
closeOtherTabs(path: string) {
const index = this.getTabIndex(path)
closeOtherTabs(fullPath: string) {
const index = this.getTabIndex(fullPath)
this.tabs = this.tabs.filter((item, i) => i === index)
},
closeLeftTabs(path: string) {
const index = this.getTabIndex(path)
closeLeftTabs(fullPath: string) {
const index = this.getTabIndex(fullPath)
this.tabs = this.tabs.filter((item, i) => i >= index)
},
closeRightTabs(path: string) {
const index = this.getTabIndex(path)
closeRightTabs(fullPath: string) {
const index = this.getTabIndex(fullPath)
this.tabs = this.tabs.filter((item, i) => i <= index)
},
clearAllTabs() {
@ -80,21 +80,25 @@ export const useTabStore = defineStore('tab-store', {
router.push('/')
},
hasExistTab(path: string) {
hasExistTab(fullPath: string) {
const _tabs = [...this.tabs, ...this.pinTabs]
return _tabs.some((item) => {
return item.path === path
return item.fullPath === fullPath
})
},
/* 设置当前激活的标签 */
setCurrentTab(path: string) {
this.currentTabPath = path
setCurrentTab(fullPath: string) {
this.currentTabPath = fullPath
},
getTabIndex(path: string) {
getTabIndex(fullPath: string) {
return this.tabs.findIndex((item) => {
return item.path === path
return item.fullPath === fullPath
})
},
modifyTab(fullPath: string, modifyFn: (route: RouteLocationNormalized) => void) {
const index = this.getTabIndex(fullPath)
modifyFn(this.tabs[index])
},
},
persist: {
storage: sessionStorage,

View File

@ -14,3 +14,7 @@ body,
.gray-mode {
filter: grayscale(100%);
}
.drag-handle {
cursor: move;
}

View File

@ -6,3 +6,9 @@
.v-x-scroll {
height: 100%;
}
/* 解决二维码尺寸问题 */
.n-qr-code{
height: unset !important;
width: unset !important;;
}

View File

@ -7,7 +7,7 @@ namespace Api {
/** 用户id */
id: number
/** 用户角色类型 */
role: Entity.RoleType
role: Entity.RoleType[]
/** 访问toekn */
accessToken: string
/** 刷新toekn */

View File

@ -1,15 +0,0 @@
/// <reference path="../global.d.ts"/>
/* 角色数据库表字段 */
namespace Entity {
interface DemoList {
id: number
name: string
age: number
gender: '0' | '1' | null
email: string
address: string
role: Entity.RoleType
disabled: boolean
}
}

View File

@ -21,17 +21,18 @@ interface ImportMetaEnv {
| 'brotliCompress'
| 'deflate'
| 'deflateRaw'
/** hash路由模式 */
/** 路由模式 */
readonly VITE_ROUTE_MODE?: 'hash' | 'web'
/** 路由加载模式 */
readonly VITE_AUTH_ROUTE_MODE: 'static' | 'dynamic'
readonly VITE_ROUTE_LOAD_MODE: 'static' | 'dynamic'
/** 首次加载页面 */
readonly VITE_HOME_PATH: string
/** 版权信息 */
readonly VITE_COPYRIGHT_INFO: string
/** 是否自动刷新token */
readonly VITE_AUTO_REFRESH_TOKEN: 'Y' | 'N'
/** 默认语言 */
readonly VITE_DEFAULT_LANG: App.lang
/** 后端服务的环境类型 */
readonly MODE: ServiceEnvType
}

View File

@ -1,67 +0,0 @@
import { fetchDictList } from '@/service'
import { session } from '@/utils'
export const dictMap: DictMap = {}
/**
*
*
* @param code -
* @return
*/
export async function dict(code: string) {
const targetDict = await getDict(code)
return {
data: () => targetDict,
enum: () => Object.fromEntries(targetDict.map(({ value, label }) => [value, label])),
valueMap: () => Object.fromEntries(targetDict.map(({ value, ...data }) => [value, data])),
labelMap: () => Object.fromEntries(targetDict.map(({ label, ...data }) => [label, data])),
}
}
/**
*
*
*
* @param code -
* @return
*/
export async function getDict(code: string) {
const isExist = Reflect.has(dictMap, code)
if (isExist) {
return dictMap[code]
}
else {
return await getDictByNet(code)
}
}
/**
*
*
* @param code -
* @return
*/
export async function getDictByNet(code: string) {
const { data, isSuccess } = await fetchDictList(code)
if (isSuccess) {
Reflect.set(dictMap, code, data)
// 同步至session
session.set('dict', dictMap)
return data
}
else {
throw new Error(`Failed to get ${code} dictionary from network, check ${code} field or network`)
}
}
function initDict() {
const dict = session.get('dict')
if (dict) {
Object.assign(dictMap, dict)
}
}
initDict()

View File

@ -1,6 +1,6 @@
import type { NDateLocale, NLocale } from 'naive-ui'
import { dateZhCN, zhCN } from 'naive-ui'
import { i18n } from '@/modules/i18n'
import { dateZhCN, zhCN } from 'naive-ui'
export function setLocale(locale: App.lang) {
i18n.global.locale.value = locale

View File

@ -1,5 +1,4 @@
export * from './icon'
export * from './storage'
export * from './array'
export * from './i18n'
export * from './dict'
export * from './icon'

View File

@ -39,9 +39,8 @@ function createLocalStorage<T extends Storage.Local>() {
window.localStorage.removeItem(`${STORAGE_PREFIX}${String(key)}`)
}
function clear() {
window.localStorage.clear()
}
const clear = window.localStorage.clear
return {
set,
get,
@ -73,9 +72,7 @@ function createSessionStorage<T extends Storage.Session>() {
function remove(key: keyof T) {
window.sessionStorage.removeItem(`${STORAGE_PREFIX}${String(key)}`)
}
function clear() {
window.sessionStorage.clear()
}
const clear = window.sessionStorage.clear
return {
set,

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { graphic } from 'echarts'
import { type ECOption, useEcharts } from '@/hooks'
import { graphic } from 'echarts'
const lineOptions = ref<ECOption>({
tooltip: {
@ -114,8 +114,7 @@ const lineOptions = ref<ECOption>({
}],
}) as Ref<ECOption>
const lineRef = ref<HTMLElement | null>(null)
useEcharts(lineRef, lineOptions)
useEcharts('lineRef', lineOptions)
</script>
<template>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { graphic } from 'echarts'
import { type ECOption, useEcharts } from '@/hooks'
import { graphic } from 'echarts'
const chartData = [
{ name: '1', value: 300 },
@ -91,8 +91,7 @@ const option = ref<ECOption>({
}],
}) as Ref<ECOption>
const lineRef = ref<HTMLElement | null>(null)
useEcharts(lineRef, option)
useEcharts('lineRef', option)
</script>
<template>

View File

@ -50,8 +50,7 @@ const option = ref<ECOption>({
},
],
}) as Ref<ECOption>
const lineRef = ref<HTMLElement | null>(null)
useEcharts(lineRef, option)
useEcharts('lineRef', option)
</script>
<template>

View File

@ -99,8 +99,7 @@ const lineOptions = ref<ECOption>({
},
],
}) as Ref<ECOption>
const lineRef = ref<HTMLElement | null>(null)
useEcharts(lineRef, lineOptions)
useEcharts('lineRef', lineOptions)
</script>
<template>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import Chart from './components/chart.vue'
import { useAuthStore } from '@/store'
import Chart from './components/chart.vue'
const { userInfo } = useAuthStore()
</script>

View File

@ -33,19 +33,19 @@ function handleValidateClick() {
<div>
<n-h2>回调value</n-h2>
<pre class="bg-#eee">
<pre class="bg-#eee:30">
{{ cbValue }}
</pre>
</div>
<div>
<n-h2>回调option</n-h2>
<pre class="bg-#eee">
<pre class="bg-#eee:30">
{{ cbOption }}
</pre>
</div>
<div>
<n-h2>回调pathValues</n-h2>
<pre class="bg-#eee">
<pre class="bg-#eee:30">
{{ cbPathValues }}
</pre>
</div>

View File

@ -1,6 +1,8 @@
<script setup lang="ts">
import { fetchDictList } from '@/service'
import { dict } from '@/utils/dict'
import { useDictStore } from '@/store'
const { dict } = useDictStore()
const dictKey = ref('')
const options = ref()
@ -88,7 +90,7 @@ const enumLabel = computed(() => {
</n-button>
</n-flex>
<pre class="bg-#eee">
<pre class="bg-#eee:30">
{{ data }}
</pre>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { graphic } from 'echarts'
import { type ECOption, useEcharts } from '@/hooks'
import { graphic } from 'echarts'
//
const pieOptions = ref<ECOption>({
@ -53,9 +53,28 @@ const pieOptions = ref<ECOption>({
],
},
],
}) as Ref<ECOption>
const pieRef = ref<HTMLElement | null>(null)
useEcharts(pieRef, pieOptions)
})
const { update } = useEcharts('pieRef', pieOptions)
const randomValue = () => Math.round(Math.random() * 100)
function updatePieChart() {
pieOptions.value.series = [
{
data: [
{ value: randomValue(), name: 'rose1' },
{ value: randomValue(), name: 'rose2' },
{ value: randomValue(), name: 'rose3' },
{ value: randomValue(), name: 'rose4' },
{ value: randomValue(), name: 'rose5' },
{ value: randomValue(), name: 'rose6' },
{ value: randomValue(), name: 'rose7' },
{ value: randomValue(), name: 'rose8' },
],
},
]
update(pieOptions.value)
}
// 线
const lineOptions = ref<ECOption>({
@ -245,9 +264,8 @@ const lineOptions = ref<ECOption>({
data: [820, 932, 901, 934, 1290, 1330, 1320],
},
],
}) as Ref<ECOption>
const lineRef = ref<HTMLElement | null>(null)
useEcharts(lineRef, lineOptions)
})
useEcharts('lineRef', lineOptions)
//
const barOptions = ref<ECOption>({
@ -386,9 +404,8 @@ const barOptions = ref<ECOption>({
data: [200, 382, 102, 267, 186, 315, 316],
},
],
}) as Ref<ECOption>
const barRef = ref<HTMLElement | null>(null)
useEcharts(barRef, barOptions)
})
useEcharts('barRef', barOptions)
//
const radarOptions = ref<ECOption>({
@ -480,9 +497,8 @@ const radarOptions = ref<ECOption>({
],
},
],
}) as Ref<ECOption>
const radarRef = ref<HTMLElement | null>(null)
useEcharts(radarRef, radarOptions)
})
useEcharts('radarRef', radarOptions)
</script>
<template>
@ -491,6 +507,9 @@ useEcharts(radarRef, radarOptions)
:size="16"
>
<n-card>
<n-button @click="updatePieChart">
手动更新图表
</n-button>
<div
ref="pieRef"
class="h-400px"

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import { useRequest } from 'alova'
import {
downloadFile,
} from '@/service'
import { useRequest } from 'alova/client'
const emit = defineEmits<{
update: [data: any]

View File

@ -15,6 +15,7 @@ async function expiredToken() {
<template>
<n-card title="Token Expiration" size="small">
注意观察第二次的请求token已刷新
<n-button type="error" @click="expiredToken">
click
</n-button>

View File

@ -1,9 +1,10 @@
<script setup lang="ts">
import { useRequest } from 'alova'
import {
fetchGet,
} from '@/service'
import { useRequest } from 'alova/client'
const emit = defineEmits<{
update: [data: any] //
}>()

View File

@ -1,20 +1,20 @@
<script setup lang="ts">
import Env from './components/Env.vue'
import Get from './components/Get.vue'
import Post from './components/Post.vue'
import FormPost from './components/FormPost.vue'
import Delete from './components/Delete.vue'
import Put from './components/Put.vue'
import UseRequest from './components/UseRequest.vue'
import NoToken from './components/NoToken.vue'
import DownLoad from './components/DownLoad.vue'
import DownLoadWithProgress from './components/DownLoadWithProgress.vue'
import Env from './components/Env.vue'
import FailedRequest from './components/FailedRequest.vue'
import FailedResponse from './components/FailedResponse.vue'
import FailedResponseWithoutTip from './components/FailedResponseWithoutTip.vue'
import Transform from './components/Transform.vue'
import FormPost from './components/FormPost.vue'
import Get from './components/Get.vue'
import NoToken from './components/NoToken.vue'
import Post from './components/Post.vue'
import Put from './components/Put.vue'
import RefreshToken from './components/RefreshToken.vue'
import TokenExpiration from './components/TokenExpiration.vue'
import Transform from './components/Transform.vue'
import UseRequest from './components/UseRequest.vue'
const msg = ref()
function handleUpdate(data: any) {
@ -46,7 +46,7 @@ function handleUpdate(data: any) {
</div>
</template>
<template #2>
<pre class="bg-#eee">
<pre class="bg-#eee:30">
{{ msg }}
</pre>
</template>

View File

@ -24,6 +24,9 @@
<div>
大大大<svg-icons-cool class="text-4em" />
</div>
<div>
nova-icon组件加载<nova-icon icon="local:cool" />
</div>
</n-card>
</n-space>
</template>

View File

@ -1,33 +1,31 @@
<script setup lang="ts">
type FormModel = Pick<Entity.DemoList, 'name' | 'age' | 'gender' | 'address' | 'email' | 'role' | 'disabled'>
const props = withDefaults(defineProps<Props>(), {
type: 'add',
modalData: null,
})
const emit = defineEmits<Emits>()
const defaultFormModal: FormModel = {
name: '',
age: 0,
gender: null,
email: '',
address: '',
role: 'user',
disabled: true,
}
const formModel = ref({ ...defaultFormModal })
interface Props {
visible: boolean
type?: ModalType
modalData?: any
}
const {
visible,
type = 'add',
modalData = null,
} = defineProps<Props>()
const emit = defineEmits<Emits>()
const defaultFormModal: Entity.User = {
userName: '',
gender: 0,
email: '',
role: [],
}
const formModel = ref({ ...defaultFormModal })
interface Emits {
(e: 'update:visible', visible: boolean): void
}
const modalVisible = computed({
get() {
return props.visible
return visible
},
set(visible) {
closeModal(visible)
@ -42,7 +40,7 @@ const title = computed(() => {
add: '添加用户',
edit: '编辑用户',
}
return titles[props.type]
return titles[type]
})
function UpdateFormModelByModalType() {
@ -51,14 +49,14 @@ function UpdateFormModelByModalType() {
formModel.value = { ...defaultFormModal }
},
edit: () => {
if (props.modalData)
formModel.value = { ...props.modalData }
if (modalData)
formModel.value = { ...modalData }
},
}
handlers[props.type]()
handlers[type]()
}
watch(
() => props.visible,
() => visible,
(newValue) => {
if (newValue)
UpdateFormModelByModalType()
@ -81,26 +79,26 @@ watch(
<n-form label-placement="left" :model="formModel" label-align="left" :label-width="80">
<n-grid :cols="24" :x-gap="18">
<n-form-item-grid-item :span="12" label="用户名" path="name">
<n-input v-model:value="formModel.name" />
<n-input v-model:value="formModel.userName" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="12" label="年龄" path="age">
<n-input-number v-model:value="formModel.age" />
<n-input-number v-model:value="formModel.gender" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="12" label="性别" path="gender">
<n-input v-model:value="formModel.gender" />
<n-radio-group v-model:value="formModel.gender">
<n-space>
<n-radio :value="1">
</n-radio>
<n-radio :value="0">
</n-radio>
</n-space>
</n-radio-group>
</n-form-item-grid-item>
<n-form-item-grid-item :span="12" label="邮箱" path="email">
<n-input v-model:value="formModel.email" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="12" label="地址" path="address">
<n-input v-model:value="formModel.address" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="12" label="角色" path="role">
<n-input v-model:value="formModel.role" />
</n-form-item-grid-item>
<n-form-item-grid-item :span="12" label="状态" path="disabled">
<n-switch v-model:value="formModel.disabled" />
</n-form-item-grid-item>
</n-grid>
</n-form>
<template #action>

View File

@ -1,10 +1,10 @@
<script setup lang="tsx">
import type { DataTableColumns, FormInst } from 'naive-ui'
import { Gender } from '@/constants'
import { useBoolean } from '@/hooks'
import { fetchUserPage } from '@/service'
import { NButton, NPopconfirm, NSpace, NSwitch, NTag } from 'naive-ui'
import TableModal from './components/TableModal.vue'
import { fetchUserList } from '@/service'
import { useBoolean } from '@/hooks'
import { Gender } from '@/constants'
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
const { bool: visible, setTrue: openModal } = useBoolean(false)
@ -18,14 +18,14 @@ const initialModel = {
const model = ref({ ...initialModel })
const formRef = ref<FormInst | null>()
function sendMail(id: number) {
function sendMail(id?: number) {
window.$message.success(`删除用户id:${id}`)
}
const columns: DataTableColumns<Entity.DemoList> = [
const columns: DataTableColumns<Entity.User> = [
{
title: '姓名',
align: 'center',
key: 'name',
key: 'userName',
},
{
title: '年龄',
@ -55,34 +55,18 @@ const columns: DataTableColumns<Entity.DemoList> = [
align: 'center',
key: 'email',
},
{
title: '地址',
align: 'center',
key: 'address',
},
{
title: '角色',
align: 'center',
key: 'role',
render: (row) => {
const tagType: Record<Entity.RoleType, NaiveUI.ThemeColor> = {
super: 'primary',
admin: 'warning',
user: 'success',
}
return <NTag type={tagType[row.role]}>{row.role}</NTag>
},
},
{
title: '状态',
align: 'center',
key: 'disabled',
key: 'status',
render: (row) => {
return (
<NSwitch
value={row.disabled}
onUpdateValue={disabled =>
handleUpdateDisabled(disabled, row.id)}
value={row.status}
checked-value={1}
unchecked-value={0}
onUpdateValue={(value: 0 | 1) =>
handleUpdateDisabled(value, row.id!)}
>
{{ checked: () => '启用', unchecked: () => '禁用' }}
</NSwitch>
@ -114,11 +98,11 @@ const columns: DataTableColumns<Entity.DemoList> = [
},
]
const listData = ref<Entity.DemoList[]>([])
function handleUpdateDisabled(disabled: boolean, id: number) {
const listData = ref<Entity.User[]>([])
function handleUpdateDisabled(value: 0 | 1, id: number) {
const index = listData.value.findIndex(item => item.id === id)
if (index > -1)
listData.value[index].disabled = disabled
listData.value[index].status = value
}
onMounted(() => {
@ -126,7 +110,7 @@ onMounted(() => {
})
async function getUserList() {
startLoading()
await fetchUserList().then((res: any) => {
await fetchUserPage().then((res: any) => {
listData.value = res.data.list
endLoading()
})
@ -144,12 +128,12 @@ function setModalType(type: ModalType) {
modalType.value = type
}
const editData = ref<Entity.DemoList | null>(null)
function setEditData(data: Entity.DemoList | null) {
const editData = ref<Entity.User | null>(null)
function setEditData(data: Entity.User | null) {
editData.value = data
}
function handleEditTable(row: Entity.DemoList) {
function handleEditTable(row: Entity.User) {
setEditData(row)
setModalType('edit')
openModal()

View File

@ -0,0 +1,170 @@
<script setup lang="tsx">
import type { DataTableColumns, FormInst, NDataTable } from 'naive-ui'
import { Gender } from '@/constants'
import { useBoolean } from '@/hooks'
import { useTableDrag } from '@/hooks/useTableDrag'
import { fetchUserPage } from '@/service'
import { NButton, NPopconfirm, NSpace, NSwitch, NTag } from 'naive-ui'
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
const initialModel = {
condition_1: '',
condition_2: '',
condition_3: '',
condition_4: '',
}
const model = ref({ ...initialModel })
const formRef = ref<FormInst | null>()
function sendMail(id?: number) {
window.$message.success(`删除用户id:${id}`)
}
const columns: DataTableColumns<Entity.User> = [
{
title: '姓名',
align: 'center',
key: 'userName',
},
{
title: '年龄',
align: 'center',
key: 'age',
},
{
title: '性别',
align: 'center',
key: 'gender',
render: (row) => {
const tagType = {
0: 'primary',
1: 'success',
} as const
if (row.gender) {
return (
<NTag type={tagType[row.gender]}>
{Gender[row.gender]}
</NTag>
)
}
},
},
{
title: '邮箱',
align: 'center',
key: 'email',
},
{
title: '状态',
align: 'center',
key: 'status',
render: (row) => {
return (
<NSwitch
value={row.status}
checked-value={1}
unchecked-value={0}
onUpdateValue={(value: 0 | 1) =>
handleUpdateDisabled(value, row.id!)}
>
{{ checked: () => '启用', unchecked: () => '禁用' }}
</NSwitch>
)
},
},
{
title: '操作',
align: 'center',
key: 'actions',
render: (row) => {
return (
<NSpace justify="center">
<NPopconfirm onPositiveClick={() => sendMail(row.id)}>
{{
default: () => '确认删除',
trigger: () => <NButton size="small">删除</NButton>,
}}
</NPopconfirm>
</NSpace>
)
},
},
]
const listData = ref<Entity.User[]>([])
function handleUpdateDisabled(value: 0 | 1, id: number) {
const index = listData.value.findIndex(item => item.id === id)
if (index > -1)
listData.value[index].status = value
}
const tableRef = ref<InstanceType<typeof NDataTable>>()
useTableDrag({
tableRef,
data: listData,
onRowDrag(data) {
const target = data[data.length - 1]
window.$message.success(`拖拽数据 id: ${target.id} name: ${target.userName}`)
},
})
onMounted(() => {
getUserList()
})
async function getUserList() {
startLoading()
await fetchUserPage().then((res: any) => {
listData.value = res.data.list
endLoading()
})
}
function changePage(page: number, size: number) {
window.$message.success(`分页器:${page},${size}`)
}
function handleResetSearch() {
model.value = { ...initialModel }
}
</script>
<template>
<NSpace vertical size="large">
<n-card>
<n-form ref="formRef" :model="model" label-placement="left" inline :show-feedback="false">
<n-flex>
<n-form-item label="姓名" path="condition_1">
<n-input v-model:value="model.condition_1" placeholder="请输入" />
</n-form-item>
<n-form-item label="年龄" path="condition_2">
<n-input v-model:value="model.condition_2" placeholder="请输入" />
</n-form-item>
<n-form-item label="性别" path="condition_3">
<n-input v-model:value="model.condition_3" placeholder="请输入" />
</n-form-item>
<n-form-item label="地址" path="condition_4">
<n-input v-model:value="model.condition_4" placeholder="请输入" />
</n-form-item>
<n-flex class="ml-auto">
<NButton type="primary" @click="getUserList">
<template #icon>
<icon-park-outline-search />
</template>
搜索
</NButton>
<NButton strong secondary @click="handleResetSearch">
<template #icon>
<icon-park-outline-redo />
</template>
重置
</NButton>
</n-flex>
</n-flex>
</n-form>
</n-card>
<n-card>
<NSpace vertical size="large">
<n-data-table ref="tableRef" row-class-name="drag-handle" :columns="columns" :data="listData" :loading="loading" />
<Pagination :count="100" @change="changePage" />
</NSpace>
</n-card>
</NSpace>
</template>

View File

@ -6,20 +6,20 @@ import AMap from './components/AMap.vue'
import BMap from './components/BMap.vue'
defineOptions({
name: 'PluginMap',
name: 'map',
})
const maps = [
{
id: 'AMap',
label: '高德地图',
component: AMap,
},
{
id: 'BMap',
label: '百度地图',
component: BMap,
},
{
id: 'AMap',
label: '高德地图',
component: AMap,
},
]
</script>

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
import { useTabStore } from '@/store'
const { modifyTab } = useTabStore()
const { fullPath, query } = useRoute()
modifyTab(fullPath, (target) => {
target.meta.title = `详情页${query.id}`
})
</script>
<template>
<n-space vertical>
<n-alert title="目前可公开的情报" type="warning">
这是详情子页他不会出现在侧边栏,他其实是上个页面的同级并不是下级这个要注意
</n-alert>
<n-h2>
详情页id:{{ query.id }}
</n-h2>
</n-space>
</template>
<style scoped></style>

View File

@ -0,0 +1,20 @@
<script setup lang="ts">
const router = useRouter()
</script>
<template>
<n-card class="h-130vh">
这个页面包含了一个不在侧边菜单的详情页面
<n-button @click="router.push({ path: '/multi/multi2/detail', query: { id: 1 } })">
跳转详情子页1
</n-button>
<n-button @click="router.push({ path: '/multi/multi2/detail', query: { id: 2 } })">
跳转详情子页2
</n-button>
<n-button @click="router.push({ path: '/multi/multi2/detail', query: { id: 3 } })">
跳转详情子页3
</n-button>
</n-card>
</template>
<style scoped></style>

View File

@ -1,10 +1,10 @@
<script setup lang="ts">
import { useAuthStore } from '@/store'
import { usePermission } from '@/hooks'
import { useAuthStore } from '@/store'
const authStore = useAuthStore()
const { hasPermission } = usePermission()
const { role } = authStore.userInfo!
const { role } = authStore.userInfo
const roleList: Entity.RoleType[] = ['super', 'admin', 'user']
@ -23,7 +23,7 @@ function toggleUserRole(role: Entity.RoleType) {
</n-button-group>
<n-h2>v-permission 指令用法</n-h2>
<n-space>
<n-button v-permission="'super'">
<n-button v-permission="['super']">
仅super可见
</n-button>
<n-button v-permission="['admin']">
@ -33,10 +33,10 @@ function toggleUserRole(role: Entity.RoleType) {
<n-h2>usePermission 函数用法</n-h2>
<n-space>
<n-button v-if="hasPermission('super')">
<n-button v-if="hasPermission(['super'])">
super可见
</n-button>
<n-button v-if="hasPermission('admin')">
<n-button v-if="hasPermission(['admin'])">
admin可见
</n-button>
<n-button v-if="hasPermission(['admin', 'user'])">
@ -45,5 +45,3 @@ function toggleUserRole(role: Entity.RoleType) {
</n-space>
</n-card>
</template>
<style scoped></style>

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { FormInst } from 'naive-ui'
import { local } from '@/utils'
import { useAuthStore } from '@/store'
import { local } from '@/utils'
const emit = defineEmits(['update:modelValue'])

View File

@ -6,9 +6,9 @@ interface Props {
modalName?: string
}
const props = withDefaults(defineProps<Props>(), {
modalName: '',
})
const {
modalName = '',
} = defineProps<Props>()
const emit = defineEmits<{
open: []
@ -36,7 +36,7 @@ const modalTitle = computed(() => {
view: '查看',
edit: '编辑',
}
return `${titleMap[modalType.value]}${props.modalName}`
return `${titleMap[modalType.value]}${modalName}`
})
async function openModal(type: ModalType = 'add', data: any) {
@ -108,7 +108,7 @@ const rules = {
},
}
const options = ref<Entity.Role[]>([])
const options = ref()
async function getRoleList() {
const { data } = await fetchRoleList()
options.value = data

View File

@ -1,11 +1,11 @@
<script setup lang="tsx">
import type { DataTableColumns, FormInst } from 'naive-ui'
import CopyText from '@/components/custom/CopyText.vue'
import { Gender } from '@/constants'
import { useBoolean } from '@/hooks'
import { fetchUserPage } from '@/service'
import { NButton, NPopconfirm, NSpace, NSwitch, NTag } from 'naive-ui'
import TableModal from './components/TableModal.vue'
import { fetchUserPage } from '@/service'
import { useBoolean } from '@/hooks'
import { Gender } from '@/constants'
import CopyText from '@/components/custom/CopyText.vue'
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
@ -74,7 +74,7 @@ const columns: DataTableColumns<Entity.User> = [
value={row.status}
checked-value={1}
unchecked-value={0}
onUpdateValue={value =>
onUpdateValue={(value: 0 | 1) =>
handleUpdateDisabled(value, row.id!)}
>
{{ checked: () => '启用', unchecked: () => '禁用' }}

View File

@ -8,10 +8,11 @@ interface Props {
isRoot?: boolean
}
const props = withDefaults(defineProps<Props>(), {
modalName: '',
isRoot: false,
})
const {
modalName = '',
dictCode,
isRoot = false,
} = defineProps<Props>()
const emit = defineEmits<{
open: []
@ -36,7 +37,7 @@ const modalTitle = computed(() => {
view: '查看',
edit: '编辑',
}
return `${titleMap[modalType.value]}${props.modalName}`
return `${titleMap[modalType.value]}${modalName}`
})
async function openModal(type: ModalType = 'add', data?: any) {
@ -47,9 +48,9 @@ async function openModal(type: ModalType = 'add', data?: any) {
async add() {
formModel.value = { ...formDefault }
formModel.value.isRoot = props.isRoot ? 1 : 0
if (props.dictCode) {
formModel.value.code = props.dictCode
formModel.value.isRoot = isRoot ? 1 : 0
if (dictCode) {
formModel.value.code = dictCode
}
},
async view() {

View File

@ -1,15 +1,17 @@
<script setup lang="tsx">
import type { DataTableColumns } from 'naive-ui'
import CopyText from '@/components/custom/CopyText.vue'
import { useBoolean } from '@/hooks'
import { fetchDictList } from '@/service'
import { useDictStore } from '@/store'
import { NButton, NFlex, NPopconfirm } from 'naive-ui'
import DictModal from './components/DictModal.vue'
import { fetchDictList } from '@/service'
import { useBoolean } from '@/hooks'
import { getDictByNet } from '@/utils'
import CopyText from '@/components/custom/CopyText.vue'
const { bool: dictLoading, setTrue: startDictLoading, setFalse: endDictLoading } = useBoolean(false)
const { bool: contentLoading, setTrue: startContentLoading, setFalse: endContentLoading } = useBoolean(false)
const { getDictByNet } = useDictStore()
const dictRef = ref<InstanceType<typeof DictModal>>()
const dictContentRef = ref<InstanceType<typeof DictModal>>()

View File

@ -3,8 +3,8 @@ import type {
FormItemRule,
} from 'naive-ui'
import HelpInfo from '@/components/common/HelpInfo.vue'
import { useBoolean } from '@/hooks'
import { Regex } from '@/constants'
import { useBoolean } from '@/hooks'
import { fetchRoleList } from '@/service'
interface Props {
@ -12,9 +12,10 @@ interface Props {
allRoutes: AppRoute.RowRoute[]
}
const props = withDefaults(defineProps<Props>(), {
modalName: '',
})
const {
modalName = '',
allRoutes,
} = defineProps<Props>()
const emit = defineEmits<{
open: []
@ -47,7 +48,7 @@ const modalTitle = computed(() => {
view: '查看',
edit: '编辑',
}
return `${titleMap[modalType.value]}${props.modalName}`
return `${titleMap[modalType.value]}${modalName}`
})
async function openModal(type: ModalType = 'add', data: AppRoute.RowRoute) {
@ -112,7 +113,7 @@ async function submitModal() {
}
const dirTreeOptions = computed(() => {
return filterDirectory(JSON.parse(JSON.stringify(props.allRoutes)))
return filterDirectory(JSON.parse(JSON.stringify(allRoutes)))
})
function filterDirectory(node: any[]) {
@ -161,7 +162,7 @@ const rules = {
},
}
const options = ref<Entity.Role[]>([])
const options = ref()
async function getRoleList() {
const { data } = await fetchRoleList()
options.value = data

View File

@ -1,11 +1,11 @@
<script setup lang="tsx">
import type { DataTableColumns } from 'naive-ui'
import CopyText from '@/components/custom/CopyText.vue'
import { useBoolean } from '@/hooks'
import { fetchAllRoutes } from '@/service'
import { arrayToTree, createIcon } from '@/utils'
import { NButton, NPopconfirm, NSpace, NTag } from 'naive-ui'
import TableModal from './components/TableModal.vue'
import { fetchAllRoutes } from '@/service'
import { useBoolean } from '@/hooks'
import { arrayToTree, createIcon } from '@/utils'
import CopyText from '@/components/custom/CopyText.vue'
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)

View File

@ -1,9 +0,0 @@
<script setup lang="ts"></script>
<template>
<n-alert title="目前可公开的情报" type="warning">
这是详情子页他不会出现在侧边栏,他其实是上个页面的同级并不是下级这个要注意
</n-alert>
</template>
<style scoped></style>

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