1
0
mirror of https://github.com/zclzone/vue-naive-admin.git synced 2026-07-25 13:34:08 +08:00

10 Commits

43 changed files with 3808 additions and 4765 deletions

View File

@@ -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'

View File

@@ -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'

View File

@@ -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"
] ]
} }

View File

@@ -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="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
> [!Tip]
> 我们提供WebPC+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,一直秉
## 极致的性能 ## 极致的性能
![性能](https://docs.isme.top/Public/Uploads/2023-11-18/6558568b2b476.png) ![](https://docs.isme.top/Public/Uploads/2023-11-18/6558568b2b476.png)
![性能](https://docs.isme.top/Public/Uploads/2023-11-18/655853caa9ce8.png) ![](https://docs.isme.top/Public/Uploads/2023-11-18/655853caa9ce8.png)
## 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协议`,默认授权给任何人,被授权人可免费地无限制的使用、复制、修改、合并、发布、发行、再许可、售卖本软件拷贝、并有权向被供应人授予同等的权利,但必须满足以下条件:

View File

@@ -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"]

View File

@@ -13,46 +13,46 @@
}, },
"dependencies": { "dependencies": {
"@arco-design/color": "^0.4.0", "@arco-design/color": "^0.4.0",
"@vueuse/core": "^14.2.1", "@vueuse/core": "^12.7.0",
"axios": "^1.16.1", "axios": "^1.8.1",
"dayjs": "^1.11.21", "dayjs": "^1.11.13",
"echarts": "^6.1.0", "echarts": "^5.6.0",
"lodash-es": "^4.18.1", "lodash-es": "^4.17.21",
"naive-ui": "^2.44.1", "naive-ui": "^2.41.0",
"pinia": "^3.0.4", "pinia": "^3.0.1",
"pinia-plugin-persistedstate": "^4.7.1", "pinia-plugin-persistedstate": "^4.2.0",
"vue": "^3.5.35", "vue": "^3.5.13",
"vue-echarts": "^8.0.1", "vue-echarts": "^7.0.3",
"vue-router": "^5.1.0", "vue-router": "^4.5.0",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^9.0.0", "@antfu/eslint-config": "^4.3.0",
"@iconify/json": "^2.2.481", "@iconify/json": "^2.2.311",
"@unocss/eslint-config": "^66.7.0", "@unocss/eslint-config": "^66.0.0",
"@unocss/eslint-plugin": "^66.7.0", "@unocss/eslint-plugin": "^66.0.0",
"@unocss/preset-rem-to-px": "^66.7.0", "@unocss/preset-rem-to-px": "^66.0.0",
"@vitejs/plugin-vue": "^6.0.7", "@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^5.1.5", "@vitejs/plugin-vue-jsx": "^4.1.1",
"eslint": "^10.4.1", "eslint": "^9.21.0",
"eslint-plugin-format": "^2.0.1", "eslint-plugin-format": "^1.0.1",
"esno": "^4.8.0", "esno": "^4.8.0",
"fs-extra": "^11.3.5", "fs-extra": "^11.3.0",
"glob": "^13.0.6", "glob": "^11.0.1",
"lint-staged": "^17.0.7", "lint-staged": "^15.4.3",
"rollup-plugin-visualizer": "^7.0.1", "rollup-plugin-visualizer": "^5.14.0",
"simple-git-hooks": "^2.13.1", "simple-git-hooks": "^2.11.1",
"taze": "^19.14.1", "taze": "^18.6.0",
"unocss": "^66.7.0", "unocss": "^66.0.0",
"unplugin-auto-import": "^21.0.0", "unplugin-auto-import": "^19.1.1",
"unplugin-vue-components": "^32.1.0", "unplugin-vue-components": "^28.4.1",
"vite": "^8.0.14", "vite": "^6.2.0",
"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.7.2",
"vue3-intro-step": "^1.0.5" "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"

7445
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -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()

View File

@@ -1,24 +1,22 @@
<template> <template>
<div class="f-c-c"> <n-tooltip trigger="hover">
<n-tooltip trigger="hover" placement="bottom"> <template #trigger>
<template #trigger> <n-color-picker
<div id="theme-setting" class="h-32 w-32"> id="theme-setting"
<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()

View File

@@ -8,8 +8,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 +39,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',
}, },
) )
} }

View File

