mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2026-01-16 00:57:09 +08:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
effcea209b | ||
|
|
7d014d6d94 | ||
|
|
ca30f95e67 | ||
| 48e6eccd1a | |||
| 8c3dc80909 | |||
|
|
7f982b8b53 | ||
| e78f7c61a0 |
@@ -1,7 +1,7 @@
|
||||
# ContiNew Admin UI
|
||||
|
||||
<a href="https://github.com/continew-org/continew-admin-ui" title="Release" target="_blank">
|
||||
<img src="https://img.shields.io/badge/RELEASE-v4.1.0-%23ff3f59.svg" alt="Release" />
|
||||
<img src="https://img.shields.io/badge/SNAPSHOT-v4.2.0-%23ff3f59.svg" alt="Release" />
|
||||
</a>
|
||||
<a href="https://vuejs.org/" title="Vue" target="_blank">
|
||||
<img src="https://img.shields.io/badge/Vue-3.5.4-%236CB52D.svg?logo=Vue.js" alt="Vue" />
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function appInfo(): Plugin {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
boxen(
|
||||
`${bold(green(`${bgGreen('ContiNew Admin v4.1.0')}`))}\n${cyan('在线文档:')}${underline('https://continew.top')}\n${cyan('吐槽广场:')}${underline('https://continew.top/docs/admin/issue-hub.html')}\n${cyan('常见问题:')}${underline('https://continew.top/docs/admin/faq.html')}\n${cyan('更新日志:')}${underline('https://continew.top/docs/admin/changelog/')}\n${cyan('持续迭代优化的,高质量多租户中后台管理系统框架')}`,
|
||||
`${bold(green(`${bgGreen('ContiNew Admin v4.2.0-SNAPSHOT')}`))}\n${cyan('在线文档:')}${underline('https://continew.top')}\n${cyan('吐槽广场:')}${underline('https://continew.top/docs/admin/issue-hub.html')}\n${cyan('常见问题:')}${underline('https://continew.top/docs/admin/faq.html')}\n${cyan('更新日志:')}${underline('https://continew.top/docs/admin/changelog/')}\n${cyan('持续迭代优化的,高质量多租户中后台管理系统框架')}`,
|
||||
{
|
||||
padding: 1,
|
||||
margin: 1,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "continew-admin-ui",
|
||||
"type": "module",
|
||||
"version": "4.1.0",
|
||||
"version": "4.2.0-SNAPSHOT",
|
||||
"private": "true",
|
||||
"scripts": {
|
||||
"bootstrap": "pnpm install --registry=https://registry.npmmirror.com",
|
||||
|
||||
@@ -295,12 +295,17 @@ export interface ClientResp {
|
||||
activeTimeout: string
|
||||
timeout: string
|
||||
status: string
|
||||
isConcurrent: boolean
|
||||
replacedRange: string
|
||||
maxLoginCount: number
|
||||
overflowLogoutMode: string
|
||||
createUser: string
|
||||
createTime: string
|
||||
updateUser: string
|
||||
updateTime: string
|
||||
createUserString: string
|
||||
updateUserString: string
|
||||
disabled: boolean
|
||||
}
|
||||
export interface ClientDetailResp {
|
||||
id: string
|
||||
@@ -309,7 +314,11 @@ export interface ClientDetailResp {
|
||||
authType: string
|
||||
activeTimeout: string
|
||||
timeout: string
|
||||
status: string
|
||||
status: number
|
||||
isConcurrent: boolean
|
||||
maxLoginCount: number
|
||||
replacedRange: string
|
||||
overflowLogoutMode: string
|
||||
createUser: string
|
||||
createTime: string
|
||||
updateUser: string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="json_pretty_container">
|
||||
<JsonViewer expand-depth="5" :value="JSONObject" :theme="currentThemeClass" sort />
|
||||
<JsonViewer :expand-depth="5" :value="JSONObject" :theme="currentThemeClass" sort />
|
||||
<icon-copy class="copy_icon" @click="onCopy(JSONObject)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
:style="appStore.menuDark ? appStore.themeCSSVar : undefined"
|
||||
>
|
||||
<Logo :collapsed="appStore.menuCollapse" />
|
||||
<Menu :menus="twoLevelMenus" :menu-style="{ width: '200px', flex: 1 }" />
|
||||
<Menu :menus="twoLevelMenus" :menu-style="{ flex: 1 }" />
|
||||
<WwAds class="ads" />
|
||||
</section>
|
||||
|
||||
@@ -83,7 +83,6 @@ const checkAndShowNotices = () => {
|
||||
// 如果有token,检查未读公告
|
||||
if (token) {
|
||||
setTimeout(() => {
|
||||
console.log(noticePopupRef.value)
|
||||
noticePopupRef.value?.open()
|
||||
}, 1000) // 延迟1秒显示,让页面先加载完成
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<template>
|
||||
<div class="layout-top">
|
||||
<a-row align="center" class="layout-top__header">
|
||||
<a-row align="center" class="layout-top__header" :style="getMenuStyle">
|
||||
<Logo></Logo>
|
||||
<Menu class="layout-top__menu"></Menu>
|
||||
<a-menu
|
||||
class="layout-top__menu" mode="horizontal" :theme="menuTheme" :menu-trigger-props="menuTriggerProps"
|
||||
:selected-keys="selectedKeys" @menu-item-click="handleMenuItemClick"
|
||||
>
|
||||
<MenuItem v-for="(item, index) in menuList" :key="item.path + index" :item="item" />
|
||||
</a-menu>
|
||||
<HeaderRightBar></HeaderRightBar>
|
||||
</a-row>
|
||||
<Tabs v-if="appStore.tab"></Tabs>
|
||||
@@ -14,29 +19,32 @@
|
||||
import HeaderRightBar from './components/HeaderRightBar/index.vue'
|
||||
import Logo from './components/Logo.vue'
|
||||
import Main from './components/Main.vue'
|
||||
import Menu from './components/Menu/index.vue'
|
||||
import MenuItem from './components/Menu/MenuItem.vue'
|
||||
|
||||
import Tabs from './components/Tabs/index.vue'
|
||||
import { useMenu } from './hooks/useMenus'
|
||||
import { useAppStore } from '@/stores'
|
||||
|
||||
defineOptions({ name: 'LayoutTop' })
|
||||
|
||||
const appStore = useAppStore()
|
||||
const { menuTheme, menuList, menuTriggerProps, handleMenuItemClick, selectedKeys, getMenuStyle } = useMenu()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-top {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&__header {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background-color: var(--color-bg-1);
|
||||
width: 100%;
|
||||
padding-right: 16px;
|
||||
overflow: hidden;
|
||||
background-color: var(--color-bg-1);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
&__menu {
|
||||
|
||||
@@ -184,7 +184,7 @@ const reload = () => {
|
||||
}
|
||||
|
||||
.tabs {
|
||||
padding-top: 5px;
|
||||
padding: 5px 0;
|
||||
background-color: var(--color-bg-1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { CSSProperties } from 'vue'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
import { eachTree } from 'xe-utils'
|
||||
import { useRouteStore } from '@/stores'
|
||||
import { useDevice, useRouteListener } from '@/hooks'
|
||||
import { useAppStore, useRouteStore } from '@/stores'
|
||||
import { filterTree } from '@/utils'
|
||||
import { isExternal } from '@/utils/validate'
|
||||
|
||||
/**
|
||||
* 菜单管理 Hooks
|
||||
@@ -11,8 +14,30 @@ import { filterTree } from '@/utils'
|
||||
* @returns {object} 包含处理后菜单列表的响应式对象
|
||||
*/
|
||||
export function useMenu() {
|
||||
const router = useRouter()
|
||||
// 路由存储实例,用于获取原始路由配置
|
||||
const routeStore = useRouteStore()
|
||||
const appStore = useAppStore()
|
||||
const { listenerRouteChange } = useRouteListener()
|
||||
const { isDesktop } = useDevice()
|
||||
|
||||
// 是否折叠菜单
|
||||
const collapsed = computed(() =>
|
||||
!isDesktop.value ? false : appStore.menuCollapse,
|
||||
)
|
||||
|
||||
// 菜单触发器配置
|
||||
const menuTriggerProps = {
|
||||
animationName: 'slide-dynamic-origin',
|
||||
}
|
||||
|
||||
// 菜单主题
|
||||
const menuTheme = computed(() => appStore.menuDark ? 'dark' : 'light')
|
||||
|
||||
// 获取菜单样式
|
||||
const getMenuStyle = computed(() => {
|
||||
return { backgroundColor: menuTheme.value === 'dark' ? 'var(--color-menu-dark-bg)' : 'var(--color-menu-light-bg)' } as CSSProperties
|
||||
})
|
||||
|
||||
/**
|
||||
* 处理后的菜单列表
|
||||
@@ -36,7 +61,6 @@ export function useMenu() {
|
||||
if (i?.children?.length === 1 && i?.meta?.alwaysShow !== true) {
|
||||
if (i.meta) {
|
||||
i.meta.title = i.meta?.title || i.children?.[0]?.meta?.title
|
||||
i.meta.svgIcon = i.meta?.svgIcon || i.children?.[0]?.meta?.svgIcon
|
||||
i.meta.icon = i.meta?.icon || i.children?.[0]?.meta?.icon
|
||||
}
|
||||
i.path = i.children?.[0]?.path
|
||||
@@ -46,7 +70,32 @@ export function useMenu() {
|
||||
return showMenuList
|
||||
})
|
||||
|
||||
const selectedKeys = ref<string[]>([])
|
||||
|
||||
function handleMenuItemClick(key: string) {
|
||||
if (isExternal(key)) {
|
||||
window.open(key)
|
||||
return
|
||||
}
|
||||
selectedKeys.value = [key]
|
||||
router.push({ path: key })
|
||||
}
|
||||
|
||||
listenerRouteChange(({ to }) => {
|
||||
if (to?.meta?.activeMenu) {
|
||||
selectedKeys.value = [to.meta.activeMenu]
|
||||
return
|
||||
}
|
||||
selectedKeys.value = [to.path]
|
||||
})
|
||||
|
||||
return {
|
||||
menuTheme,
|
||||
getMenuStyle,
|
||||
menuList,
|
||||
menuTriggerProps,
|
||||
selectedKeys,
|
||||
collapsed,
|
||||
handleMenuItemClick,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ onMounted(() => {
|
||||
|
||||
.captcha-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,16 +32,30 @@ const visible = ref(false)
|
||||
const isUpdate = computed(() => !!dataId.value)
|
||||
const title = computed(() => (isUpdate.value ? '修改客户端' : '新增客户端'))
|
||||
const formRef = ref<InstanceType<typeof GiForm>>()
|
||||
const { client_type, auth_type_enum } = useDict('auth_type_enum', 'client_type')
|
||||
const { client_type, auth_type_enum, replaced_range_enum, logout_mode_enum } = useDict('auth_type_enum', 'client_type', 'replaced_range_enum', 'logout_mode_enum')
|
||||
|
||||
const [form, resetForm] = useResetReactive({
|
||||
activeTimeout: 1800,
|
||||
timeout: 86400,
|
||||
isConcurrent: 1,
|
||||
isShare: 1,
|
||||
isConcurrent: true,
|
||||
maxLoginCount: -1,
|
||||
status: 1,
|
||||
})
|
||||
|
||||
// 监听 isConcurrent 的变化,处理字段互斥逻辑
|
||||
watch(
|
||||
() => form.isConcurrent,
|
||||
(newVal) => {
|
||||
if (!newVal) {
|
||||
form.maxLoginCount = -1
|
||||
// replacedRange 只有在 isConcurrent=false 时才有意义
|
||||
} else if (newVal) {
|
||||
// 当 isConcurrent=true 时,清空 maxLoginCount
|
||||
form.maxLoginCount = -1
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const columns: ColumnItem[] = reactive([
|
||||
{
|
||||
label: '客户端类型',
|
||||
@@ -66,7 +80,7 @@ const columns: ColumnItem[] = reactive([
|
||||
},
|
||||
{
|
||||
label: () => (
|
||||
<a-tooltip content="-1 代表不限制,永不冻结">
|
||||
<a-tooltip content="-1:不限制,永不冻结">
|
||||
Token 最低活跃频率
|
||||
<icon-question-circle />
|
||||
</a-tooltip>
|
||||
@@ -86,8 +100,8 @@ const columns: ColumnItem[] = reactive([
|
||||
},
|
||||
{
|
||||
label: () => (
|
||||
<a-tooltip content="-1 代表永不过期">
|
||||
Token 有效期
|
||||
<a-tooltip content="-1:永不过期">
|
||||
Token 有效期
|
||||
<icon-question-circle />
|
||||
</a-tooltip>
|
||||
),
|
||||
@@ -104,6 +118,78 @@ const columns: ColumnItem[] = reactive([
|
||||
},
|
||||
rules: [{ required: true, message: '请输入 Token 有效期' }],
|
||||
},
|
||||
{
|
||||
label: '是否允许同一账号多地同时登录',
|
||||
field: 'isConcurrent',
|
||||
type: 'switch',
|
||||
span: 12,
|
||||
props: {
|
||||
type: 'round',
|
||||
checkedValue: true,
|
||||
uncheckedValue: false,
|
||||
checkedText: '允许',
|
||||
uncheckedText: '不允许',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '顶人下线的范围',
|
||||
field: 'replacedRange',
|
||||
type: 'select',
|
||||
span: 12,
|
||||
props: {
|
||||
options: replaced_range_enum,
|
||||
placeholder: '请选择顶人下线的范围',
|
||||
},
|
||||
disabled: () => {
|
||||
return form.isConcurrent
|
||||
},
|
||||
},
|
||||
{
|
||||
label: () => (
|
||||
<a-tooltip content="-1:不限制">
|
||||
同一账号最大登录数量
|
||||
<icon-question-circle />
|
||||
</a-tooltip>
|
||||
),
|
||||
field: 'maxLoginCount',
|
||||
type: 'input-number',
|
||||
span: 12,
|
||||
slots: {
|
||||
append: () => (
|
||||
<span style={{ width: '30px', textAlign: 'center' }}>个</span>
|
||||
),
|
||||
},
|
||||
props: {
|
||||
placeholder: '请输入同一账号最大登录数量',
|
||||
min: -1,
|
||||
},
|
||||
disabled: () => {
|
||||
return !form.isConcurrent
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
validator: (value: number, callback: (errorMessage?: string) => void) => {
|
||||
if (value <= 0 && value !== -1) {
|
||||
callback('最大登录数量只能为 -1 或正整数')
|
||||
}
|
||||
callback()
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '溢出人数的下线方式',
|
||||
field: 'overflowLogoutMode',
|
||||
type: 'select',
|
||||
span: 12,
|
||||
props: {
|
||||
options: logout_mode_enum,
|
||||
placeholder: '请选择溢出人数的下线方式',
|
||||
},
|
||||
disabled: () => {
|
||||
return form.maxLoginCount === -1 || form.maxLoginCount === 0
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
field: 'status',
|
||||
|
||||
@@ -17,6 +17,19 @@
|
||||
<a-tag v-if="dataDetail?.status === 1" color="green">启用</a-tag>
|
||||
<a-tag v-else color="red">禁用</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="多地登录">
|
||||
<a-tag v-if="dataDetail?.isConcurrent" color="blue">允许</a-tag>
|
||||
<a-tag v-else color="orange">不允许</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="下线范围">
|
||||
<GiCellTag :value="dataDetail?.replacedRange" :dict="replaced_range_enum" />
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="登录数量">
|
||||
{{ dataDetail?.maxLoginCount === -1 ? '不限制' : dataDetail?.maxLoginCount }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="溢出处理">
|
||||
<GiCellTag :value="dataDetail?.overflowLogoutMode" :dict="logout_mode_enum" />
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建人">{{ dataDetail?.createUserString }}</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">{{ dataDetail?.createTime }}</a-descriptions-item>
|
||||
<a-descriptions-item label="更新人">{{ dataDetail?.updateUserString }}</a-descriptions-item>
|
||||
@@ -29,11 +42,14 @@
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { type ClientDetailResp, getClient as getDetail } from '@/apis/system/client'
|
||||
import { useDict } from '@/hooks/app'
|
||||
import GiCellTag from '@/components/GiCell/GiCellTag.vue'
|
||||
|
||||
const {
|
||||
client_type,
|
||||
auth_type_enum,
|
||||
} = useDict('client_type', 'auth_type_enum')
|
||||
replaced_range_enum,
|
||||
logout_mode_enum,
|
||||
} = useDict('client_type', 'auth_type_enum', 'replaced_range_enum', 'logout_mode_enum')
|
||||
|
||||
const { width } = useWindowSize()
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:data="dataList"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:scroll="{ x: '100%', y: '100%', minWidth: 1200 }"
|
||||
:scroll="{ x: '100%', y: '100%', minWidth: 1400 }"
|
||||
:pagination="pagination"
|
||||
:disabled-tools="['size']"
|
||||
:disabled-column-keys="['clientKey']"
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
<script setup lang="tsx">
|
||||
import type { LabelValue } from '@arco-design/web-vue/es/tree-select/interface'
|
||||
import type { TableInstance } from '@arco-design/web-vue'
|
||||
import { type TableInstance, Tag } from '@arco-design/web-vue'
|
||||
import AddModal from './AddModal.vue'
|
||||
import DetailDrawer from './DetailDrawer.vue'
|
||||
import { type ClientQuery, type ClientResp, deleteClient, listClient } from '@/apis/system/client'
|
||||
@@ -79,7 +79,9 @@ defineOptions({ name: 'SystemClient' })
|
||||
const {
|
||||
client_type,
|
||||
auth_type_enum,
|
||||
} = useDict('client_type', 'auth_type_enum')
|
||||
replaced_range_enum,
|
||||
logout_mode_enum,
|
||||
} = useDict('client_type', 'auth_type_enum', 'replaced_range_enum', 'logout_mode_enum')
|
||||
|
||||
const queryForm = reactive<ClientQuery>({
|
||||
clientType: '',
|
||||
@@ -89,7 +91,7 @@ const queryForm = reactive<ClientQuery>({
|
||||
})
|
||||
const formatAuthType = (data: string[]) => {
|
||||
return data.map((item: string) => {
|
||||
return auth_type_enum.value.find((d: LabelValue) => d.value === item).label
|
||||
return auth_type_enum.value?.find((d: LabelValue) => d.value === item)?.label
|
||||
})
|
||||
}
|
||||
|
||||
@@ -112,6 +114,7 @@ const columns: TableInstance['columns'] = [
|
||||
title: '客户端 ID',
|
||||
dataIndex: 'clientId',
|
||||
slotName: 'clientId',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
render: ({ record }) => {
|
||||
@@ -145,7 +148,7 @@ const columns: TableInstance['columns'] = [
|
||||
},
|
||||
},
|
||||
{ title: 'Token 最低活跃频率', dataIndex: 'activeTimeout', slotName: 'activeTimeout', width: 180, align: 'center', render: ({ record }) => `${record.activeTimeout} 秒` },
|
||||
{ title: 'Token 有效期', dataIndex: 'timeout', slotName: 'timeout', align: 'center', render: ({ record }) => `${record.timeout} 秒` },
|
||||
{ title: 'Token 有效期', dataIndex: 'timeout', slotName: 'timeout', width: 180, align: 'center', render: ({ record }) => `${record.timeout} 秒` },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
@@ -155,10 +158,39 @@ const columns: TableInstance['columns'] = [
|
||||
return <GiCellStatus status={record.status} />
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '多地登录',
|
||||
dataIndex: 'isConcurrent',
|
||||
align: 'center',
|
||||
render: ({ record }) => {
|
||||
return <Tag>{record.isConcurrent ? '允许' : '不允许'}</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '下线范围',
|
||||
dataIndex: 'replacedRange',
|
||||
align: 'center',
|
||||
render: ({ record }) => {
|
||||
return <GiCellTag value={record.replacedRange} dict={replaced_range_enum.value} />
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '登录数量',
|
||||
dataIndex: 'maxLoginCount',
|
||||
align: 'center',
|
||||
render: ({ record }) => {
|
||||
return record.maxLoginCount === -1 ? '不限制' : record.maxLoginCount
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '溢出处理',
|
||||
dataIndex: 'overflowLogoutMode',
|
||||
align: 'center',
|
||||
render: ({ record }) => {
|
||||
return <GiCellTag value={record.overflowLogoutMode} dict={logout_mode_enum.value} />
|
||||
},
|
||||
},
|
||||
{ title: '创建人', dataIndex: 'createUserString', width: 140, ellipsis: true, tooltip: true, show: false },
|
||||
{ title: '创建时间', dataIndex: 'createTime', width: 180 },
|
||||
{ title: '修改人', dataIndex: 'updateUserString', width: 140, ellipsis: true, tooltip: true, show: false },
|
||||
{ title: '修改时间', dataIndex: 'updateTime', width: 180, show: false },
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
|
||||
Reference in New Issue
Block a user