mirror of
https://github.com/zclzone/vue-naive-admin.git
synced 2026-07-25 13:34:08 +08:00
Compare commits
2 Commits
2.x
...
779b43f2ff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
779b43f2ff | ||
|
|
07754975da |
@@ -6,7 +6,7 @@ VITE_PUBLIC_PATH = '/'
|
|||||||
|
|
||||||
# Axios 基础路径
|
# Axios 基础路径
|
||||||
# VITE_AXIOS_BASE_URL = '/api' # 用于代理
|
# VITE_AXIOS_BASE_URL = '/api' # 用于代理
|
||||||
VITE_AXIOS_BASE_URL = 'https://m1.apifoxmock.com/m1/3776410-3408296-default' # apifox云端mock
|
VITE_AXIOS_BASE_URL = 'https://mock.apipark.cn/m1/3776410-0-default' # apifox云端mock
|
||||||
|
|
||||||
# 代理配置-target
|
# 代理配置-target
|
||||||
VITE_PROXY_TARGET = 'http://localhost:8085'
|
VITE_PROXY_TARGET = 'http://localhost:8085'
|
||||||
|
|||||||
@@ -4,5 +4,7 @@ VITE_USE_HASH = 'false'
|
|||||||
# 资源公共路径,需要以 /开头和结尾
|
# 资源公共路径,需要以 /开头和结尾
|
||||||
VITE_PUBLIC_PATH = '/'
|
VITE_PUBLIC_PATH = '/'
|
||||||
|
|
||||||
# VITE_AXIOS_BASE_URL = '/api' # 用于代理
|
VITE_AXIOS_BASE_URL = '/api' # 用于代理
|
||||||
VITE_AXIOS_BASE_URL = 'https://m1.apifoxmock.com/m1/3776410-3408296-default' # apifox云端mock
|
|
||||||
|
# 代理配置-target
|
||||||
|
VITE_PROXY_TARGET = 'http://localhost:8085'
|
||||||
|
|||||||
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@@ -7,6 +7,6 @@
|
|||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"sdras.vue-vscode-snippets",
|
"sdras.vue-vscode-snippets",
|
||||||
"mikestead.dotenv",
|
"mikestead.dotenv",
|
||||||
"tu6ge.naive-ui-intelligence"
|
"simonhe.common-intellisense"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -19,5 +19,6 @@
|
|||||||
"markdown",
|
"markdown",
|
||||||
"json",
|
"json",
|
||||||
"jsonc"
|
"jsonc"
|
||||||
]
|
],
|
||||||
|
"common-intellisense.ui": ["naiveUi2"]
|
||||||
}
|
}
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -5,17 +5,8 @@
|
|||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="./LICENSE"><img alt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
|
<a href="./LICENSE"><img alt="MIT License" src="https://badgen.net/github/license/zclzone/vue-naive-admin"/></a>
|
||||||
<a href="https://gitcode.com/zclzone/vue-naive-admin"><img alt="GitCode" src="https://gitcode.com/zclzone/vue-naive-admin/star/badge.svg
|
|
||||||
"/></a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<a href="https://hellogithub.com/repository/54f19ba1f9ae4238b3cbd111f3c428b0" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=54f19ba1f9ae4238b3cbd111f3c428b0&claim_uid=jXGayRdJZScqMNr" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
|
||||||
|
|
||||||
> [!Tip]
|
|
||||||
> 我们提供Web(PC+H5+小程序)的技术支持服务及定制开发,不限前后端,详细需求可联系作者,微信: isme-admin,邮箱: admin@isme.top
|
|
||||||
|
|
||||||
## 简介
|
## 简介
|
||||||
|
|
||||||
Vue Naive Admin 是一款极简风格的后台管理模板,包含前后端解决方案,前端使用 Vite + Vue3 + Pinia + Unocss,后端使用 Nestjs + TypeOrm + MySql,简单易用,赏心悦目,历经十几次重构和细节打磨,诚意满满!!
|
Vue Naive Admin 是一款极简风格的后台管理模板,包含前后端解决方案,前端使用 Vite + Vue3 + Pinia + Unocss,后端使用 Nestjs + TypeOrm + MySql,简单易用,赏心悦目,历经十几次重构和细节打磨,诚意满满!!
|
||||||
@@ -40,8 +31,8 @@ Vue Naive Admin 2022年2月开始开源,从 1.0 到现在的 2.0,一直秉
|
|||||||
|
|
||||||
## 极致的性能
|
## 极致的性能
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## 2.0 和 1.0 区别
|
## 2.0 和 1.0 区别
|
||||||
|
|
||||||
@@ -60,7 +51,6 @@ Vue Naive Admin 提供一套后端代码,技术栈使用 Nestjs + TypeOrm + My
|
|||||||
|
|
||||||
- 源码-github: [isme-nest-serve | github](https://github.com/zclzone/isme-nest-serve)
|
- 源码-github: [isme-nest-serve | github](https://github.com/zclzone/isme-nest-serve)
|
||||||
- 源码-gitee: [isme-nest-serve | gitee](https://gitee.com/isme-admin/isme-nest-serve)
|
- 源码-gitee: [isme-nest-serve | gitee](https://gitee.com/isme-admin/isme-nest-serve)
|
||||||
- 源码-gitcode: [isme-nest-serve | gitcode](https://gitcode.com/zclzone/isme-nest-serve)
|
|
||||||
|
|
||||||
## 文档
|
## 文档
|
||||||
|
|
||||||
@@ -79,10 +69,6 @@ Vue Naive Admin 提供一套后端代码,技术栈使用 Nestjs + TypeOrm + My
|
|||||||
npx degit zclzone/vue-naive-admin
|
npx degit zclzone/vue-naive-admin
|
||||||
```
|
```
|
||||||
|
|
||||||
## 鸣谢
|
|
||||||
|
|
||||||
本项目开发测试及运行环境由 [野草云](https://my.yecaoyun.com/aff.php?aff=3162) 提供支持。
|
|
||||||
|
|
||||||
## 版权说明
|
## 版权说明
|
||||||
|
|
||||||
本项目使用 `MIT协议`,默认授权给任何人,被授权人可免费地无限制的使用、复制、修改、合并、发布、发行、再许可、售卖本软件拷贝、并有权向被供应人授予同等的权利,但必须满足以下条件:
|
本项目使用 `MIT协议`,默认授权给任何人,被授权人可免费地无限制的使用、复制、修改、合并、发布、发行、再许可、售卖本软件拷贝、并有权向被供应人授予同等的权利,但必须满足以下条件:
|
||||||
@@ -97,6 +83,8 @@ npx degit zclzone/vue-naive-admin
|
|||||||
- [naive-admin-go](https://github.com/ituserxxx/naive-admin-go): 一个 Go 后端服务,基于 gin、gorm、mysql、jwt和session,已对接 Vue Naive Admin 2.0。
|
- [naive-admin-go](https://github.com/ituserxxx/naive-admin-go): 一个 Go 后端服务,基于 gin、gorm、mysql、jwt和session,已对接 Vue Naive Admin 2.0。
|
||||||
- [isme-java](https://github.com/AllenDengMs/isme-java): 一个轻量且完成度高的Java后端服务,基于Springboot 3 + JDK21,层次结构严谨,注释齐全,避免过度封装,代码可读性度高,依赖最简化,上手成本低,已集成 账号管理、权限管理、API鉴权、消息国际化等功能。
|
- [isme-java](https://github.com/AllenDengMs/isme-java): 一个轻量且完成度高的Java后端服务,基于Springboot 3 + JDK21,层次结构严谨,注释齐全,避免过度封装,代码可读性度高,依赖最简化,上手成本低,已集成 账号管理、权限管理、API鉴权、消息国际化等功能。
|
||||||
|
|
||||||
## 联系作者 or 进交流群
|
## 入群交流
|
||||||
|
|
||||||
[https://www.isme.top/contact.html](https://www.isme.top/contact.html)
|
添加微信,拉你进群
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"jsx": "preserve",
|
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"],
|
"@/*": ["src/*"],
|
||||||
"~/*": ["./*"]
|
"~/*": ["./*"]
|
||||||
},
|
},
|
||||||
|
"jsx": "preserve",
|
||||||
"allowJs": true
|
"allowJs": true
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
|
|||||||
69
package.json
69
package.json
@@ -13,46 +13,45 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@arco-design/color": "^0.4.0",
|
"@arco-design/color": "^0.4.0",
|
||||||
"@vueuse/core": "^14.2.1",
|
"@vueuse/core": "^11.0.3",
|
||||||
"axios": "^1.16.1",
|
"axios": "^1.7.7",
|
||||||
"dayjs": "^1.11.21",
|
"dayjs": "^1.11.13",
|
||||||
"echarts": "^6.1.0",
|
"echarts": "^5.5.1",
|
||||||
"lodash-es": "^4.18.1",
|
"lodash-es": "^4.17.21",
|
||||||
"naive-ui": "^2.44.1",
|
"naive-ui": "^2.39.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^2.2.2",
|
||||||
"pinia-plugin-persistedstate": "^4.7.1",
|
"pinia-plugin-persistedstate": "^4.0.0",
|
||||||
"vue": "^3.5.35",
|
"vue": "^3.5.3",
|
||||||
"vue-echarts": "^8.0.1",
|
"vue-echarts": "^7.0.3",
|
||||||
"vue-router": "^5.1.0",
|
"vue-router": "^4.4.3",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^9.0.0",
|
"@antfu/eslint-config": "^3.3.2",
|
||||||
"@iconify/json": "^2.2.481",
|
"@iconify/json": "^2.2.245",
|
||||||
"@unocss/eslint-config": "^66.7.0",
|
"@unocss/eslint-config": "^0.62.3",
|
||||||
"@unocss/eslint-plugin": "^66.7.0",
|
"@unocss/eslint-plugin": "^0.62.3",
|
||||||
"@unocss/preset-rem-to-px": "^66.7.0",
|
"@unocss/preset-rem-to-px": "^0.62.3",
|
||||||
"@vitejs/plugin-vue": "^6.0.7",
|
"@vitejs/plugin-vue": "^5.1.3",
|
||||||
"@vitejs/plugin-vue-jsx": "^5.1.5",
|
"@vitejs/plugin-vue-jsx": "^4.0.1",
|
||||||
"eslint": "^10.4.1",
|
"eslint": "^9.9.1",
|
||||||
"eslint-plugin-format": "^2.0.1",
|
"eslint-plugin-format": "^0.1.2",
|
||||||
"esno": "^4.8.0",
|
"esno": "^4.7.0",
|
||||||
"fs-extra": "^11.3.5",
|
"fs-extra": "^11.2.0",
|
||||||
"glob": "^13.0.6",
|
"lint-staged": "^15.2.10",
|
||||||
"lint-staged": "^17.0.7",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"rollup-plugin-visualizer": "^7.0.1",
|
"sass": "^1.78.0",
|
||||||
"simple-git-hooks": "^2.13.1",
|
"simple-git-hooks": "^2.11.1",
|
||||||
"taze": "^19.14.1",
|
"taze": "^0.16.7",
|
||||||
"unocss": "^66.7.0",
|
"unocss": "^0.62.3",
|
||||||
"unplugin-auto-import": "^21.0.0",
|
"unplugin-auto-import": "^0.18.2",
|
||||||
"unplugin-vue-components": "^32.1.0",
|
"unplugin-vue-components": "^0.27.4",
|
||||||
"vite": "^8.0.14",
|
"vite": "^5.4.3",
|
||||||
"vite-plugin-router-warn": "^2.0.0",
|
"vite-plugin-router-warn": "^1.0.0",
|
||||||
"vite-plugin-vue-devtools": "^8.1.2",
|
"vite-plugin-vue-devtools": "^7.4.4"
|
||||||
"vue3-intro-step": "^1.0.5"
|
|
||||||
},
|
},
|
||||||
"simple-git-hooks": {
|
"simple-git-hooks": {
|
||||||
"pre-commit": "npx lint-staged"
|
"pre-commit": "pnpm lint-staged"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*": "eslint --fix"
|
"*": "eslint --fix"
|
||||||
|
|||||||
8591
pnpm-lock.yaml
generated
8591
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -29,9 +29,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { darkTheme, dateZhCN, zhCN } from 'naive-ui'
|
|
||||||
import { LayoutSetting } from '@/components'
|
import { LayoutSetting } from '@/components'
|
||||||
import { useAppStore, useTabStore } from '@/store'
|
import { useAppStore, useTabStore } from '@/store'
|
||||||
|
import { darkTheme, dateZhCN, zhCN } from 'naive-ui'
|
||||||
import { layoutSettingVisible } from './settings'
|
import { layoutSettingVisible } from './settings'
|
||||||
|
|
||||||
const layouts = new Map()
|
const layouts = new Map()
|
||||||
|
|||||||
@@ -11,9 +11,8 @@
|
|||||||
<main :class="{ 'flex-1': full }" class="m-12">
|
<main :class="{ 'flex-1': full }" class="m-12">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
<slot name="footer">
|
<slot v-if="$slots.footer" name="footer" />
|
||||||
<TheFooter v-if="showFooter" class="mb-12 mt-auto" />
|
<TheFooter v-else-if="showFooter" class="mb-12 mt-auto" />
|
||||||
</slot>
|
|
||||||
<n-back-top :bottom="20" />
|
<n-back-top :bottom="20" />
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
<slot v-if="$slots.header" name="header" />
|
<slot v-if="$slots.header" name="header" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<slot name="title-prefix">
|
<slot v-if="$slots['title-prefix']" name="title-prefix" />
|
||||||
<template v-if="back">
|
<template v-else-if="back">
|
||||||
<div
|
<div
|
||||||
class="mr-16 flex cursor-pointer items-center text-16 opacity-60 transition-all-300 hover:opacity-40"
|
class="mr-16 flex cursor-pointer items-center text-16 opacity-60 transition-all-300 hover:opacity-40"
|
||||||
@click="router.back()"
|
@click="router.back()"
|
||||||
@@ -26,8 +26,6 @@
|
|||||||
<span class="ml-4">返回</span>
|
<span class="ml-4">返回</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</slot>
|
|
||||||
|
|
||||||
<div class="mr-12 h-16 w-4 rounded-l-2 bg-primary" />
|
<div class="mr-12 h-16 w-4 rounded-l-2 bg-primary" />
|
||||||
<h2 class="font-normal">
|
<h2 class="font-normal">
|
||||||
{{ title ?? route.meta?.title }}
|
{{ title ?? route.meta?.title }}
|
||||||
@@ -41,11 +39,10 @@
|
|||||||
<slot />
|
<slot />
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
|
||||||
<slot name="footer">
|
<slot v-if="$slots.footer" name="footer" />
|
||||||
<AppCard v-if="showFooter" class="flex-shrink-0 py-12">
|
<AppCard v-else-if="showFooter" class="flex-shrink-0 py-12">
|
||||||
<TheFooter />
|
<TheFooter />
|
||||||
</AppCard>
|
</AppCard>
|
||||||
</slot>
|
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<n-tooltip trigger="hover" placement="left">
|
<n-tooltip trigger="hover" placement="left">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<div id="layout-setting" class="f-c-c rounded-4 bg-primary p-8" @click="modalRef.open()">
|
<div class="f-c-c rounded-4 bg-primary p-8" @click="modalRef.open()">
|
||||||
<i class="i-fe:settings cursor-pointer bg-white text-20" />
|
<i class="i-fe:settings cursor-pointer bg-white text-20" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="f-c-c">
|
<n-tooltip trigger="hover">
|
||||||
<n-tooltip trigger="hover" placement="bottom">
|
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<div id="theme-setting" class="h-32 w-32">
|
|
||||||
<n-color-picker
|
<n-color-picker
|
||||||
|
class="mr-16 h-32 w-32"
|
||||||
:value="appStore.primaryColor"
|
:value="appStore.primaryColor"
|
||||||
:swatches="primaryColors"
|
:swatches="primaryColors"
|
||||||
:on-update:value="(v) => appStore.setPrimaryColor(v)"
|
:on-update:value="(v) => appStore.setPrimaryColor(v)"
|
||||||
:render-label="() => ''"
|
:render-label="() => ''"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
设置主题色
|
设置主题色
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { getPresetColors } from '@arco-design/color'
|
|
||||||
import { useAppStore } from '@/store'
|
import { useAppStore } from '@/store'
|
||||||
|
import { getPresetColors } from '@arco-design/color'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<i
|
<i
|
||||||
id="toggleTheme"
|
|
||||||
class="mr-16 cursor-pointer"
|
class="mr-16 cursor-pointer"
|
||||||
:class="isDark ? 'i-fe:moon' : 'i-fe:sun'"
|
:class="isDark ? 'i-fe:moon' : 'i-fe:sun'"
|
||||||
@click="toggleDark"
|
@click="toggleDark"
|
||||||
@@ -8,8 +7,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useDark, useToggle } from '@vueuse/core'
|
|
||||||
import { useAppStore } from '@/store'
|
import { useAppStore } from '@/store'
|
||||||
|
import { useDark, useToggle } from '@vueuse/core'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const isDark = useDark()
|
const isDark = useDark()
|
||||||
@@ -39,7 +38,6 @@ async function toggleDark({ clientX, clientY }) {
|
|||||||
duration: 500,
|
duration: 500,
|
||||||
easing: 'ease-in',
|
easing: 'ease-in',
|
||||||
pseudoElement: `::view-transition-${isDark.value ? 'old' : 'new'}(root)`,
|
pseudoElement: `::view-transition-${isDark.value ? 'old' : 'new'}(root)`,
|
||||||
fill: 'both',
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
--------------------------------->
|
--------------------------------->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full flex flex-col overflow-hidden">
|
|
||||||
<AppCard v-if="$slots.default" bordered bg="#fafafc dark:black" class="mb-30 min-h-60 rounded-4">
|
<AppCard v-if="$slots.default" bordered bg="#fafafc dark:black" class="mb-30 min-h-60 rounded-4">
|
||||||
<form class="flex justify-between p-16" @submit.prevent="handleSearch()">
|
<form class="flex justify-between p-16" @submit.prevent="handleSearch()">
|
||||||
<n-scrollbar x-scrollable>
|
<n-scrollbar x-scrollable>
|
||||||
@@ -47,12 +46,9 @@
|
|||||||
:data="tableData"
|
:data="tableData"
|
||||||
:row-key="(row) => row[rowKey]"
|
:row-key="(row) => row[rowKey]"
|
||||||
:pagination="isPagination ? pagination : false"
|
:pagination="isPagination ? pagination : false"
|
||||||
flex-height
|
|
||||||
class="flex-1"
|
|
||||||
@update:checked-row-keys="onChecked"
|
@update:checked-row-keys="onChecked"
|
||||||
@update:page="onPageChange"
|
@update:page="onPageChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -73,6 +69,13 @@ const props = defineProps({
|
|||||||
isPagination: {
|
isPagination: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @isShowSizePicker 是否显示每页条数选择
|
||||||
|
*/
|
||||||
|
isShowSizePicker: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
},
|
},
|
||||||
scrollX: {
|
scrollX: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -120,6 +123,16 @@ const pagination = reactive({
|
|||||||
prefix({ itemCount }) {
|
prefix({ itemCount }) {
|
||||||
return `共 ${itemCount} 条数据`
|
return `共 ${itemCount} 条数据`
|
||||||
},
|
},
|
||||||
|
pageSizes: [10, 20, 50, 100],
|
||||||
|
showSizePicker: props.isShowSizePicker,
|
||||||
|
onChange: (page) => {
|
||||||
|
pagination.page = page
|
||||||
|
},
|
||||||
|
onUpdatePageSize: (pageSize) => {
|
||||||
|
pagination.pageSize = pageSize
|
||||||
|
pagination.page = 1
|
||||||
|
handleQuery()
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 是否展开
|
// 是否展开
|
||||||
|
|||||||
@@ -26,8 +26,8 @@
|
|||||||
|
|
||||||
<!-- 底部按钮 -->
|
<!-- 底部按钮 -->
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<slot name="footer">
|
<slot v-if="$slots.footer" name="footer" />
|
||||||
<footer v-if="modalOptions.showFooter" class="flex justify-end">
|
<footer v-else-if="modalOptions.showFooter" class="flex justify-end">
|
||||||
<n-button v-if="modalOptions.showCancel" @click="handleCancel()">
|
<n-button v-if="modalOptions.showCancel" @click="handleCancel()">
|
||||||
{{ modalOptions.cancelText }}
|
{{ modalOptions.cancelText }}
|
||||||
</n-button>
|
</n-button>
|
||||||
@@ -41,7 +41,6 @@
|
|||||||
{{ modalOptions.okText }}
|
{{ modalOptions.okText }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</footer>
|
</footer>
|
||||||
</slot>
|
|
||||||
</template>
|
</template>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
import { cloneDeep } from 'lodash-es'
|
|
||||||
import { useForm, useModal } from '.'
|
import { useForm, useModal } from '.'
|
||||||
|
|
||||||
const ACTIONS = {
|
const ACTIONS = {
|
||||||
@@ -22,7 +21,7 @@ export function useCrud({ name, initForm = {}, doCreate, doDelete, doUpdate, ref
|
|||||||
|
|
||||||
/** 新增 */
|
/** 新增 */
|
||||||
function handleAdd(row = {}, title) {
|
function handleAdd(row = {}, title) {
|
||||||
handleOpen({ action: 'add', title, row: Object.assign({}, cloneDeep(initForm), cloneDeep(row)) })
|
handleOpen({ action: 'add', title, row: { ...initForm, ...row } })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 修改 */
|
/** 修改 */
|
||||||
|
|||||||
@@ -6,11 +6,9 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
import { cloneDeep } from 'lodash-es'
|
|
||||||
|
|
||||||
export function useForm(initFormData = {}) {
|
export function useForm(initFormData = {}) {
|
||||||
const formRef = ref(null)
|
const formRef = ref(null)
|
||||||
const formModel = ref(cloneDeep(initFormData))
|
const formModel = ref({ ...initFormData })
|
||||||
const rules = {
|
const rules = {
|
||||||
required: {
|
required: {
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
import { withDirectives } from 'vue'
|
|
||||||
import { router } from '@/router'
|
import { router } from '@/router'
|
||||||
|
|
||||||
const permission = {
|
const permission = {
|
||||||
@@ -22,17 +21,3 @@ const permission = {
|
|||||||
export function setupDirectives(app) {
|
export function setupDirectives(app) {
|
||||||
app.directive('permission', permission)
|
app.directive('permission', permission)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 用于h函数使用自定义权限指令
|
|
||||||
*
|
|
||||||
* @param {*} vnode 虚拟节点
|
|
||||||
* @param {*} code 权限码
|
|
||||||
* @returns 返回一个包含权限指令的vnode
|
|
||||||
*
|
|
||||||
* 使用示例:withPermission(h('button', {class: 'text-red-500'}, '删除'), 'user:delete')
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function withPermission(vnode, code) {
|
|
||||||
return withDirectives(vnode, [[permission, code]])
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-tooltip trigger="hover">
|
|
||||||
<template #trigger>
|
|
||||||
<i
|
|
||||||
class="i-fe:beginner mr-16 cursor-pointer text-20"
|
|
||||||
@click="show = true"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
操作指引
|
|
||||||
</n-tooltip>
|
|
||||||
|
|
||||||
<Vue3IntroStep
|
|
||||||
ref="myIntroStep"
|
|
||||||
v-model:show="show"
|
|
||||||
:config="config"
|
|
||||||
>
|
|
||||||
<template #prev="{ tipItem, index }">
|
|
||||||
<NButton class="mr-12" type="primary" color="#fff" text-color="#fff" ghost round size="small" @click="prev(tipItem, index)">
|
|
||||||
上一步
|
|
||||||
</NButton>
|
|
||||||
</template>
|
|
||||||
<template #next="{ tipItem }">
|
|
||||||
<NButton class="mr-12" type="primary" color="#fff" text-color="#fff" ghost round size="small" @click="next(tipItem)">
|
|
||||||
下一步
|
|
||||||
</NButton>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #skip>
|
|
||||||
<NButton type="primary" color="#fff" text-color="#fff" ghost round size="small" @click="skip">
|
|
||||||
跳过
|
|
||||||
</NButton>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #done>
|
|
||||||
<NButton type="primary" color="#fff" text-color="#fff" ghost round size="small" @click="done">
|
|
||||||
完成
|
|
||||||
</NButton>
|
|
||||||
</template>
|
|
||||||
</Vue3IntroStep>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import Vue3IntroStep from 'vue3-intro-step'
|
|
||||||
|
|
||||||
const myIntroStep = shallowRef(null)
|
|
||||||
const show = shallowRef(false)
|
|
||||||
const config = {
|
|
||||||
backgroundOpacity: 0.8,
|
|
||||||
titleStyle: {
|
|
||||||
textAlign: 'left',
|
|
||||||
fontSize: '18px',
|
|
||||||
},
|
|
||||||
contentStyle: {
|
|
||||||
textAlign: 'left',
|
|
||||||
fontSize: '14px',
|
|
||||||
},
|
|
||||||
tips: [
|
|
||||||
{
|
|
||||||
el: '#toggleTheme',
|
|
||||||
tipPosition: 'bottom',
|
|
||||||
title: '切换系统主题',
|
|
||||||
content: '一键开启护眼模式',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
el: '#fullscreen',
|
|
||||||
tipPosition: 'bottom',
|
|
||||||
title: '全屏/退出全屏',
|
|
||||||
content: '一键开启全屏',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
el: '#theme-setting',
|
|
||||||
tipPosition: 'bottom',
|
|
||||||
title: '设置主题色',
|
|
||||||
content: '调整为你喜欢的主题色',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
el: '#user-dropdown',
|
|
||||||
tipPosition: 'bottom',
|
|
||||||
title: '个人中心',
|
|
||||||
content: '查看个人资料和退出系统',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
el: '#menu-collapse',
|
|
||||||
tipPosition: 'bottom',
|
|
||||||
title: '展开/收起菜单',
|
|
||||||
content: '一键展开/收起菜单',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
el: '#top-tab',
|
|
||||||
tipPosition: 'bottom',
|
|
||||||
title: '标签栏',
|
|
||||||
content: '鼠标滚轮滑动可调整至最佳视野',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
el: '#layout-setting',
|
|
||||||
tipPosition: 'left',
|
|
||||||
title: '调整系统布局',
|
|
||||||
content: '将系统布局调整为你喜欢的样子',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
function skip() {
|
|
||||||
show.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function done() {
|
|
||||||
show.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function next() {
|
|
||||||
// tipItem当前的提示项信息
|
|
||||||
// 调用vue3-intro-step的next方法 手动触发下一步
|
|
||||||
myIntroStep.value.next()
|
|
||||||
}
|
|
||||||
function prev() {
|
|
||||||
// 调用vue3-intro-step的prev方法 手动触发上一步
|
|
||||||
myIntroStep.value.prev()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<i
|
<i
|
||||||
id="fullscreen"
|
|
||||||
class="mr-16 cursor-pointer"
|
class="mr-16 cursor-pointer"
|
||||||
:class="isFullscreen ? 'i-fe:minimize' : 'i-fe:maximize'"
|
:class="isFullscreen ? 'i-fe:minimize' : 'i-fe:maximize'"
|
||||||
@click="toggle"
|
@click="toggle"
|
||||||
|
|||||||
@@ -8,8 +8,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
id="menu-collapse"
|
class="f-c-c cursor-pointer rounded-4 p-6 text-22 transition-all-300 auto-bg-hover"
|
||||||
class="f-c-c cursor-pointer rounded-4 auto-bg-hover p-6 text-22 transition-all-300"
|
|
||||||
@click="appStore.switchCollapsed"
|
@click="appStore.switchCollapsed"
|
||||||
>
|
>
|
||||||
<i :class="appStore.collapsed ? 'i-line-md-menu-unfold-left' : 'i-line-md-menu-fold-left'" />
|
<i :class="appStore.collapsed ? 'i-line-md-menu-unfold-left' : 'i-line-md-menu-fold-left'" />
|
||||||
|
|||||||
@@ -54,14 +54,12 @@ function handleMenuSelect(key, item) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (!item.path)
|
|
||||||
return
|
|
||||||
router.push(item.path)
|
router.push(item.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
.side-menu:not(.n-menu--collapsed) {
|
.side-menu:not(.n-menu--collapsed) {
|
||||||
.n-menu-item-content {
|
.n-menu-item-content {
|
||||||
&::before {
|
&::before {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-dropdown :options="options" @select="handleSelect">
|
<n-dropdown :options="options" @select="handleSelect">
|
||||||
<div id="user-dropdown" class="flex cursor-pointer items-center">
|
<div class="flex cursor-pointer items-center">
|
||||||
<n-avatar round :size="36" :src="userStore.avatar" />
|
<n-avatar round :size="36" :src="userStore.avatar" />
|
||||||
<div v-if="userStore.userInfo" class="ml-12 flex-col flex-shrink-0 items-center">
|
<div v-if="userStore.userInfo" class="ml-12 flex-col flex-shrink-0 items-center">
|
||||||
<span class="text-14">{{ userStore.nickName ?? userStore.username }}</span>
|
<span class="text-14">{{ userStore.nickName ?? userStore.username }}</span>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export { default as BeginnerGuide } from './BeginnerGuide.vue'
|
|
||||||
export { default as BreadCrumb } from './BreadCrumb.vue'
|
export { default as BreadCrumb } from './BreadCrumb.vue'
|
||||||
export { default as Fullscreen } from './Fullscreen.vue'
|
export { default as Fullscreen } from './Fullscreen.vue'
|
||||||
export { default as MenuCollapse } from './MenuCollapse.vue'
|
export { default as MenuCollapse } from './MenuCollapse.vue'
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
--------------------------------->
|
--------------------------------->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="top-tab">
|
<div>
|
||||||
<n-tabs
|
<n-tabs
|
||||||
:value="tabStore.activeTab"
|
:value="tabStore.activeTab"
|
||||||
:closable="tabStore.tabs.length > 1"
|
:closable="tabStore.tabs.length > 1"
|
||||||
@@ -74,7 +74,7 @@ async function handleContextMenu(e, tagItem) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="scss">
|
||||||
:deep(.n-tabs) {
|
:deep(.n-tabs) {
|
||||||
.n-tabs-tab {
|
.n-tabs-tab {
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
|
|||||||
@@ -13,8 +13,6 @@
|
|||||||
<BreadCrumb />
|
<BreadCrumb />
|
||||||
|
|
||||||
<div class="ml-auto flex flex-shrink-0 items-center px-12 text-18">
|
<div class="ml-auto flex flex-shrink-0 items-center px-12 text-18">
|
||||||
<BeginnerGuide />
|
|
||||||
|
|
||||||
<ToggleTheme />
|
<ToggleTheme />
|
||||||
|
|
||||||
<Fullscreen />
|
<Fullscreen />
|
||||||
@@ -37,7 +35,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ToggleTheme } from '@/components'
|
import { ToggleTheme } from '@/components'
|
||||||
import { BeginnerGuide, BreadCrumb, Fullscreen, MenuCollapse, UserAvatar } from '@/layouts/components'
|
import { BreadCrumb, Fullscreen, MenuCollapse, UserAvatar } from '@/layouts/components'
|
||||||
|
|
||||||
function handleLinkClick(link) {
|
function handleLinkClick(link) {
|
||||||
window.open(link)
|
window.open(link)
|
||||||
|
|||||||
@@ -15,8 +15,6 @@
|
|||||||
<span class="mx-6 opacity-20">|</span>
|
<span class="mx-6 opacity-20">|</span>
|
||||||
|
|
||||||
<div class="flex flex-shrink-0 items-center px-12 text-18">
|
<div class="flex flex-shrink-0 items-center px-12 text-18">
|
||||||
<BeginnerGuide />
|
|
||||||
|
|
||||||
<ToggleTheme />
|
<ToggleTheme />
|
||||||
|
|
||||||
<Fullscreen />
|
<Fullscreen />
|
||||||
@@ -39,7 +37,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ToggleTheme } from '@/components'
|
import { ToggleTheme } from '@/components'
|
||||||
import { AppTab, BeginnerGuide, Fullscreen, MenuCollapse, UserAvatar } from '@/layouts/components'
|
import { AppTab, Fullscreen, MenuCollapse, UserAvatar } from '@/layouts/components'
|
||||||
|
|
||||||
function handleLinkClick(link) {
|
function handleLinkClick(link) {
|
||||||
window.open(link)
|
window.open(link)
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import { setupDirectives } from './directives'
|
|||||||
import { setupRouter } from './router'
|
import { setupRouter } from './router'
|
||||||
import { setupStore } from './store'
|
import { setupStore } from './store'
|
||||||
import { setupNaiveDiscreteApi } from './utils'
|
import { setupNaiveDiscreteApi } from './utils'
|
||||||
|
import '@/styles/global.scss'
|
||||||
import '@/styles/reset.css'
|
import '@/styles/reset.css'
|
||||||
import '@/styles/global.css'
|
|
||||||
import 'uno.css'
|
import 'uno.css'
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function createPermissionGuard(router) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const routes = router.getRoutes()
|
const routes = router.getRoutes()
|
||||||
if (routes.some(route => route.name === to.name))
|
if (routes.find(route => route.name === to.name))
|
||||||
return true
|
return true
|
||||||
|
|
||||||
// 判断是无权限还是404
|
// 判断是无权限还是404
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { cloneDeep } from 'lodash-es'
|
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { basePermissions } from '@/settings'
|
import { basePermissions } from '@/settings'
|
||||||
|
|
||||||
@@ -27,5 +26,5 @@ export async function getPermissions() {
|
|||||||
catch (error) {
|
catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
return cloneDeep(basePermissions).concat(asyncPermissions)
|
return basePermissions.concat(asyncPermissions)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
|
import { defaultLayout, defaultPrimaryColor, naiveThemeOverrides } from '@/settings'
|
||||||
import { generate, getRgbStr } from '@arco-design/color'
|
import { generate, getRgbStr } from '@arco-design/color'
|
||||||
import { useDark } from '@vueuse/core'
|
import { useDark } from '@vueuse/core'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { defaultLayout, defaultPrimaryColor, naiveThemeOverrides } from '@/settings'
|
|
||||||
|
|
||||||
export const useAppStore = defineStore('app', {
|
export const useAppStore = defineStore('app', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@@ -50,7 +50,7 @@ export const useAppStore = defineStore('app', {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
persist: {
|
persist: {
|
||||||
pick: ['collapsed', 'layout', 'primaryColor', 'naiveThemeOverrides'],
|
paths: ['collapsed', 'layout', 'primaryColor', 'naiveThemeOverrides'],
|
||||||
storage: sessionStorage,
|
storage: sessionStorage,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
import { defineStore } from 'pinia'
|
|
||||||
import { usePermissionStore, useRouterStore, useTabStore, useUserStore } from '@/store'
|
import { usePermissionStore, useRouterStore, useTabStore, useUserStore } from '@/store'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', {
|
export const useAuthStore = defineStore('auth', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
|
import { isExternal } from '@/utils'
|
||||||
import { hyphenate } from '@vueuse/core'
|
import { hyphenate } from '@vueuse/core'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { isExternal } from '@/utils'
|
|
||||||
|
|
||||||
export const usePermissionStore = defineStore('permission', {
|
export const usePermissionStore = defineStore('permission', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@@ -29,6 +29,8 @@ export const usePermissionStore = defineStore('permission', {
|
|||||||
const route = this.generateRoute(item, item.show ? null : parent?.key)
|
const route = this.generateRoute(item, item.show ? null : parent?.key)
|
||||||
if (item.enable && route.path && !route.path.startsWith('http'))
|
if (item.enable && route.path && !route.path.startsWith('http'))
|
||||||
this.accessRoutes.push(route)
|
this.accessRoutes.push(route)
|
||||||
|
if (!item.show)
|
||||||
|
return null
|
||||||
const menuItem = {
|
const menuItem = {
|
||||||
label: route.meta.title,
|
label: route.meta.title,
|
||||||
key: route.name,
|
key: route.name,
|
||||||
@@ -46,8 +48,6 @@ export const usePermissionStore = defineStore('permission', {
|
|||||||
if (!menuItem.children.length)
|
if (!menuItem.children.length)
|
||||||
delete menuItem.children
|
delete menuItem.children
|
||||||
}
|
}
|
||||||
if (!item.show)
|
|
||||||
return null
|
|
||||||
return menuItem
|
return menuItem
|
||||||
},
|
},
|
||||||
generateRoute(item, parentKey) {
|
generateRoute(item, parentKey) {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const useTabStore = defineStore('tab', {
|
|||||||
const curIndex = this.tabs.findIndex(item => item.path === curPath)
|
const curIndex = this.tabs.findIndex(item => item.path === curPath)
|
||||||
const filterTabs = this.tabs.filter((item, index) => index >= curIndex)
|
const filterTabs = this.tabs.filter((item, index) => index >= curIndex)
|
||||||
this.setTabs(filterTabs)
|
this.setTabs(filterTabs)
|
||||||
if (!filterTabs.some(item => item.path === this.activeTab)) {
|
if (!filterTabs.find(item => item.path === this.activeTab)) {
|
||||||
useRouterStore().router?.push(filterTabs[filterTabs.length - 1].path)
|
useRouterStore().router?.push(filterTabs[filterTabs.length - 1].path)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -79,7 +79,7 @@ export const useTabStore = defineStore('tab', {
|
|||||||
const curIndex = this.tabs.findIndex(item => item.path === curPath)
|
const curIndex = this.tabs.findIndex(item => item.path === curPath)
|
||||||
const filterTabs = this.tabs.filter((item, index) => index <= curIndex)
|
const filterTabs = this.tabs.filter((item, index) => index <= curIndex)
|
||||||
this.setTabs(filterTabs)
|
this.setTabs(filterTabs)
|
||||||
if (!filterTabs.some(item => item.path === this.activeTab.value)) {
|
if (!filterTabs.find(item => item.path === this.activeTab.value)) {
|
||||||
useRouterStore().router?.push(filterTabs[filterTabs.length - 1].path)
|
useRouterStore().router?.push(filterTabs[filterTabs.length - 1].path)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -88,7 +88,7 @@ export const useTabStore = defineStore('tab', {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
persist: {
|
persist: {
|
||||||
pick: ['tabs'],
|
paths: ['tabs'],
|
||||||
storage: sessionStorage,
|
storage: sessionStorage,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ body {
|
|||||||
|
|
||||||
.fade-slide-enter-from {
|
.fade-slide-enter-from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-2%);
|
transform: translateX(-30px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-slide-leave-to {
|
.fade-slide-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(2%);
|
transform: translateX(30px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 自定义滚动条样式 */
|
/* 自定义滚动条样式 */
|
||||||
@@ -34,8 +34,8 @@ html,
|
|||||||
-webkit-text-size-adjust: 100%; /* 2 */
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
-moz-tab-size: 4; /* 3 */
|
-moz-tab-size: 4; /* 3 */
|
||||||
tab-size: 4; /* 3 */
|
tab-size: 4; /* 3 */
|
||||||
font-family:
|
font-family: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||||
ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; /* 4 */
|
'Noto Color Emoji'; /* 4 */
|
||||||
font-feature-settings: normal; /* 5 */
|
font-feature-settings: normal; /* 5 */
|
||||||
font-variation-settings: normal; /* 6 */
|
font-variation-settings: normal; /* 6 */
|
||||||
-webkit-tap-highlight-color: transparent; /* 7 */
|
-webkit-tap-highlight-color: transparent; /* 7 */
|
||||||
@@ -114,8 +114,7 @@ code,
|
|||||||
kbd,
|
kbd,
|
||||||
samp,
|
samp,
|
||||||
pre {
|
pre {
|
||||||
font-family:
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; /* 1 */
|
||||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; /* 1 */
|
|
||||||
font-feature-settings: normal; /* 2 */
|
font-feature-settings: normal; /* 2 */
|
||||||
font-variation-settings: normal; /* 3 */
|
font-variation-settings: normal; /* 3 */
|
||||||
font-size: 1em; /* 4 */
|
font-size: 1em; /* 4 */
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function throttle(fn, wait) {
|
|||||||
let previous = 0
|
let previous = 0
|
||||||
|
|
||||||
return function (...argArr) {
|
return function (...argArr) {
|
||||||
const now = Date.now()
|
const now = +new Date()
|
||||||
context = this
|
context = this
|
||||||
args = argArr
|
args = argArr
|
||||||
if (now - previous > wait) {
|
if (now - previous > wait) {
|
||||||
|
|||||||
@@ -10,15 +10,16 @@
|
|||||||
import { useAuthStore } from '@/store'
|
import { useAuthStore } from '@/store'
|
||||||
|
|
||||||
let isConfirming = false
|
let isConfirming = false
|
||||||
|
export function resolveResError(code, message, needTip = true) {
|
||||||
function handleAuthExpired(content, needTip) {
|
switch (code) {
|
||||||
|
case 401:
|
||||||
if (isConfirming || !needTip)
|
if (isConfirming || !needTip)
|
||||||
return
|
return
|
||||||
isConfirming = true
|
isConfirming = true
|
||||||
$dialog.confirm({
|
$dialog.confirm({
|
||||||
title: '提示',
|
title: '提示',
|
||||||
type: 'info',
|
type: 'info',
|
||||||
content,
|
content: '登录已过期,是否重新登录?',
|
||||||
confirm() {
|
confirm() {
|
||||||
useAuthStore().logout()
|
useAuthStore().logout()
|
||||||
window.$message?.success('已退出登录')
|
window.$message?.success('已退出登录')
|
||||||
@@ -29,15 +30,25 @@ function handleAuthExpired(content, needTip) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
return false
|
return false
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveResError(code, message, needTip = true) {
|
|
||||||
switch (code) {
|
|
||||||
case 401:
|
|
||||||
return handleAuthExpired('登录已过期,是否重新登录?', needTip)
|
|
||||||
case 11007:
|
case 11007:
|
||||||
case 11008:
|
case 11008:
|
||||||
return handleAuthExpired(`${message},是否重新登录?`, needTip)
|
if (isConfirming || !needTip)
|
||||||
|
return
|
||||||
|
isConfirming = true
|
||||||
|
$dialog.confirm({
|
||||||
|
title: '提示',
|
||||||
|
type: 'info',
|
||||||
|
content: `${message},是否重新登录?`,
|
||||||
|
confirm() {
|
||||||
|
useAuthStore().logout()
|
||||||
|
window.$message?.success('已退出登录')
|
||||||
|
isConfirming = false
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
isConfirming = false
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return false
|
||||||
case 403:
|
case 403:
|
||||||
message = '请求被拒绝'
|
message = '请求被拒绝'
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
import * as NaiveUI from 'naive-ui'
|
|
||||||
import { useAppStore } from '@/store'
|
import { useAppStore } from '@/store'
|
||||||
import { isNullOrUndef } from '@/utils'
|
import { isNullOrUndef } from '@/utils'
|
||||||
|
import * as NaiveUI from 'naive-ui'
|
||||||
|
|
||||||
export function setupMessage(NMessage) {
|
export function setupMessage(NMessage) {
|
||||||
class Message {
|
class Message {
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ import { createStorage } from './storage'
|
|||||||
|
|
||||||
const prefixKey = 'vue-naive-admin_'
|
const prefixKey = 'vue-naive-admin_'
|
||||||
|
|
||||||
export function createLocalStorage(option = {}) {
|
export const createLocalStorage = function (option = {}) {
|
||||||
return createStorage({
|
return createStorage({
|
||||||
prefixKey: option.prefixKey || '',
|
prefixKey: option.prefixKey || '',
|
||||||
storage: localStorage,
|
storage: localStorage,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSessionStorage(option = {}) {
|
export const createSessionStorage = function (option = {}) {
|
||||||
return createStorage({
|
return createStorage({
|
||||||
prefixKey: option.prefixKey || '',
|
prefixKey: option.prefixKey || '',
|
||||||
storage: sessionStorage,
|
storage: sessionStorage,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class Storage {
|
|||||||
const stringData = JSON.stringify({
|
const stringData = JSON.stringify({
|
||||||
value,
|
value,
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
expire: !isNullOrUndef(expire) ? Date.now() + expire * 1000 : null,
|
expire: !isNullOrUndef(expire) ? new Date().getTime() + expire * 1000 : null,
|
||||||
})
|
})
|
||||||
this.storage.setItem(this.getKey(key), stringData)
|
this.storage.setItem(this.getKey(key), stringData)
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ class Storage {
|
|||||||
try {
|
try {
|
||||||
const data = JSON.parse(val)
|
const data = JSON.parse(val)
|
||||||
const { value, time, expire } = data
|
const { value, time, expire } = data
|
||||||
if (isNullOrUndef(expire) || expire > Date.now()) {
|
if (isNullOrUndef(expire) || expire > new Date().getTime()) {
|
||||||
return { value, time }
|
return { value, time }
|
||||||
}
|
}
|
||||||
this.remove(key)
|
this.remove(key)
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { sleep } from '@/utils'
|
import { sleep } from '@/utils'
|
||||||
|
|
||||||
function handleDelete() {
|
const handleDelete = function () {
|
||||||
$dialog.confirm({
|
$dialog.confirm({
|
||||||
content: '确认删除?',
|
content: '确认删除?',
|
||||||
confirm() {
|
confirm() {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<li
|
<li
|
||||||
v-for="item in icons"
|
v-for="item in icons"
|
||||||
:key="item"
|
:key="item"
|
||||||
class="w-160 f-c-c flex-col cursor-pointer card-border rounded-12 auto-bg px-12 py-24"
|
class="w-160 f-c-c flex-col cursor-pointer rounded-12 px-12 py-24 card-border auto-bg"
|
||||||
@click="copy(`<i class="${item}" />`)"
|
@click="copy(`<i class="${item}" />`)"
|
||||||
>
|
>
|
||||||
<i :class="`${item}?mask`" class="text-28 text-gray-600 hover:bg-primary" />
|
<i :class="`${item}?mask`" class="text-28 text-gray-600 hover:bg-primary" />
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div mt-20 w-350 f-c-c flex-col>
|
<div mt-20 w-350 f-c-c flex-col>
|
||||||
<div flex flex-wrap justify-around card-border rounded-10 p-10>
|
<div flex flex-wrap justify-around rounded-10 p-10 card-border>
|
||||||
<div m-20 h-50 w-50 f-c-c rounded-5 p-10 border="1 solid">
|
<div m-20 h-50 w-50 f-c-c rounded-5 p-10 border="1 solid">
|
||||||
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
<span h-6 w-6 rounded-3 bg-black dark:bg-white />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,523 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { useClipboard, useStorage } from '@vueuse/core'
|
|
||||||
import {
|
|
||||||
API_BASE_URL_OPTIONS,
|
|
||||||
DEFAULT_SETTINGS,
|
|
||||||
EXPERIENCE_KEY_MAX_TEXT_LENGTH,
|
|
||||||
isExperienceApiKey,
|
|
||||||
isSettingsReady,
|
|
||||||
MODEL_OPTIONS,
|
|
||||||
STORE_KEY,
|
|
||||||
translateText,
|
|
||||||
} from './translate'
|
|
||||||
|
|
||||||
defineOptions({ name: 'DemoTranslate' })
|
|
||||||
|
|
||||||
const { copy, copied } = useClipboard()
|
|
||||||
|
|
||||||
const sourceText = ref('')
|
|
||||||
const resultText = ref('')
|
|
||||||
const errorText = ref('')
|
|
||||||
const loading = ref(false)
|
|
||||||
const showSettings = ref(false)
|
|
||||||
const runApiUrl = 'https://runapi.co/register?aff=Da0v'
|
|
||||||
|
|
||||||
const settings = useStorage(STORE_KEY, { ...DEFAULT_SETTINGS })
|
|
||||||
const draftSettings = reactive({ ...settings.value })
|
|
||||||
|
|
||||||
const canTranslate = computed(() => Boolean(sourceText.value.trim()) && !loading.value)
|
|
||||||
const apiReady = computed(() => isSettingsReady(settings.value))
|
|
||||||
const isUsingExperienceKey = computed(() => isExperienceApiKey(settings.value.apiKey))
|
|
||||||
const isDraftExperienceKey = computed(() => isExperienceApiKey(draftSettings.apiKey))
|
|
||||||
const sourceLength = computed(() => sourceText.value.length)
|
|
||||||
const sourceLimitText = computed(() => `${sourceLength.value}/${EXPERIENCE_KEY_MAX_TEXT_LENGTH}`)
|
|
||||||
const footerText = computed(() => {
|
|
||||||
if (!apiReady.value)
|
|
||||||
return '请先完善模型配置'
|
|
||||||
if (isUsingExperienceKey.value)
|
|
||||||
return '当前是体验 Key,所有人每天共享使用约 1000 次翻译'
|
|
||||||
return '已就绪'
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(sourceText, (value) => {
|
|
||||||
if (isUsingExperienceKey.value && value.length > EXPERIENCE_KEY_MAX_TEXT_LENGTH)
|
|
||||||
sourceText.value = value.slice(0, EXPERIENCE_KEY_MAX_TEXT_LENGTH)
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(copied, (value) => {
|
|
||||||
if (value)
|
|
||||||
$message.success('已复制译文')
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (!apiReady.value)
|
|
||||||
showSettings.value = true
|
|
||||||
})
|
|
||||||
|
|
||||||
function syncDraftSettings() {
|
|
||||||
Object.assign(draftSettings, settings.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function openSettings() {
|
|
||||||
syncDraftSettings()
|
|
||||||
showSettings.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveSettings() {
|
|
||||||
if (!isSettingsReady(draftSettings))
|
|
||||||
return
|
|
||||||
|
|
||||||
settings.value = {
|
|
||||||
apiKey: draftSettings.apiKey.trim(),
|
|
||||||
apiBaseUrl: draftSettings.apiBaseUrl.trim().replace(/\/+$/, ''),
|
|
||||||
model: draftSettings.model.trim(),
|
|
||||||
}
|
|
||||||
showSettings.value = false
|
|
||||||
errorText.value = ''
|
|
||||||
$message.success('配置已保存')
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectModel(model) {
|
|
||||||
draftSettings.model = model
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleTranslate() {
|
|
||||||
const text = sourceText.value.trim()
|
|
||||||
if (!text || loading.value)
|
|
||||||
return
|
|
||||||
|
|
||||||
resultText.value = ''
|
|
||||||
errorText.value = ''
|
|
||||||
|
|
||||||
if (!apiReady.value) {
|
|
||||||
syncDraftSettings()
|
|
||||||
showSettings.value = true
|
|
||||||
errorText.value = '请先保存模型配置'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
resultText.value = await translateText(text, settings.value)
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
errorText.value = error?.message || '翻译失败'
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleCopy() {
|
|
||||||
if (!resultText.value)
|
|
||||||
return
|
|
||||||
await copy(resultText.value)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CommonPage>
|
|
||||||
<div class="translate-page">
|
|
||||||
<div class="translate-toolbar">
|
|
||||||
<div class="min-w-0 flex items-center gap-12">
|
|
||||||
<div class="tool-icon">
|
|
||||||
<i class="i-fe:globe" />
|
|
||||||
</div>
|
|
||||||
<div class="min-w-0">
|
|
||||||
<div class="flex items-center gap-10">
|
|
||||||
<h3 class="m-0 text-18 font-600">
|
|
||||||
AI 翻译
|
|
||||||
</h3>
|
|
||||||
<n-tag size="small" round :type="apiReady ? 'success' : 'warning'">
|
|
||||||
{{ settings.model || '未配置模型' }}
|
|
||||||
</n-tag>
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 truncate text-13 text-#7a807f">
|
|
||||||
{{ footerText }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-10">
|
|
||||||
<a
|
|
||||||
v-if="isUsingExperienceKey"
|
|
||||||
class="runapi-badge"
|
|
||||||
:href="runApiUrl"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
title="打开 RunAPI"
|
|
||||||
>
|
|
||||||
<i class="i-fe:zap" />
|
|
||||||
<span>默认体验模型由中转站平台 RunAPI 提供支持</span>
|
|
||||||
<i class="i-fe:external-link" />
|
|
||||||
</a>
|
|
||||||
<n-button quaternary circle title="模型配置" @click="openSettings">
|
|
||||||
<template #icon>
|
|
||||||
<i class="i-fe:settings" />
|
|
||||||
</template>
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="translate-workspace">
|
|
||||||
<section class="translate-panel">
|
|
||||||
<div class="panel-head">
|
|
||||||
<div>
|
|
||||||
<div class="text-14 font-600">
|
|
||||||
原文
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 text-12 text-#9aa0a0">
|
|
||||||
Enter 翻译
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-10">
|
|
||||||
<n-tag v-if="isUsingExperienceKey" size="small" :bordered="false">
|
|
||||||
{{ sourceLimitText }}
|
|
||||||
</n-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="source-input-wrap">
|
|
||||||
<n-input
|
|
||||||
v-model:value="sourceText"
|
|
||||||
class="translate-input"
|
|
||||||
type="textarea"
|
|
||||||
:maxlength="isUsingExperienceKey ? EXPERIENCE_KEY_MAX_TEXT_LENGTH : undefined"
|
|
||||||
:placeholder="isUsingExperienceKey ? `请输入要翻译的内容(体验 Key 最多 ${EXPERIENCE_KEY_MAX_TEXT_LENGTH} 字符)` : '请输入要翻译的内容'"
|
|
||||||
@keydown.enter.exact.prevent="handleTranslate"
|
|
||||||
/>
|
|
||||||
<n-button
|
|
||||||
class="translate-action"
|
|
||||||
type="primary"
|
|
||||||
:loading="loading"
|
|
||||||
:disabled="!canTranslate"
|
|
||||||
@click="handleTranslate"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<i class="i-fe:send" />
|
|
||||||
</template>
|
|
||||||
翻译
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="translate-panel">
|
|
||||||
<div class="panel-head">
|
|
||||||
<div>
|
|
||||||
<div class="text-14 font-600">
|
|
||||||
译文
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 text-12 text-#9aa0a0">
|
|
||||||
自动识别中英方向
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<n-button quaternary circle :disabled="!resultText" title="复制译文" @click="handleCopy">
|
|
||||||
<template #icon>
|
|
||||||
<i :class="copied ? 'i-fe:check' : 'i-fe:copy'" />
|
|
||||||
</template>
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="result-box">
|
|
||||||
<div v-if="errorText" class="result-error">
|
|
||||||
<i class="i-fe:alert-circle text-22" />
|
|
||||||
<span>{{ errorText }}</span>
|
|
||||||
</div>
|
|
||||||
<span v-else-if="resultText" class="whitespace-pre-wrap break-words">{{ resultText }}</span>
|
|
||||||
<div v-else class="result-empty">
|
|
||||||
<i class="i-fe:message-square text-34" />
|
|
||||||
<span>翻译结果会显示在这里</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<n-drawer v-model:show="showSettings" :width="420" placement="right">
|
|
||||||
<n-drawer-content title="模型配置" closable>
|
|
||||||
<div class="flex flex-col gap-16">
|
|
||||||
<n-alert v-if="isDraftExperienceKey" type="warning" :show-icon="false">
|
|
||||||
当前是体验 Key,所有人每天共享使用约 1000 次翻译,单次最多支持 {{ EXPERIENCE_KEY_MAX_TEXT_LENGTH }} 字符。
|
|
||||||
</n-alert>
|
|
||||||
|
|
||||||
<a class="runapi-card" :href="runApiUrl" target="_blank" rel="noopener noreferrer">
|
|
||||||
<span class="min-w-0 flex-1">
|
|
||||||
<strong>默认体验模型由中转站平台 RunAPI 提供支持</strong>
|
|
||||||
<span>点击获取稳定、快速且实惠的模型 API 服务</span>
|
|
||||||
</span>
|
|
||||||
<i class="i-fe:external-link" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class="mb-8 text-13 text-#5f6665">
|
|
||||||
API Key
|
|
||||||
</div>
|
|
||||||
<n-input
|
|
||||||
v-model:value="draftSettings.apiKey"
|
|
||||||
type="password"
|
|
||||||
show-password-on="click"
|
|
||||||
placeholder="sk-..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class="mb-8 text-13 text-#5f6665">
|
|
||||||
API Base URL
|
|
||||||
</div>
|
|
||||||
<n-auto-complete
|
|
||||||
v-model:value="draftSettings.apiBaseUrl"
|
|
||||||
:options="API_BASE_URL_OPTIONS.filter(item => item.includes(draftSettings.apiBaseUrl)).map(value => ({ label: value, value }))"
|
|
||||||
:get-show="() => true"
|
|
||||||
placeholder="例如:https://runapi.co/v1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class="mb-8 text-13 text-#5f6665">
|
|
||||||
Model
|
|
||||||
</div>
|
|
||||||
<n-auto-complete
|
|
||||||
v-model:value="draftSettings.model"
|
|
||||||
:options="MODEL_OPTIONS.filter(item => item.includes(draftSettings.model)).map(value => ({ label: value, value }))"
|
|
||||||
:get-show="() => true"
|
|
||||||
placeholder="例如:gpt-5.4-mini"
|
|
||||||
/>
|
|
||||||
<div class="mt-10 flex flex-wrap gap-8">
|
|
||||||
<n-tag
|
|
||||||
v-for="model in MODEL_OPTIONS"
|
|
||||||
:key="model"
|
|
||||||
class="cursor-pointer"
|
|
||||||
size="small"
|
|
||||||
:type="draftSettings.model === model ? 'primary' : 'default'"
|
|
||||||
round
|
|
||||||
@click="selectModel(model)"
|
|
||||||
>
|
|
||||||
{{ model }}
|
|
||||||
</n-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-end gap-10">
|
|
||||||
<n-button @click="showSettings = false">
|
|
||||||
取消
|
|
||||||
</n-button>
|
|
||||||
<n-button type="primary" :disabled="!isSettingsReady(draftSettings)" @click="saveSettings">
|
|
||||||
保存配置
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</n-drawer-content>
|
|
||||||
</n-drawer>
|
|
||||||
</CommonPage>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.translate-page {
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.translate-toolbar {
|
|
||||||
min-height: 70px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 0 4px 16px;
|
|
||||||
border-bottom: 1px solid rgba(239, 239, 245, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-icon {
|
|
||||||
width: 42px;
|
|
||||||
height: 42px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
flex: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: rgb(var(--primary-color));
|
|
||||||
background: rgba(var(--primary-color), 0.09);
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.runapi-badge {
|
|
||||||
height: 34px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 0 12px;
|
|
||||||
border: 1px solid rgba(var(--primary-color), 0.22);
|
|
||||||
border-radius: 8px;
|
|
||||||
color: rgb(var(--primary-color));
|
|
||||||
background: rgba(var(--primary-color), 0.08);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
white-space: nowrap;
|
|
||||||
transition:
|
|
||||||
background 0.2s ease,
|
|
||||||
border-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.runapi-badge:hover {
|
|
||||||
border-color: rgba(var(--primary-color), 0.38);
|
|
||||||
background: rgba(var(--primary-color), 0.13);
|
|
||||||
}
|
|
||||||
|
|
||||||
.runapi-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 14px;
|
|
||||||
border: 1px solid rgba(var(--primary-color), 0.18);
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #1f2322;
|
|
||||||
background: linear-gradient(135deg, rgba(var(--primary-color), 0.1), rgba(255, 255, 255, 0.9));
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.runapi-card strong,
|
|
||||||
.runapi-card span span {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.runapi-card strong {
|
|
||||||
color: rgb(var(--primary-color));
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.runapi-card span span {
|
|
||||||
margin-top: 4px;
|
|
||||||
color: #7a807f;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.runapi-card-icon {
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
flex: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: rgb(var(--primary-color));
|
|
||||||
background: rgba(var(--primary-color), 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.translate-workspace {
|
|
||||||
min-height: 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.translate-panel {
|
|
||||||
min-height: 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
|
||||||
border: 1px solid #efeff5;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-head {
|
|
||||||
min-height: 68px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 14px 16px;
|
|
||||||
border-bottom: 1px solid #f1f1f4;
|
|
||||||
background: #fbfcfc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.translate-input {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.source-input-wrap {
|
|
||||||
position: relative;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.translate-input :deep(.n-input),
|
|
||||||
.translate-input :deep(.n-input-wrapper),
|
|
||||||
.translate-input :deep(.n-input__textarea),
|
|
||||||
.translate-input :deep(.n-input__textarea-el) {
|
|
||||||
height: 100%;
|
|
||||||
resize: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.translate-input :deep(textarea),
|
|
||||||
.translate-input :deep(.n-input__textarea-el) {
|
|
||||||
resize: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.translate-input :deep(.n-input) {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.translate-input :deep(.n-input__textarea-el) {
|
|
||||||
padding-bottom: 68px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.translate-input :deep(.n-input__border),
|
|
||||||
.translate-input :deep(.n-input__state-border) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.translate-action {
|
|
||||||
position: absolute;
|
|
||||||
right: 16px;
|
|
||||||
bottom: 16px;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-box {
|
|
||||||
min-height: 0;
|
|
||||||
overflow: auto;
|
|
||||||
padding: 18px;
|
|
||||||
color: #1f2322;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1.8;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-empty {
|
|
||||||
height: 100%;
|
|
||||||
min-height: 220px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
color: #a0a5a4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-error {
|
|
||||||
min-height: 220px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
color: #d03050;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 960px) {
|
|
||||||
.translate-toolbar {
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.translate-workspace {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.runapi-badge {
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
export const STORE_KEY = 'demo-translate-settings'
|
|
||||||
|
|
||||||
export const API_BASE_URL_OPTIONS = [
|
|
||||||
'https://runapi.co/v1',
|
|
||||||
]
|
|
||||||
|
|
||||||
export const MODEL_OPTIONS = [
|
|
||||||
'gpt-5.4-mini',
|
|
||||||
'gpt-5.2',
|
|
||||||
'gpt-5.4',
|
|
||||||
'gpt-5.5',
|
|
||||||
'deepseek-v4-flash',
|
|
||||||
'deepseek-v4-pro',
|
|
||||||
]
|
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS = {
|
|
||||||
apiKey: 'sk-17UVTOjYEB3M9Erg32yGdEhMVFKEaALuIkjy1CkGrCUdiQqJ',
|
|
||||||
apiBaseUrl: API_BASE_URL_OPTIONS[0],
|
|
||||||
model: MODEL_OPTIONS[0],
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EXPERIENCE_KEY_MAX_TEXT_LENGTH = 100
|
|
||||||
|
|
||||||
const RUNAPI_PROXY_BASE_URL = '/runapi'
|
|
||||||
|
|
||||||
const TRANSLATION_RULES = 'Translate directly. Return only the translation. Preserve formatting, markdown, code, URLs, numbers, placeholders, and line breaks.'
|
|
||||||
|
|
||||||
export function isSettingsReady(target) {
|
|
||||||
return Boolean(target.apiKey?.trim() && target.apiBaseUrl?.trim() && target.model?.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isExperienceApiKey(apiKey) {
|
|
||||||
return apiKey?.trim() === DEFAULT_SETTINGS.apiKey
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateTranslateText(text, settings) {
|
|
||||||
if (isExperienceApiKey(settings.apiKey) && text.length > EXPERIENCE_KEY_MAX_TEXT_LENGTH)
|
|
||||||
throw new Error(`体验 Key 最多支持 ${EXPERIENCE_KEY_MAX_TEXT_LENGTH} 字符`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function countMatches(text, pattern) {
|
|
||||||
return text.match(pattern)?.length || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectSourceLanguage(text) {
|
|
||||||
const cjkCount = countMatches(text, /[\u3400-\u9FFF]/g)
|
|
||||||
const latinCount = countMatches(text, /[A-Z]/gi)
|
|
||||||
|
|
||||||
if (!cjkCount && !latinCount)
|
|
||||||
return 'auto'
|
|
||||||
if (!cjkCount)
|
|
||||||
return 'en'
|
|
||||||
if (!latinCount)
|
|
||||||
return 'zh'
|
|
||||||
|
|
||||||
const cjkRatio = cjkCount / (cjkCount + latinCount)
|
|
||||||
if (cjkRatio >= 0.2)
|
|
||||||
return 'zh'
|
|
||||||
if (cjkRatio <= 0.08)
|
|
||||||
return 'en'
|
|
||||||
|
|
||||||
const firstScript = text.match(/[\u3400-\u9FFFA-Z]/i)?.[0]
|
|
||||||
return firstScript && /[\u3400-\u9FFF]/.test(firstScript) ? 'zh' : 'en'
|
|
||||||
}
|
|
||||||
|
|
||||||
function createSystemPrompt(sourceLanguage) {
|
|
||||||
if (sourceLanguage === 'zh')
|
|
||||||
return `${TRANSLATION_RULES} Target: natural English.`
|
|
||||||
if (sourceLanguage === 'en')
|
|
||||||
return `${TRANSLATION_RULES} Target: natural Simplified Chinese.`
|
|
||||||
return `${TRANSLATION_RULES} Target: Simplified Chinese.`
|
|
||||||
}
|
|
||||||
|
|
||||||
function createFastModelOptions(model) {
|
|
||||||
const normalized = model.trim().toLowerCase()
|
|
||||||
if (!normalized.startsWith('gpt-5'))
|
|
||||||
return {}
|
|
||||||
|
|
||||||
return {
|
|
||||||
reasoning_effort: /^gpt-5\.[1-9]\d*/.test(normalized) ? 'none' : 'low',
|
|
||||||
verbosity: 'low',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createRequestBaseUrl(apiBaseUrl) {
|
|
||||||
const baseUrl = apiBaseUrl.trim().replace(/\/+$/, '')
|
|
||||||
if (baseUrl === API_BASE_URL_OPTIONS[0])
|
|
||||||
return RUNAPI_PROXY_BASE_URL
|
|
||||||
return baseUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function translateText(text, settings) {
|
|
||||||
validateTranslateText(text, settings)
|
|
||||||
|
|
||||||
const baseUrl = createRequestBaseUrl(settings.apiBaseUrl)
|
|
||||||
const model = settings.model.trim() || DEFAULT_SETTINGS.model
|
|
||||||
const sourceLanguage = detectSourceLanguage(text)
|
|
||||||
|
|
||||||
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${settings.apiKey.trim()}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model,
|
|
||||||
temperature: 0,
|
|
||||||
...createFastModelOptions(model),
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: createSystemPrompt(sourceLanguage),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: text,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await response.json().catch(() => ({}))
|
|
||||||
if (!response.ok)
|
|
||||||
throw new Error(data?.error?.message || `请求失败:${response.status}`)
|
|
||||||
|
|
||||||
const result = data?.choices?.[0]?.message?.content?.trim() || ''
|
|
||||||
if (!result)
|
|
||||||
throw new Error('未收到翻译结果')
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -62,7 +62,12 @@ defineOptions({ name: 'ImgUpload' })
|
|||||||
|
|
||||||
const { copy, copied } = useClipboard()
|
const { copy, copied } = useClipboard()
|
||||||
|
|
||||||
const imgList = reactive([])
|
const imgList = reactive([
|
||||||
|
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
|
||||||
|
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
|
||||||
|
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
|
||||||
|
{ url: 'https://cdn.isme.top/images/5c23d52f880511ebb6edd017c2d2eca2.jpg' },
|
||||||
|
])
|
||||||
|
|
||||||
watch(copied, (val) => {
|
watch(copied, (val) => {
|
||||||
if (val)
|
if (val)
|
||||||
|
|||||||
@@ -30,9 +30,10 @@
|
|||||||
<n-card class="ml-12 w-70%" title="✨ 欢迎使用 Vue Naive Admin 2.0">
|
<n-card class="ml-12 w-70%" title="✨ 欢迎使用 Vue Naive Admin 2.0">
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<a
|
<a
|
||||||
class="text-highlight text-14 text-primary hover:underline hover:opacity-80"
|
class="text-14 text-primary text-highlight hover:underline hover:opacity-80"
|
||||||
href="https://isme.top"
|
href="https://isme.top"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@click.prevent="message?.info('官网正在火速开发中...')"
|
||||||
>
|
>
|
||||||
isme.top
|
isme.top
|
||||||
</a>
|
</a>
|
||||||
@@ -68,7 +69,7 @@
|
|||||||
<div class="mt-12 flex">
|
<div class="mt-12 flex">
|
||||||
<n-card class="w-50%" title="💯 特性" segmented>
|
<n-card class="w-50%" title="💯 特性" segmented>
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<span class="text-highlight opacity-90">👏 历经十几次重构和细节打磨</span>
|
<span class="opacity-90 text-highlight">👏 历经十几次重构和细节打磨</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<ul class="opacity-90">
|
<ul class="opacity-90">
|
||||||
@@ -159,13 +160,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { useUserStore } from '@/store'
|
||||||
import { BarChart, LineChart, PieChart } from 'echarts/charts'
|
import { BarChart, LineChart, PieChart } from 'echarts/charts'
|
||||||
import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
|
import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
|
||||||
import * as echarts from 'echarts/core'
|
import * as echarts from 'echarts/core'
|
||||||
import { UniversalTransition } from 'echarts/features'
|
import { UniversalTransition } from 'echarts/features'
|
||||||
import { CanvasRenderer } from 'echarts/renderers'
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
import VChart from 'vue-echarts'
|
import VChart from 'vue-echarts'
|
||||||
import { useUserStore } from '@/store'
|
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
@@ -250,7 +251,7 @@ const skillOption = {
|
|||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
bottom: '12%',
|
top: '12%',
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
radius: ['35%', '90%'],
|
radius: ['35%', '90%'],
|
||||||
avoidLabelOverlap: true,
|
avoidLabelOverlap: true,
|
||||||
@@ -283,4 +284,6 @@ const skillOption = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const message = $message
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="wh-full flex-col bg-[url(@/assets/images/login_bg.webp)] bg-cover">
|
<div class="wh-full flex-col bg-[url(@/assets/images/login_bg.webp)] bg-cover">
|
||||||
<div
|
<div
|
||||||
class="m-auto max-w-700 min-w-345 f-c-c rounded-8 auto-bg bg-opacity-20 bg-cover p-12 card-shadow"
|
class="m-auto max-w-700 min-w-345 f-c-c rounded-8 bg-opacity-20 bg-cover p-12 card-shadow auto-bg"
|
||||||
>
|
>
|
||||||
<div class="hidden w-380 px-20 py-35 md:block">
|
<div class="hidden w-380 px-20 py-35 md:block">
|
||||||
<img src="@/assets/images/login_banner.webp" class="w-full" alt="login_banner">
|
<img src="@/assets/images/login_banner.webp" class="w-full" alt="login_banner">
|
||||||
@@ -101,9 +101,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useStorage } from '@vueuse/core'
|
|
||||||
import { useAuthStore } from '@/store'
|
import { useAuthStore } from '@/store'
|
||||||
import { lStorage, throttle } from '@/utils'
|
import { lStorage, throttle } from '@/utils'
|
||||||
|
import { useStorage } from '@vueuse/core'
|
||||||
import api from './api'
|
import api from './api'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
* Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
|
||||||
**********************************/
|
**********************************/
|
||||||
|
|
||||||
import axios from 'axios'
|
|
||||||
import { request } from '@/utils'
|
import { request } from '@/utils'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getMenuTree: () => request.get('/permission/menu/tree'),
|
getMenuTree: () => request.get('/permission/menu/tree'),
|
||||||
|
|||||||
@@ -159,10 +159,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import icons from 'isme:icons'
|
|
||||||
import pagePathes from 'isme:page-pathes'
|
|
||||||
import { MeModal } from '@/components'
|
import { MeModal } from '@/components'
|
||||||
import { useForm, useModal } from '@/composables'
|
import { useForm, useModal } from '@/composables'
|
||||||
|
import icons from 'isme:icons'
|
||||||
|
import pagePathes from 'isme:page-pathes'
|
||||||
import api from '../api'
|
import api from '../api'
|
||||||
import QuestionLabel from './QuestionLabel.vue'
|
import QuestionLabel from './QuestionLabel.vue'
|
||||||
|
|
||||||
|
|||||||
@@ -92,8 +92,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { NButton, NSwitch } from 'naive-ui'
|
|
||||||
import { MeCrud } from '@/components'
|
import { MeCrud } from '@/components'
|
||||||
|
import { NButton, NSwitch } from 'naive-ui'
|
||||||
import api from './api'
|
import api from './api'
|
||||||
import MenuTree from './components/MenuTree.vue'
|
import MenuTree from './components/MenuTree.vue'
|
||||||
import ResAddOrEdit from './components/ResAddOrEdit.vue'
|
import ResAddOrEdit from './components/ResAddOrEdit.vue'
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
:checked-keys="modalForm.permissionIds"
|
:checked-keys="modalForm.permissionIds"
|
||||||
:on-update:checked-keys="(keys) => (modalForm.permissionIds = keys)"
|
:on-update:checked-keys="(keys) => (modalForm.permissionIds = keys)"
|
||||||
|
|
||||||
checkable check-on-click default-expand-all
|
default-expand-all checkable check-on-click
|
||||||
class="cus-scroll max-h-200 w-full"
|
class="cus-scroll max-h-200 w-full"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
@@ -95,9 +95,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { NButton, NSwitch } from 'naive-ui'
|
|
||||||
import { MeCrud, MeModal, MeQueryItem } from '@/components'
|
import { MeCrud, MeModal, MeQueryItem } from '@/components'
|
||||||
import { useCrud } from '@/composables'
|
import { useCrud } from '@/composables'
|
||||||
|
import { NButton, NSwitch } from 'naive-ui'
|
||||||
import api from './api'
|
import api from './api'
|
||||||
|
|
||||||
defineOptions({ name: 'RoleMgt' })
|
defineOptions({ name: 'RoleMgt' })
|
||||||
|
|||||||
@@ -67,10 +67,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
|
|
||||||
import { h } from 'vue'
|
|
||||||
import { MeCrud, MeQueryItem } from '@/components'
|
import { MeCrud, MeQueryItem } from '@/components'
|
||||||
import { formatDateTime } from '@/utils'
|
import { formatDateTime } from '@/utils'
|
||||||
|
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
|
||||||
|
import { h } from 'vue'
|
||||||
import api from './api'
|
import api from './api'
|
||||||
|
|
||||||
defineOptions({ name: 'RoleUser' })
|
defineOptions({ name: 'RoleUser' })
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
trigger: ['input', 'blur'],
|
trigger: ['input', 'blur'],
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<n-input v-model:value="modalForm.password" type="password" show-password-on="mousedown" />
|
<n-input v-model:value="modalForm.password" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
<n-form-item v-if="['add', 'setRole'].includes(modalAction)" label="角色" path="roleIds">
|
<n-form-item v-if="['add', 'setRole'].includes(modalAction)" label="角色" path="roleIds">
|
||||||
@@ -110,11 +110,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
|
|
||||||
import { MeCrud, MeModal, MeQueryItem } from '@/components'
|
import { MeCrud, MeModal, MeQueryItem } from '@/components'
|
||||||
import { useCrud } from '@/composables'
|
import { useCrud } from '@/composables'
|
||||||
import { withPermission } from '@/directives'
|
|
||||||
import { formatDateTime } from '@/utils'
|
import { formatDateTime } from '@/utils'
|
||||||
|
import { NAvatar, NButton, NSwitch, NTag } from 'naive-ui'
|
||||||
import api from './api'
|
import api from './api'
|
||||||
|
|
||||||
defineOptions({ name: 'UserMgt' })
|
defineOptions({ name: 'UserMgt' })
|
||||||
@@ -220,29 +219,17 @@ const columns = [
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
width: 420,
|
width: 320,
|
||||||
align: 'right',
|
align: 'right',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
hideInExcel: true,
|
hideInExcel: true,
|
||||||
render(row) {
|
render(row) {
|
||||||
return [
|
return [
|
||||||
withPermission(
|
|
||||||
h(NButton, {
|
|
||||||
size: 'small',
|
|
||||||
type: 'primary',
|
|
||||||
secondary: true,
|
|
||||||
}, {
|
|
||||||
default: () => '超管专属',
|
|
||||||
icon: () => h('i', { class: 'i-carbon:user-role text-14' }),
|
|
||||||
}),
|
|
||||||
'SuperAdmin',
|
|
||||||
),
|
|
||||||
h(
|
h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
size: 'small',
|
size: 'small',
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
class: 'ml-12px',
|
|
||||||
secondary: true,
|
secondary: true,
|
||||||
onClick: () => handleOpenRolesSet(row),
|
onClick: () => handleOpenRolesSet(row),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -73,10 +73,10 @@
|
|||||||
require-mark-placement="left"
|
require-mark-placement="left"
|
||||||
>
|
>
|
||||||
<n-form-item label="原密码" path="oldPassword" :rule="required">
|
<n-form-item label="原密码" path="oldPassword" :rule="required">
|
||||||
<n-input v-model:value="pwdForm.oldPassword" type="password" placeholder="请输入原密码" show-password-on="mousedown" />
|
<n-input v-model:value="pwdForm.oldPassword" type="password" placeholder="请输入原密码" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="新密码" path="newPassword" :rule="required">
|
<n-form-item label="新密码" path="newPassword" :rule="required">
|
||||||
<n-input v-model:value="pwdForm.newPassword" type="password" placeholder="请输入新密码" show-password-on="mousedown" />
|
<n-input v-model:value="pwdForm.newPassword" type="password" placeholder="请输入新密码" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
</MeModal>
|
</MeModal>
|
||||||
|
|||||||
@@ -8,13 +8,13 @@
|
|||||||
|
|
||||||
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders'
|
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders'
|
||||||
import presetRemToPx from '@unocss/preset-rem-to-px'
|
import presetRemToPx from '@unocss/preset-rem-to-px'
|
||||||
import { defineConfig, presetAttributify, presetIcons, presetWind3 } from 'unocss'
|
import { defineConfig, presetAttributify, presetIcons, presetUno } from 'unocss'
|
||||||
import { getIcons } from './build/index.js'
|
import { getIcons } from './build/index.js'
|
||||||
|
|
||||||
const icons = getIcons()
|
const icons = getIcons()
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
presets: [
|
presets: [
|
||||||
presetWind3(),
|
presetUno(),
|
||||||
presetAttributify(),
|
presetAttributify(),
|
||||||
presetIcons({
|
presetIcons({
|
||||||
warn: true,
|
warn: true,
|
||||||
|
|||||||
@@ -67,17 +67,8 @@ export default defineConfig(({ mode }) => {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'/runapi': {
|
|
||||||
target: 'https://runapi.co',
|
|
||||||
changeOrigin: true,
|
|
||||||
rewrite: path => path.replace(/^\/runapi/, '/v1'),
|
|
||||||
secure: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
optimizeDeps: {
|
|
||||||
include: ['vue3-intro-step'],
|
|
||||||
},
|
|
||||||
build: {
|
build: {
|
||||||
chunkSizeWarningLimit: 1024, // chunk 大小警告的限制(单位kb)
|
chunkSizeWarningLimit: 1024, // chunk 大小警告的限制(单位kb)
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user