@@ -7,52 +7,48 @@
---------------------------------> --------------------------------->
<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> <n-space :wrap="!expand || isExpanded" :size="[32, 16]" class="p-10">
<n-space :wrap="!expand || isExpanded" :size="[32, 16]" class="p-10"> <slot />
<slot /> </n-space>
</n-space> </n-scrollbar>
</n-scrollbar> <div class="flex-shrink-0 p-10">
<div class="flex-shrink-0 p-10"> <n-button ghost type="primary" @click="handleReset">
<n-button ghost type="primary" @click="handleReset"> <i class="i-fe:rotate-ccw mr-4" />
<i class="i-fe:rotate-ccw mr-4" /> 重置
重置 </n-button>
</n-button> <n-button attr-type="submit" class="ml-20" type="primary">
<n-button attr-type="submit" class="ml-20" type="primary"> <i class="i-fe:search mr-4" />
<i class="i-fe:search mr-4" /> 搜索
搜索 </n-button>
</n-button>
<template v-if="expand"> <template v-if="expand">
<n-button v-if="!isExpanded" type="primary" text @click="toggleExpand"> <n-button v-if="!isExpanded" type="primary" text @click="toggleExpand">
<i class="i-fe:chevrons-down ml-4" /> <i class="i-fe:chevrons-down ml-4" />
展开 展开
</n-button> </n-button>
<n-button v-else text type="primary" @click="toggleExpand"> <n-button v-else text type="primary" @click="toggleExpand">
<i class="i-fe:chevrons-up ml-4" /> <i class="i-fe:chevrons-up ml-4" />
收起 收起
</n-button> </n-button>
</template> </template>
</div> </div>
</form> </form>
</AppCard> </AppCard>
<NDataTable <NDataTable
:remote="remote" :remote="remote"
:loading="loading" :loading="loading"
:scroll-x="scrollX" :scroll-x="scrollX"
:columns="columns" :columns="columns"
:data="tableData" :data="tableData"
:row-key="(row) => row[rowKey]" :row-key="(row) => row[rowKey]"
:pagination="isPagination ? pagination : false" :pagination="isPagination ? pagination : false"
flex-height @update:checked-row-keys="onChecked"
class="flex-1" @update:page="onPageChange"
@update:checked-row-keys="onChecked" />
@update:page="onPageChange"
/>
</div>
</template> </template>
<script setup> <script setup>

View File

@@ -6,10 +6,9 @@
* 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 = { export const permission = {
mounted(el, binding) { mounted(el, binding) {
const currentRoute = unref(router.currentRoute) const currentRoute = unref(router.currentRoute)
const btns = currentRoute.meta?.btns?.map(item => item.code) || [] const btns = currentRoute.meta?.btns?.map(item => item.code) || []
@@ -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]])
}

View File

@@ -9,7 +9,7 @@
<template> <template>
<div <div
id="menu-collapse" id="menu-collapse"
class="f-c-c cursor-pointer rounded-4 auto-bg-hover p-6 text-22 transition-all-300" class="f-c-c cursor-pointer rounded-4 p-6 text-22 transition-all-300 auto-bg-hover"
@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'" />

View File

@@ -54,8 +54,6 @@ function handleMenuSelect(key, item) {
}) })
} }
else { else {
if (!item.path)
return
router.push(item.path) router.push(item.path)
} }
} }

View File

@@ -43,8 +43,14 @@ export function createPermissionGuard(router) {
return { ...to, replace: true } return { ...to, replace: true }
} }
router.addRoute({
name: 'catchAll',
path: '/:catchAll(.*)',
redirect: '/404',
})
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

View File

@@ -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)
} }

View File

@@ -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: () => ({

View File

@@ -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: () => ({

View File

@@ -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) {

View File

@@ -16,6 +16,8 @@ export const useRouterStore = defineStore('router', () => {
accessRoutes.forEach((item) => { accessRoutes.forEach((item) => {
router.hasRoute(item.name) && router.removeRoute(item.name) router.hasRoute(item.name) && router.removeRoute(item.name)
}) })
router.removeRoute('catchAll')
} }
return { return {

View File

@@ -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)
} }
}, },

View File

@@ -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 */

View File

@@ -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) {

View File

@@ -10,34 +10,45 @@
import { useAuthStore } from '@/store' import { useAuthStore } from '@/store'
let isConfirming = false let isConfirming = false
function handleAuthExpired(content, needTip) {
if (isConfirming || !needTip)
return
isConfirming = true
$dialog.confirm({
title: '提示',
type: 'info',
content,
confirm() {
useAuthStore().logout()
window.$message?.success('已退出登录')
isConfirming = false
},
cancel() {
isConfirming = false
},
})
return false
}
export function resolveResError(code, message, needTip = true) { export function resolveResError(code, message, needTip = true) {
switch (code) { switch (code) {
case 401: case 401:
return handleAuthExpired('登录已过期,是否重新登录?', needTip) if (isConfirming || !needTip)
return
isConfirming = true
$dialog.confirm({
title: '提示',
type: 'info',
content: '登录已过期,是否重新登录?',
confirm() {
useAuthStore().logout()
window.$message?.success('已退出登录')
isConfirming = false
},
cancel() {
isConfirming = false
},
})
return false
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

View File

@@ -11,4 +11,5 @@ export * from './common'
export * from './http' export * from './http'
export * from './is' export * from './is'
export * from './naiveTools' export * from './naiveTools'
export * from './permissionH'
export * from './storage' export * from './storage'

View File

@@ -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 {

14
src/utils/permissionH.js Normal file
View File

@@ -0,0 +1,14 @@
import { permission } from '@/directives'
/**
* @description: 按钮权限渲染
*/
import { h, withDirectives } from 'vue'
export function PH(el, props, slots, butCode) {
return withDirectives(
h(el, props, slots),
[
[permission, butCode],
],
)
}

View File

@@ -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)

View File

@@ -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(`&lt;i class=&quot;${item}&quot; /&gt;`)" @click="copy(`&lt;i class=&quot;${item}&quot; /&gt;`)"
> >
<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" />

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
}

View File

@@ -62,7 +62,11 @@ defineOptions({ name: 'ImgUpload' })
const { copy, copied } = useClipboard() const { copy, copied } = useClipboard()
const imgList = reactive([]) const imgList = reactive([
{ url: 'https://img.isme.top/isme/67208863145ef.jpg' },
{ url: 'https://img.isme.top/isme/67208ab2a9de0.jpg' },
{ url: 'https://img.isme.top/isme/67208ab4c6596.jpg' },
])
watch(copied, (val) => { watch(copied, (val) => {
if (val) if (val)

View File

@@ -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>

View File

@@ -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()

View File

@@ -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'),

View File

@@ -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'

View File

@@ -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'

View File

@@ -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' })

View File

@@ -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' })
@@ -163,31 +163,31 @@ const columns = [
render(row) { render(row) {
return row.roles?.some(item => item.id === +route.params.roleId) return row.roles?.some(item => item.id === +route.params.roleId)
? h( ? h(
NButton, NButton,
{ {
size: 'small', size: 'small',
type: 'error', type: 'error',
secondary: true, secondary: true,
onClick: () => handleBatchRemove([row.id]), onClick: () => handleBatchRemove([row.id]),
}, },
{ {
default: () => '取消授权', default: () => '取消授权',
icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }), icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
}, },
) )
: h( : h(
NButton, NButton,
{ {
size: 'small', size: 'small',
type: 'primary', type: 'primary',
secondary: true, secondary: true,
onClick: () => handleBatchAdd([row.id]), onClick: () => handleBatchAdd([row.id]),
}, },
{ {
default: () => '授权', default: () => '授权',
icon: () => h('i', { class: 'i-line-md:confirm-circle text-14' }), icon: () => h('i', { class: 'i-line-md:confirm-circle text-14' }),
}, },
) )
}, },
}, },
] ]

View File

@@ -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, PH } 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,22 +219,25 @@ const columns = [
{ {
title: '操作', title: '操作',
key: 'actions', key: 'actions',
width: 420, width: 380,
align: 'right', align: 'right',
fixed: 'right', fixed: 'right',
hideInExcel: true, hideInExcel: true,
render(row) { render(row) {
return [ return [
withPermission( PH(
h(NButton, { NButton,
{
size: 'small', size: 'small',
type: 'primary', type: 'primary',
secondary: true, secondary: true,
}, { onClick: () => handleOpenRolesSet(row),
},
{
default: () => '超管专属', default: () => '超管专属',
icon: () => h('i', { class: 'i-carbon:user-role text-14' }), icon: () => h('i', { class: 'i-carbon:user-role text-14' }),
}), },
'SuperAdmin', 'superAdmin',
), ),
h( h(
NButton, NButton,

View File

@@ -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>

View File

@@ -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
}, },