Compare commits

..

No commits in common. "c546d4635a1af8c6c5fa4e1a2f9caae4ab167566" and "dcda5fa528b26b8fe948f3bb4db0ab58cb49d51d" have entirely different histories.

424 changed files with 177 additions and 90792 deletions

View File

@ -1,313 +0,0 @@
import request from '@/utils/http'
// 扫描 API
export const scanApi = {
// 启动扫描
startScan(networkSegment: string, subnetMask: string) {
return request.post({
url: '/api/scan/start',
params: { networkSegment, subnetMask }
})
},
// 获取扫描状态
getScanStatus(taskId: string) {
return request.get({
url: `/api/scan/status/${taskId}`
})
},
// 取消扫描
cancelScan(taskId: string) {
return request.post({
url: `/api/scan/cancel/${taskId}`
})
}
}
// 设备 API
export const deviceApi = {
// 获取所有设备
getAllDevices() {
return request.get<any[]>({
url: '/api/devices'
})
},
// 搜索设备
searchDevices(keyword: string) {
return request.get<any[]>({
url: '/api/devices/search',
params: { keyword }
})
},
// 手动添加设备
addDevice(data: { ipAddress: string; hostname?: string; description?: string; windowsUsername?: string; windowsPassword?: string }) {
return request.post({
url: '/api/devices',
params: data,
showSuccessMessage: true
})
},
// 更新设备
updateDevice(id: number, data: { hostname?: string; description?: string }) {
return request.put({
url: `/api/devices/${id}`,
params: data,
showSuccessMessage: true
})
},
// 删除设备
deleteDevice(id: number) {
return request.del({
url: `/api/devices/${id}`,
showSuccessMessage: true
})
},
// 检测所有设备在线状态
checkAllDevicesStatus() {
return request.get<any[]>({
url: '/api/devices/status'
})
},
// 检测单个设备在线状态
checkDeviceStatus(id: number) {
return request.get({
url: `/api/devices/${id}/status`
})
},
// 设置设备 Windows 凭据
setDeviceCredentials(id: number, data: { username?: string; password?: string }) {
return request.put({
url: `/api/devices/${id}/credentials`,
params: data,
showSuccessMessage: true
})
},
// 获取设备 Windows 凭据
getDeviceCredentials(id: number) {
return request.get({
url: `/api/devices/${id}/credentials`
})
}
}
// 硬件信息 API
export const hardwareApi = {
// 获取设备硬件信息
getHardwareInfo(deviceId: number, refresh = false) {
return request.get({
url: `/api/hardware-info/${deviceId}`,
params: { refresh }
})
},
// 批量获取硬件信息
getBatchHardwareInfo(deviceIds: number[], refresh = false) {
return request.post({
url: '/api/hardware-info/batch',
params: { deviceIds, refresh }
})
}
}
// 电源管理 API
export const powerApi = {
// 获取电源状态
getPowerState(deviceId: number) {
return request.get({
url: `/api/power/${deviceId}/state`
})
},
// 开机
powerOn(deviceId: number) {
return request.post({
url: `/api/power/${deviceId}/power-on`
})
},
// 关机(优雅关机)
powerOff(deviceId: number) {
return request.post({
url: `/api/power/${deviceId}/power-off`
})
},
// 强制关机
forceOff(deviceId: number) {
return request.post({
url: `/api/power/${deviceId}/force-off`
})
},
// 重启(优雅重启)
restart(deviceId: number) {
return request.post({
url: `/api/power/${deviceId}/restart`
})
},
// 强制重启
forceRestart(deviceId: number) {
return request.post({
url: `/api/power/${deviceId}/force-restart`
})
},
// 电源循环
powerCycle(deviceId: number) {
return request.post({
url: `/api/power/${deviceId}/power-cycle`
})
}
}
// AMT 凭据 API
export const credentialApi = {
// 获取所有凭据
getAllCredentials() {
return request.get<any[]>({
url: '/api/credentials'
})
},
// 创建凭据
createCredential(data: { name: string; username: string; password: string; isDefault?: boolean; description?: string }) {
return request.post({
url: '/api/credentials',
params: data,
showSuccessMessage: true
})
},
// 更新凭据
updateCredential(id: number, data: { name?: string; username?: string; password?: string; isDefault?: boolean; description?: string }) {
return request.put({
url: `/api/credentials/${id}`,
params: data,
showSuccessMessage: true
})
},
// 删除凭据
deleteCredential(id: number) {
return request.del({
url: `/api/credentials/${id}`,
showSuccessMessage: true
})
}
}
// Windows 凭据 API
export const windowsCredentialsApi = {
// 获取所有凭据
getAll() {
return request.get<any[]>({
url: '/api/windowscredentials'
})
},
// 创建凭据
create(data: { name: string; username: string; password: string; domain?: string; isDefault?: boolean; description?: string }) {
return request.post({
url: '/api/windowscredentials',
params: data,
showSuccessMessage: true
})
},
// 更新凭据
update(id: number, data: { name?: string; username?: string; password?: string; domain?: string; isDefault?: boolean; description?: string }) {
return request.put({
url: `/api/windowscredentials/${id}`,
params: data,
showSuccessMessage: true
})
},
// 删除凭据
delete(id: number) {
return request.del({
url: `/api/windowscredentials/${id}`,
showSuccessMessage: true
})
},
// 设置默认凭据
setDefault(id: number) {
return request.post({
url: `/api/windowscredentials/${id}/set-default`,
showSuccessMessage: true
})
}
}
// 远程桌面 API
export const remoteDesktopApi = {
// 直接连接(需要凭据)
connect(deviceId: number, credentials: { username: string; password: string; domain?: string }) {
return request.post({
url: `/api/remote-desktop/connect/${deviceId}`,
params: credentials
})
},
// 生成访问 Token
generateToken(deviceId: number, options: { credentialId?: number; expiresInMinutes?: number; maxUseCount?: number; note?: string } = {}) {
return request.post({
url: `/api/remote-desktop/generate-token/${deviceId}`,
params: options
})
},
// 通过 Token 连接
connectByToken(token: string) {
return request.get({
url: `/api/remote-desktop/connect-by-token/${token}`
})
},
// 验证 Token
validateToken(token: string) {
return request.get({
url: `/api/remote-desktop/validate-token/${token}`
})
},
// 获取设备的所有 Token
getDeviceTokens(deviceId: number) {
return request.get<any[]>({
url: `/api/remote-desktop/list-tokens/${deviceId}`
})
},
// 撤销 Token
revokeToken(tokenId: number) {
return request.del({
url: `/api/remote-desktop/revoke-token/${tokenId}`,
showSuccessMessage: true
})
},
// 清理过期 Token
cleanupTokens() {
return request.post({
url: '/api/remote-desktop/cleanup-tokens'
})
},
// 测试 Guacamole 连接
test() {
return request.get({
url: '/api/remote-desktop/test'
})
}
}

View File

@ -64,56 +64,3 @@ export function fetchGetMenuList() {
url: '/api/v3/system/menus/simple' url: '/api/v3/system/menus/simple'
}) })
} }
// 创建菜单
export function fetchCreateMenu(data: {
parentId?: number | null
name: string
path: string
component?: string
title: string
icon?: string
sort?: number
isHide?: boolean
keepAlive?: boolean
link?: string
isIframe?: boolean
roles?: string[]
autoCreateComponent?: boolean // 是否自动创建组件文件
}) {
return request.post({
url: '/api/menu',
params: data,
showSuccessMessage: true
})
}
// 更新菜单
export function fetchUpdateMenu(id: number, data: {
parentId?: number | null
name?: string
path?: string
component?: string
title?: string
icon?: string
sort?: number
isHide?: boolean
keepAlive?: boolean
link?: string
isIframe?: boolean
roles?: string[]
}) {
return request.put({
url: `/api/menu/${id}`,
params: data,
showSuccessMessage: true
})
}
// 删除菜单
export function fetchDeleteMenu(id: number) {
return request.del({
url: `/api/menu/${id}`,
showSuccessMessage: true
})
}

View File

@ -129,23 +129,12 @@ async function handleRouteGuard(
NProgress.start() NProgress.start()
} }
// 1. 检查是否为静态路由(不需要权限验证) // 1. 检查登录状态
if (isStaticRoute(to.path)) {
// 静态路由直接放行,不需要登录验证和权限验证
if (to.matched.length > 0) {
setWorktab(to)
setPageTitle(to)
next()
return
}
}
// 2. 检查登录状态
if (!handleLoginStatus(to, userStore, next)) { if (!handleLoginStatus(to, userStore, next)) {
return return
} }
// 3. 处理动态路由注册 // 2. 处理动态路由注册
if (!routeRegistry?.isRegistered() && userStore.isLogin) { if (!routeRegistry?.isRegistered() && userStore.isLogin) {
await handleDynamicRoutes(to, next, router) await handleDynamicRoutes(to, next, router)
return return

View File

@ -11,13 +11,13 @@ import { AppRouteRecordRaw } from '@/utils/router'
* 2访 * 2访
*/ */
export const staticRoutes: AppRouteRecordRaw[] = [ export const staticRoutes: AppRouteRecordRaw[] = [
// 远程桌面访问页面(不需要登录) // 不需要登录就能访问的路由示例
{ // {
path: '/remote/:token', // path: '/welcome',
name: 'RemoteAccess', // name: 'WelcomeStatic',
component: () => import('@views/remote/index.vue'), // component: () => import('@views/dashboard/console/index.vue'),
meta: { title: '远程桌面', isHideTab: true } // meta: { title: 'menus.dashboard.title' }
}, // },
{ {
path: '/auth/login', path: '/auth/login',
name: 'Login', name: 'Login',

View File

@ -1,174 +0,0 @@
<template>
<div class="credentials-page">
<ElCard shadow="never">
<template #header>
<div class="card-header">
<span>AMT 凭据管理</span>
<ElButton type="primary" @click="showAddDialog">
<el-icon><Plus /></el-icon>
添加凭据
</ElButton>
</div>
</template>
<ElTable :data="credentials" v-loading="loading" style="width: 100%">
<ElTableColumn prop="name" label="名称" width="180" />
<ElTableColumn prop="username" label="用户名" width="150" />
<ElTableColumn label="密码" width="120">
<template #default="{ row }">
<ElTag v-if="row.hasPassword" type="success" size="small">已设置</ElTag>
<ElTag v-else type="danger" size="small">未设置</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="默认" width="100">
<template #default="{ row }">
<ElTag v-if="row.isDefault" type="primary" size="small">默认</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="description" label="描述" />
<ElTableColumn label="操作" width="180">
<template #default="{ row }">
<ElButton size="small" @click="showEditDialog(row)">编辑</ElButton>
<ElButton size="small" type="danger" @click="handleDelete(row)">删除</ElButton>
</template>
</ElTableColumn>
</ElTable>
</ElCard>
<!-- 添加/编辑对话框 -->
<ElDialog v-model="dialogVisible" :title="isEdit ? '编辑凭据' : '添加凭据'" width="500px">
<ElForm :model="form" label-width="100px">
<ElFormItem label="名称" required>
<ElInput v-model="form.name" placeholder="例如:默认凭据" />
</ElFormItem>
<ElFormItem label="用户名" required>
<ElInput v-model="form.username" placeholder="例如admin" />
</ElFormItem>
<ElFormItem label="密码" required>
<ElInput v-model="form.password" type="password" placeholder="请输入AMT密码" show-password />
<div v-if="isEdit" style="color: #909399; font-size: 12px; margin-top: 5px;">
留空则不修改密码
</div>
</ElFormItem>
<ElFormItem label="设为默认">
<ElSwitch v-model="form.isDefault" />
<div style="color: #909399; font-size: 12px; margin-top: 5px;">
默认凭据将用于扫描时的认证
</div>
</ElFormItem>
<ElFormItem label="描述">
<ElInput v-model="form.description" type="textarea" :rows="3" placeholder="可选,描述此凭据的用途" />
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit">确定</ElButton>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { credentialApi } from '@/api/amt'
defineOptions({ name: 'AmtCredentials' })
const credentials = ref<any[]>([])
const loading = ref(false)
const dialogVisible = ref(false)
const isEdit = ref(false)
const form = reactive({
id: null as number | null,
name: '',
username: '',
password: '',
isDefault: false,
description: ''
})
const loadCredentials = async () => {
loading.value = true
try {
credentials.value = await credentialApi.getAllCredentials()
} catch (error) {
ElMessage.error('加载凭据列表失败')
console.error(error)
} finally {
loading.value = false
}
}
const showAddDialog = () => {
isEdit.value = false
Object.assign(form, { id: null, name: '', username: '', password: '', isDefault: false, description: '' })
dialogVisible.value = true
}
const showEditDialog = (row: any) => {
isEdit.value = true
Object.assign(form, { id: row.id, name: row.name, username: row.username, password: '', isDefault: row.isDefault, description: row.description })
dialogVisible.value = true
}
const handleSubmit = async () => {
if (!form.name || !form.username) {
ElMessage.warning('请填写名称和用户名')
return
}
if (!isEdit.value && !form.password) {
ElMessage.warning('请填写密码')
return
}
try {
if (isEdit.value && form.id) {
await credentialApi.updateCredential(form.id, form)
} else {
await credentialApi.createCredential(form)
}
dialogVisible.value = false
loadCredentials()
} catch (error) {
ElMessage.error(isEdit.value ? '更新失败' : '添加失败')
console.error(error)
}
}
const handleDelete = async (row: any) => {
try {
await ElMessageBox.confirm(`确定要删除凭据 "${row.name}" 吗?`, '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await credentialApi.deleteCredential(row.id)
loadCredentials()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
console.error(error)
}
}
}
onMounted(() => {
loadCredentials()
})
</script>
<style scoped>
.credentials-page {
padding: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@ -1,373 +0,0 @@
<template>
<div class="devices-page">
<ElCard shadow="never">
<template #header>
<div class="card-header">
<span>AMT 设备管理</span>
<div class="header-actions">
<ElTag v-if="isCheckingStatus" type="info" size="small" style="margin-right: 10px">
<el-icon class="is-loading"><Refresh /></el-icon>
检测中...
</ElTag>
<ElInput
v-model="searchKeyword"
placeholder="搜索 IP 地址"
style="width: 200px; margin-right: 10px"
clearable
@clear="handleSearch"
@keyup.enter="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</ElInput>
<ElButton type="primary" :icon="Refresh" @click="handleRefresh">刷新</ElButton>
</div>
</div>
</template>
<ElTable :data="devices" v-loading="loading" stripe style="width: 100%">
<ElTableColumn prop="ipAddress" label="IP 地址" width="140" />
<ElTableColumn label="AMT 版本" width="100">
<template #default="{ row }">
{{ row.majorVersion }}.{{ row.minorVersion }}
</template>
</ElTableColumn>
<ElTableColumn label="配置状态" width="100">
<template #default="{ row }">
<ElTag :type="getStateTagType(row.provisioningState)" size="small">
{{ getProvisioningStateText(row.provisioningState) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="AMT状态" width="90">
<template #default="{ row }">
<ElTag :type="row.amtOnline ? 'success' : 'info'" size="small">
{{ row.amtOnline ? '在线' : '离线' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="系统状态" width="90">
<template #default="{ row }">
<ElTag :type="row.osOnline ? 'success' : 'danger'" size="small">
{{ row.osOnline ? '运行中' : '已关机' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="Windows账号" width="120">
<template #default="{ row }">
<ElTag v-if="row.windowsUsername" type="success" size="small">{{ row.windowsUsername }}</ElTag>
<ElTag v-else type="warning" size="small">未配置</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="hostname" label="主机名" min-width="120" />
<ElTableColumn label="发现时间" width="160">
<template #default="{ row }">
{{ formatDateTime(row.discoveredAt) }}
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="420" fixed="right">
<template #default="{ row }">
<ElButton type="success" size="small" @click="handleRemoteDesktop(row)" :disabled="!row.osOnline || !row.windowsUsername">
远程桌面
</ElButton>
<ElButton type="info" size="small" @click="handleSetCredentials(row)">
配置账号
</ElButton>
<ElDropdown trigger="click" @command="(cmd: string) => handlePowerCommand(cmd, row)" style="margin-left: 8px">
<ElButton type="warning" size="small">
电源管理 <el-icon class="el-icon--right"><ArrowDown /></el-icon>
</ElButton>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem command="power-on" :icon="VideoPlay">开机</ElDropdownItem>
<ElDropdownItem command="power-off" :icon="VideoPause">关机</ElDropdownItem>
<ElDropdownItem command="restart" :icon="RefreshRight">重启</ElDropdownItem>
<ElDropdownItem divided command="force-off" :icon="CircleClose">强制关机</ElDropdownItem>
<ElDropdownItem command="force-restart" :icon="Refresh">强制重启</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
<ElButton type="primary" size="small" @click="handleViewHardware(row)" style="margin-left: 8px">
硬件配置
</ElButton>
<ElButton type="danger" size="small" @click="handleDelete(row)">
删除
</ElButton>
</template>
</ElTableColumn>
</ElTable>
</ElCard>
<!-- 硬件信息弹窗 -->
<HardwareInfoModal v-model="showHardwareModal" :device-id="selectedDeviceId" />
<!-- 远程桌面弹窗 -->
<RemoteDesktopModal v-model="showRemoteDesktopModal" :device="selectedDevice" />
<!-- 配置 Windows 账号弹窗 -->
<ElDialog v-model="showCredentialsDialog" title="配置 Windows 登录账号" width="450px">
<ElForm :model="credentialsForm" label-width="100px">
<ElFormItem label="设备 IP">
<ElInput :model-value="selectedDevice?.ipAddress" disabled />
</ElFormItem>
<ElFormItem label="用户名">
<ElInput v-model="credentialsForm.username" placeholder="Windows 登录用户名" />
</ElFormItem>
<ElFormItem label="密码">
<ElInput v-model="credentialsForm.password" type="password" placeholder="Windows 登录密码" show-password />
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="showCredentialsDialog = false">取消</ElButton>
<ElButton type="primary" @click="saveCredentials" :loading="savingCredentials">保存</ElButton>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, ArrowDown, VideoPlay, VideoPause, RefreshRight, CircleClose } from '@element-plus/icons-vue'
import { deviceApi, powerApi } from '@/api/amt'
import HardwareInfoModal from './modules/hardware-info-modal.vue'
import RemoteDesktopModal from './modules/remote-desktop-modal.vue'
defineOptions({ name: 'AmtDevices' })
const devices = ref<any[]>([])
const loading = ref(false)
const isCheckingStatus = ref(false)
const searchKeyword = ref('')
const showHardwareModal = ref(false)
const showRemoteDesktopModal = ref(false)
const showCredentialsDialog = ref(false)
const selectedDeviceId = ref(0)
const selectedDevice = ref<any>(null)
const credentialsForm = ref({ username: '', password: '' })
const savingCredentials = ref(false)
let statusCheckInterval: number | null = null
onMounted(() => {
fetchDevices()
startStatusCheck()
})
onUnmounted(() => {
stopStatusCheck()
})
const fetchDevices = async () => {
loading.value = true
try {
devices.value = await deviceApi.getAllDevices()
} catch (error) {
console.error('获取设备列表失败:', error)
} finally {
loading.value = false
}
}
const handleSearch = async () => {
if (searchKeyword.value) {
loading.value = true
try {
devices.value = await deviceApi.searchDevices(searchKeyword.value)
} catch (error) {
console.error('搜索设备失败:', error)
} finally {
loading.value = false
}
} else {
fetchDevices()
}
}
const handleRefresh = async () => {
await fetchDevices()
await checkAllDevicesStatus()
ElMessage.success('刷新成功')
}
const checkAllDevicesStatus = async () => {
if (isCheckingStatus.value || devices.value.length === 0) return
isCheckingStatus.value = true
try {
const statusList = await deviceApi.checkAllDevicesStatus()
const statusMap = new Map(statusList.map((s: any) => [s.id, { amtOnline: s.amtOnline, osOnline: s.osOnline }]))
devices.value.forEach(device => {
if (statusMap.has(device.id)) {
const status = statusMap.get(device.id)
device.amtOnline = status.amtOnline
device.osOnline = status.osOnline
}
})
} catch (error) {
console.error('检测设备状态失败:', error)
} finally {
isCheckingStatus.value = false
}
}
const startStatusCheck = () => {
checkAllDevicesStatus()
statusCheckInterval = window.setInterval(() => {
checkAllDevicesStatus()
}, 30000)
}
const stopStatusCheck = () => {
if (statusCheckInterval) {
clearInterval(statusCheckInterval)
statusCheckInterval = null
}
}
const handleDelete = async (device: any) => {
try {
await ElMessageBox.confirm(`确定要删除设备 ${device.ipAddress} 吗?`, '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await deviceApi.deleteDevice(device.id)
fetchDevices()
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
const handleViewHardware = (device: any) => {
selectedDeviceId.value = device.id
showHardwareModal.value = true
}
const handleRemoteDesktop = (device: any) => {
if (!device.osOnline) {
ElMessage.warning('设备操作系统未运行,无法连接远程桌面')
return
}
if (!device.windowsUsername) {
ElMessage.warning('请先配置该设备的 Windows 登录账号')
return
}
selectedDevice.value = device
showRemoteDesktopModal.value = true
}
const handleSetCredentials = async (device: any) => {
selectedDevice.value = device
credentialsForm.value = { username: device.windowsUsername || '', password: '' }
showCredentialsDialog.value = true
}
const saveCredentials = async () => {
if (!credentialsForm.value.username) {
ElMessage.warning('请输入用户名')
return
}
savingCredentials.value = true
try {
await deviceApi.setDeviceCredentials(selectedDevice.value.id, credentialsForm.value)
//
const device = devices.value.find(d => d.id === selectedDevice.value.id)
if (device) {
device.windowsUsername = credentialsForm.value.username
}
showCredentialsDialog.value = false
ElMessage.success('账号配置成功')
} catch (error) {
ElMessage.error('保存失败')
} finally {
savingCredentials.value = false
}
}
const handlePowerCommand = async (command: string, device: any) => {
const actionMap: Record<string, { api: Function; name: string; confirmMsg: string }> = {
'power-on': { api: powerApi.powerOn, name: '开机', confirmMsg: '确定要开机吗?' },
'power-off': { api: powerApi.powerOff, name: '关机', confirmMsg: '确定要关机吗?这将优雅地关闭操作系统。' },
'restart': { api: powerApi.restart, name: '重启', confirmMsg: '确定要重启吗?这将优雅地重启操作系统。' },
'force-off': { api: powerApi.forceOff, name: '强制关机', confirmMsg: '确定要强制关机吗?这可能导致数据丢失!' },
'force-restart': { api: powerApi.forceRestart, name: '强制重启', confirmMsg: '确定要强制重启吗?这可能导致数据丢失!' }
}
const action = actionMap[command]
if (!action) return
try {
await ElMessageBox.confirm(`设备: ${device.ipAddress}\n${action.confirmMsg}`, `确认${action.name}`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: command.includes('force') ? 'warning' : 'info'
})
ElMessage.info(`正在执行${action.name}...`)
const response = await action.api(device.id)
if (response.success) {
ElMessage.success(response.message || `${action.name}命令已发送`)
setTimeout(() => checkAllDevicesStatus(), 3000)
} else {
ElMessage.error(response.error || `${action.name}失败`)
}
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(`${action.name}失败`)
}
}
}
const getProvisioningStateText = (state: string) => {
const stateMap: Record<string, string> = {
'PRE': '预配置',
'IN': '配置中',
'POST': '已配置',
'UNKNOWN': '未知'
}
return stateMap[state] || state
}
const getStateTagType = (state: string) => {
const typeMap: Record<string, string> = {
'PRE': 'warning',
'IN': 'primary',
'POST': 'success',
'UNKNOWN': 'info'
}
return typeMap[state] || 'info'
}
const formatDateTime = (dateTime: string) => {
if (!dateTime) return '-'
return new Date(dateTime).toLocaleString('zh-CN')
}
</script>
<style scoped>
.devices-page {
padding: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: 500;
}
.header-actions {
display: flex;
align-items: center;
}
</style>

View File

@ -1,204 +0,0 @@
<template>
<ElDialog
v-model="visible"
title="硬件配置信息"
width="800px"
:before-close="handleClose"
>
<div v-loading="loading">
<ElAlert
v-if="error"
type="error"
:title="error"
:closable="false"
style="margin-bottom: 20px"
/>
<div v-if="hardwareInfo && !error">
<!-- 系统信息 -->
<ElDescriptions title="系统信息" :column="2" border style="margin-bottom: 20px">
<ElDescriptionsItem label="IP 地址">
{{ hardwareInfo.ipAddress }}
</ElDescriptionsItem>
<ElDescriptionsItem label="最后更新">
{{ formatDateTime(hardwareInfo.lastUpdated) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="制造商" v-if="hardwareInfo.systemInfo?.manufacturer">
{{ hardwareInfo.systemInfo.manufacturer }}
</ElDescriptionsItem>
<ElDescriptionsItem label="型号" v-if="hardwareInfo.systemInfo?.model">
{{ hardwareInfo.systemInfo.model }}
</ElDescriptionsItem>
<ElDescriptionsItem label="序列号" v-if="hardwareInfo.systemInfo?.serialNumber" :span="2">
{{ hardwareInfo.systemInfo.serialNumber }}
</ElDescriptionsItem>
</ElDescriptions>
<!-- CPU 信息 -->
<ElDescriptions
v-if="hardwareInfo.processor"
title="处理器信息"
:column="2"
border
style="margin-bottom: 20px"
>
<ElDescriptionsItem label="型号" :span="2">
{{ hardwareInfo.processor.model || '信息不可用' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="核心数" v-if="hardwareInfo.processor.cores">
{{ hardwareInfo.processor.cores }}
</ElDescriptionsItem>
<ElDescriptionsItem label="线程数" v-if="hardwareInfo.processor.threads">
{{ hardwareInfo.processor.threads }}
</ElDescriptionsItem>
<ElDescriptionsItem label="最大频率" v-if="hardwareInfo.processor.maxClockSpeed">
{{ hardwareInfo.processor.maxClockSpeed }} MHz
</ElDescriptionsItem>
<ElDescriptionsItem label="当前频率" v-if="hardwareInfo.processor.currentClockSpeed">
{{ hardwareInfo.processor.currentClockSpeed }} MHz
</ElDescriptionsItem>
</ElDescriptions>
<!-- 内存信息 -->
<div v-if="hardwareInfo.memory" style="margin-bottom: 20px">
<h3 style="margin-bottom: 10px">内存信息</h3>
<ElDescriptions :column="1" border style="margin-bottom: 10px">
<ElDescriptionsItem label="总容量">
{{ hardwareInfo.memory.totalCapacityGB ? hardwareInfo.memory.totalCapacityGB + ' GB' : '信息不可用' }}
</ElDescriptionsItem>
</ElDescriptions>
<ElTable
v-if="hardwareInfo.memory.modules && hardwareInfo.memory.modules.length > 0"
:data="hardwareInfo.memory.modules"
border
style="width: 100%"
>
<ElTableColumn prop="slot" label="插槽" width="100" />
<ElTableColumn label="容量" width="100">
<template #default="{ row }">
{{ row.capacityGB ? row.capacityGB + ' GB' : '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="频率" width="100">
<template #default="{ row }">
{{ row.speed ? row.speed + ' MHz' : '-' }}
</template>
</ElTableColumn>
<ElTableColumn prop="type" label="类型" width="100" />
<ElTableColumn prop="manufacturer" label="制造商" />
<ElTableColumn prop="partNumber" label="型号" />
</ElTable>
<ElEmpty v-else description="无内存模块信息" :image-size="80" />
</div>
<!-- 存储信息 -->
<div v-if="hardwareInfo.storage">
<h3 style="margin-bottom: 10px">存储设备</h3>
<ElTable
v-if="hardwareInfo.storage.devices && hardwareInfo.storage.devices.length > 0"
:data="hardwareInfo.storage.devices"
border
style="width: 100%"
>
<ElTableColumn prop="deviceId" label="设备 ID" width="120" />
<ElTableColumn prop="model" label="型号" />
<ElTableColumn label="容量" width="120">
<template #default="{ row }">
{{ formatCapacity(row.capacity, row.capacityGB) }}
</template>
</ElTableColumn>
<ElTableColumn prop="interfaceType" label="接口类型" width="120" />
</ElTable>
<ElEmpty v-else description="无存储设备信息" :image-size="80" />
</div>
</div>
</div>
<template #footer>
<ElButton @click="handleClose">关闭</ElButton>
<ElButton type="primary" @click="handleRefresh" :loading="loading">刷新</ElButton>
</template>
</ElDialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { hardwareApi } from '@/api/amt'
interface Props {
modelValue: boolean
deviceId: number
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false
})
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
}>()
const visible = ref(props.modelValue)
const loading = ref(false)
const error = ref('')
const hardwareInfo = ref<any>(null)
watch(() => props.modelValue, (newVal) => {
visible.value = newVal
if (newVal) {
fetchHardwareInfo(false)
}
})
watch(visible, (newVal) => {
emit('update:modelValue', newVal)
})
const fetchHardwareInfo = async (refresh = false) => {
loading.value = true
error.value = ''
try {
hardwareInfo.value = await hardwareApi.getHardwareInfo(props.deviceId, refresh)
} catch (err: any) {
error.value = err.message || '获取硬件信息失败'
ElMessage.error(error.value)
} finally {
loading.value = false
}
}
const handleRefresh = () => {
fetchHardwareInfo(true)
}
const handleClose = () => {
visible.value = false
}
const formatDateTime = (dateTime: string) => {
if (!dateTime) return '-'
return new Date(dateTime).toLocaleString('zh-CN')
}
const formatCapacity = (bytes: number, gb: number) => {
if (gb) {
return gb >= 1024 ? `${(gb / 1024).toFixed(2)} TB` : `${gb} GB`
}
if (bytes) {
const gbValue = bytes / (1024 * 1024 * 1024)
return gbValue >= 1024 ? `${(gbValue / 1024).toFixed(2)} TB` : `${gbValue.toFixed(2)} GB`
}
return '-'
}
</script>
<style scoped>
h3 {
font-size: 16px;
font-weight: bold;
color: #303133;
}
</style>

View File

@ -1,248 +0,0 @@
<template>
<ElDialog
v-model="visible"
:title="`远程桌面 - ${device?.ipAddress || ''}`"
width="95%"
:fullscreen="isFullscreen"
:close-on-click-modal="false"
:close-on-press-escape="false"
@close="handleClose"
class="remote-desktop-dialog"
>
<template #header>
<div class="dialog-header">
<span>远程桌面 - {{ device?.ipAddress }}</span>
<div class="header-actions">
<ElButton v-if="connectionUrl" type="danger" size="small" @click="handleDisconnect">
断开连接
</ElButton>
<ElButton v-if="connectionUrl" :type="isFullscreen ? 'warning' : 'primary'" size="small" @click="toggleFullscreen" style="margin-left: 10px">
{{ isFullscreen ? '退出全屏' : '全屏显示' }}
</ElButton>
</div>
</div>
</template>
<!-- 连接方式选择 -->
<div v-if="!connectionUrl" class="connection-options">
<ElTabs v-model="activeTab">
<ElTabPane label="快速连接" name="quick">
<div class="quick-connect">
<ElAlert type="info" :closable="false" style="margin-bottom: 20px">
使用设备配置的 Windows 账号快速连接
</ElAlert>
<div v-if="device?.windowsUsername" style="margin-bottom: 20px">
<ElTag type="success">当前账号: {{ device.windowsUsername }}</ElTag>
</div>
<ElButton type="primary" size="large" @click="quickConnect" :loading="connecting">
一键连接
</ElButton>
</div>
</ElTabPane>
<ElTabPane label="生成分享链接" name="share">
<div class="share-form">
<ElForm :model="tokenForm" label-width="120px">
<ElFormItem label="有效期(分钟)">
<ElInputNumber v-model="tokenForm.expiresInMinutes" :min="5" :max="1440" />
</ElFormItem>
<ElFormItem label="最大使用次数">
<ElInputNumber v-model="tokenForm.maxUseCount" :min="1" :max="100" />
</ElFormItem>
<ElFormItem label="备注">
<ElInput v-model="tokenForm.note" placeholder="可选备注" />
</ElFormItem>
<ElFormItem>
<ElButton type="primary" @click="generateToken" :loading="generating">生成链接</ElButton>
</ElFormItem>
</ElForm>
<div v-if="generatedToken" class="generated-link">
<ElAlert type="success" :closable="false">
<template #title>链接已生成有效期至 {{ formatDate(generatedToken.expiresAt) }}</template>
</ElAlert>
<div class="link-box">
<ElInput v-model="generatedToken.accessUrl" readonly>
<template #append><ElButton @click="copyLink">复制</ElButton></template>
</ElInput>
</div>
</div>
</div>
</ElTabPane>
<ElTabPane label="链接管理" name="tokens">
<div class="tokens-list">
<ElTable :data="deviceTokens" v-loading="loadingTokens" size="small">
<ElTableColumn label="创建时间" width="160">
<template #default="{ row }">{{ formatDate(row.createdAt) }}</template>
</ElTableColumn>
<ElTableColumn label="过期时间" width="160">
<template #default="{ row }">{{ formatDate(row.expiresAt) }}</template>
</ElTableColumn>
<ElTableColumn label="使用情况" width="100">
<template #default="{ row }">{{ row.useCount }} / {{ row.maxUseCount || '∞' }}</template>
</ElTableColumn>
<ElTableColumn prop="note" label="备注" />
<ElTableColumn label="操作" width="150">
<template #default="{ row }">
<ElButton size="small" @click="copyTokenLink(row)">复制</ElButton>
<ElButton size="small" type="danger" @click="revokeToken(row)">撤销</ElButton>
</template>
</ElTableColumn>
</ElTable>
</div>
</ElTabPane>
</ElTabs>
</div>
<!-- 远程桌面 iframe -->
<div v-else class="remote-desktop-container">
<iframe ref="rdpFrame" :src="connectionUrl" class="rdp-iframe" allowfullscreen />
</div>
</ElDialog>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { remoteDesktopApi } from '@/api/amt'
interface Props {
modelValue: boolean
device: any
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
}>()
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const activeTab = ref('quick')
const connectionUrl = ref('')
const connecting = ref(false)
const generating = ref(false)
const isFullscreen = ref(false)
const rdpFrame = ref<HTMLIFrameElement | null>(null)
const tokenForm = ref({ expiresInMinutes: 30, maxUseCount: 1, note: '' })
const generatedToken = ref<any>(null)
const deviceTokens = ref<any[]>([])
const loadingTokens = ref(false)
const loadDeviceTokens = async () => {
if (!props.device?.id) return
loadingTokens.value = true
try {
deviceTokens.value = await remoteDesktopApi.getDeviceTokens(props.device.id)
} catch (error) {
console.error('加载 Token 列表失败', error)
} finally {
loadingTokens.value = false
}
}
const quickConnect = async () => {
connecting.value = true
try {
const tokenResponse = await remoteDesktopApi.generateToken(props.device.id, { expiresInMinutes: 60, maxUseCount: 1 })
if (!tokenResponse.success) {
ElMessage.error(tokenResponse.error || '生成连接失败')
return
}
const connectResponse = await remoteDesktopApi.connectByToken(tokenResponse.token)
if (connectResponse.success) {
connectionUrl.value = connectResponse.connectionUrl
ElMessage.success('正在连接远程桌面...')
} else {
ElMessage.error(connectResponse.error || '连接失败')
}
} catch (error: any) {
ElMessage.error(error.message || '连接失败,请先配置设备的 Windows 登录账号')
} finally {
connecting.value = false
}
}
const generateToken = async () => {
generating.value = true
try {
const response = await remoteDesktopApi.generateToken(props.device.id, {
expiresInMinutes: tokenForm.value.expiresInMinutes,
maxUseCount: tokenForm.value.maxUseCount,
note: tokenForm.value.note || undefined
})
if (response.success) {
response.accessUrl = `${window.location.origin}/#/remote/${response.token}`
generatedToken.value = response
ElMessage.success('链接已生成')
loadDeviceTokens()
} else {
ElMessage.error(response.error || '生成失败')
}
} catch (error: any) {
ElMessage.error(error.message || '生成链接失败')
} finally {
generating.value = false
}
}
const copyLink = async () => {
if (generatedToken.value?.accessUrl) {
await navigator.clipboard.writeText(generatedToken.value.accessUrl)
ElMessage.success('链接已复制到剪贴板')
}
}
const copyTokenLink = async (token: any) => {
const url = `${window.location.origin}/#/remote/${token.token}`
await navigator.clipboard.writeText(url)
ElMessage.success('链接已复制')
}
const revokeToken = async (token: any) => {
try {
await ElMessageBox.confirm('确定要撤销此链接吗?', '确认撤销', { type: 'warning' })
await remoteDesktopApi.revokeToken(token.id)
ElMessage.success('链接已撤销')
loadDeviceTokens()
} catch (error) {
if (error !== 'cancel') ElMessage.error('撤销失败')
}
}
const formatDate = (dateStr: string) => {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleString('zh-CN')
}
const toggleFullscreen = () => { isFullscreen.value = !isFullscreen.value }
const handleDisconnect = () => { connectionUrl.value = ''; ElMessage.info('已断开远程桌面连接') }
const handleClose = () => { connectionUrl.value = ''; visible.value = false }
watch(() => props.modelValue, (newVal) => {
if (newVal) {
connectionUrl.value = ''
generatedToken.value = null
activeTab.value = 'quick'
loadDeviceTokens()
}
})
</script>
<style scoped>
.dialog-header { display: flex; justify-content: space-between; align-items: center; }
.header-actions { margin-right: 40px; display: flex; align-items: center; }
.connection-options { padding: 20px; }
.quick-connect { text-align: center; padding: 40px 20px; }
.share-form { max-width: 500px; margin: 0 auto; padding: 20px; }
.generated-link { margin-top: 20px; }
.link-box { margin-top: 10px; }
.tokens-list { padding: 10px; }
.remote-desktop-container { width: 100%; height: calc(85vh - 100px); min-height: 600px; display: flex; justify-content: center; align-items: center; background: #1a1a1a; }
.rdp-iframe { width: 100%; height: 100%; border: none; background: transparent; }
:deep(.el-dialog__body) { padding: 0; overflow: hidden; }
:deep(.el-dialog__header) { padding: 15px 20px; margin: 0; border-bottom: 1px solid #e4e7ed; }
</style>

View File

@ -1,333 +0,0 @@
<template>
<div class="scan-page">
<!-- 添加方式选择 -->
<ElCard shadow="never">
<template #header>
<div class="card-header">
<span>设备添加</span>
</div>
</template>
<ElTabs v-model="activeTab">
<!-- 网络扫描 -->
<ElTabPane label="网络扫描" name="scan">
<ElForm :model="scanForm" :rules="scanRules" ref="scanFormRef" label-width="120px">
<ElRow :gutter="20">
<ElCol :span="8">
<ElFormItem label="网段地址" prop="networkSegment">
<ElInput v-model="scanForm.networkSegment" placeholder="例如: 192.168.1.0" />
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="子网掩码" prop="subnetMask">
<ElInput v-model="scanForm.subnetMask" placeholder="例如: 255.255.255.0 或 /24" />
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem>
<ElButton type="primary" @click="handleStartScan" :loading="scanning">
<el-icon><Search /></el-icon>
开始扫描
</ElButton>
<ElButton @click="handleResetScan">重置</ElButton>
</ElFormItem>
</ElCol>
</ElRow>
</ElForm>
<ElAlert
title="提示"
type="info"
description="扫描将检测指定网段内所有支持 Intel AMT 的设备,请确保网络连接正常。"
:closable="false"
show-icon
style="margin-top: 10px"
/>
</ElTabPane>
<!-- 手动添加 -->
<ElTabPane label="手动添加" name="manual">
<ElForm :model="manualForm" :rules="manualRules" ref="manualFormRef" label-width="120px" style="max-width: 600px">
<ElFormItem label="IP 地址" prop="ipAddress">
<ElInput v-model="manualForm.ipAddress" placeholder="设备 IP 地址" />
</ElFormItem>
<ElFormItem label="主机名">
<ElInput v-model="manualForm.hostname" placeholder="可选,设备主机名" />
</ElFormItem>
<ElFormItem label="备注">
<ElInput v-model="manualForm.description" type="textarea" :rows="2" placeholder="可选,设备备注信息" />
</ElFormItem>
<ElFormItem>
<ElButton type="primary" @click="handleAddDevice" :loading="adding">添加设备</ElButton>
<ElButton @click="handleResetManual">重置</ElButton>
</ElFormItem>
</ElForm>
</ElTabPane>
</ElTabs>
</ElCard>
<!-- 扫描进度 -->
<ElCard v-if="scanning || scanProgress.status === 'completed'" class="progress-card" shadow="never">
<template #header>
<div class="card-header">
<span>扫描进度</span>
<ElButton v-if="scanning" type="danger" size="small" @click="handleCancelScan">
取消扫描
</ElButton>
</div>
</template>
<div class="progress-info">
<ElProgress
:percentage="scanProgress.progressPercentage"
:status="progressStatus"
:stroke-width="20"
/>
<div class="progress-stats">
<ElStatistic title="已扫描" :value="scanProgress.scannedCount" />
<ElStatistic title="总数量" :value="scanProgress.totalCount" />
<ElStatistic title="发现设备" :value="scanProgress.foundDevices" />
</div>
</div>
<ElResult
v-if="scanProgress.status === 'completed'"
icon="success"
:title="`扫描完成,发现 ${scanProgress.foundDevices} 个 AMT 设备`"
>
<template #extra>
<ElButton type="primary" @click="goToDeviceList">查看设备列表</ElButton>
</template>
</ElResult>
</ElCard>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
import { scanApi, deviceApi } from '@/api/amt'
defineOptions({ name: 'AmtScan' })
const router = useRouter()
const activeTab = ref('scan')
const scanFormRef = ref()
const manualFormRef = ref()
const scanning = ref(false)
const adding = ref(false)
//
const scanForm = reactive({
networkSegment: '192.168.1.0',
subnetMask: '255.255.255.0'
})
//
const manualForm = reactive({
ipAddress: '',
hostname: '',
description: ''
})
const scanProgress = reactive({
taskId: '',
scannedCount: 0,
totalCount: 0,
foundDevices: 0,
progressPercentage: 0,
status: 'idle' as 'idle' | 'running' | 'completed' | 'cancelled'
})
const progressStatus = computed(() => {
if (scanProgress.status === 'completed') return 'success'
if (scanProgress.status === 'cancelled') return 'exception'
return undefined
})
const validateIp = (_rule: any, value: string, callback: Function) => {
const ipRegex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
if (!value) {
callback(new Error('请输入 IP 地址'))
} else if (!ipRegex.test(value)) {
callback(new Error('请输入有效的 IP 地址'))
} else {
callback()
}
}
const validateSubnetMask = (_rule: any, value: string, callback: Function) => {
if (!value) {
callback(new Error('请输入子网掩码'))
} else if (value.startsWith('/')) {
const cidr = parseInt(value.substring(1))
if (isNaN(cidr) || cidr < 0 || cidr > 32) {
callback(new Error('CIDR 格式无效,应为 /0 到 /32'))
} else {
callback()
}
} else {
const maskRegex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
if (!maskRegex.test(value)) {
callback(new Error('请输入有效的子网掩码'))
} else {
callback()
}
}
}
const scanRules = {
networkSegment: [{ validator: validateIp, trigger: 'blur' }],
subnetMask: [{ validator: validateSubnetMask, trigger: 'blur' }]
}
const manualRules = {
ipAddress: [{ validator: validateIp, trigger: 'blur' }]
}
let pollTimer: number | null = null
//
const handleStartScan = async () => {
if (!scanFormRef.value) return
await scanFormRef.value.validate(async (valid: boolean) => {
if (valid) {
scanning.value = true
scanProgress.status = 'running'
scanProgress.scannedCount = 0
scanProgress.totalCount = 0
scanProgress.foundDevices = 0
scanProgress.progressPercentage = 0
try {
const result = await scanApi.startScan(scanForm.networkSegment, scanForm.subnetMask)
scanProgress.taskId = result.taskId
ElMessage.success('扫描任务已启动')
startPolling()
} catch (error) {
scanning.value = false
scanProgress.status = 'idle'
ElMessage.error('启动扫描失败')
}
}
})
}
const startPolling = () => {
pollTimer = window.setInterval(async () => {
try {
const status = await scanApi.getScanStatus(scanProgress.taskId)
scanProgress.scannedCount = status.scannedCount || 0
scanProgress.totalCount = status.totalCount || 0
scanProgress.foundDevices = status.foundDevices || 0
scanProgress.progressPercentage = status.totalCount > 0
? Math.round((status.scannedCount / status.totalCount) * 100)
: 0
if (status.status === 'completed' || status.scannedCount >= status.totalCount) {
stopPolling()
scanning.value = false
scanProgress.status = 'completed'
scanProgress.progressPercentage = 100
ElMessage.success(`扫描完成,发现 ${scanProgress.foundDevices} 个 AMT 设备`)
}
} catch (error) {
console.error('获取扫描状态失败:', error)
}
}, 1000)
}
const stopPolling = () => {
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
}
const handleCancelScan = async () => {
try {
await scanApi.cancelScan(scanProgress.taskId)
stopPolling()
scanning.value = false
scanProgress.status = 'cancelled'
ElMessage.warning('扫描已取消')
} catch (error) {
ElMessage.error('取消扫描失败')
}
}
const handleResetScan = () => {
scanFormRef.value?.resetFields()
}
//
const handleAddDevice = async () => {
if (!manualFormRef.value) return
await manualFormRef.value.validate(async (valid: boolean) => {
if (valid) {
adding.value = true
try {
await deviceApi.addDevice({
ipAddress: manualForm.ipAddress,
hostname: manualForm.hostname || undefined,
description: manualForm.description || undefined
})
ElMessage.success('设备添加成功')
handleResetManual()
goToDeviceList()
} catch (error: any) {
ElMessage.error(error.message || '添加设备失败')
} finally {
adding.value = false
}
}
})
}
const handleResetManual = () => {
manualFormRef.value?.resetFields()
manualForm.ipAddress = ''
manualForm.hostname = ''
manualForm.description = ''
}
const goToDeviceList = () => {
router.push('/amt/devices')
}
onUnmounted(() => {
stopPolling()
})
</script>
<style scoped>
.scan-page {
padding: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: 500;
}
.progress-card {
margin-top: 20px;
}
.progress-info {
padding: 20px 0;
}
.progress-stats {
display: flex;
justify-content: space-around;
margin-top: 30px;
}
</style>

View File

@ -1,201 +0,0 @@
<template>
<div class="windows-credentials-page">
<ElCard shadow="never">
<template #header>
<div class="card-header">
<span>Windows 凭据管理</span>
<ElButton type="primary" @click="showAddDialog">
<el-icon><Plus /></el-icon>
添加凭据
</ElButton>
</div>
</template>
<ElTable :data="credentials" v-loading="loading" style="width: 100%">
<ElTableColumn prop="name" label="名称" width="180">
<template #default="{ row }">
{{ row.name }}
<ElTag v-if="row.isDefault" type="success" size="small" style="margin-left: 8px">默认</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="username" label="用户名" width="150" />
<ElTableColumn prop="domain" label="域名" width="120">
<template #default="{ row }">
{{ row.domain || '-' }}
</template>
</ElTableColumn>
<ElTableColumn prop="note" label="备注">
<template #default="{ row }">
{{ row.note || '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="240">
<template #default="{ row }">
<ElButton size="small" @click="showEditDialog(row)">编辑</ElButton>
<ElButton v-if="!row.isDefault" size="small" type="success" @click="handleSetDefault(row)">设为默认</ElButton>
<ElButton size="small" type="danger" @click="handleDelete(row)">删除</ElButton>
</template>
</ElTableColumn>
</ElTable>
</ElCard>
<!-- 添加/编辑对话框 -->
<ElDialog v-model="dialogVisible" :title="isEdit ? '编辑凭据' : '添加凭据'" width="500px">
<ElForm :model="form" label-width="100px">
<ElFormItem label="名称" required>
<ElInput v-model="form.name" placeholder="例如:服务器管理员" />
</ElFormItem>
<ElFormItem label="用户名" required>
<ElInput v-model="form.username" placeholder="Windows 用户名" />
</ElFormItem>
<ElFormItem label="密码" :required="!isEdit">
<ElInput v-model="form.password" type="password" :placeholder="isEdit ? '留空则不修改' : '密码'" show-password />
</ElFormItem>
<ElFormItem label="域名">
<ElInput v-model="form.domain" placeholder="可选WORKGROUP" />
</ElFormItem>
<ElFormItem label="设为默认">
<ElSwitch v-model="form.isDefault" />
<div style="color: #909399; font-size: 12px; margin-top: 5px;">
默认凭据将用于远程桌面连接
</div>
</ElFormItem>
<ElFormItem label="备注">
<ElInput v-model="form.note" type="textarea" :rows="2" placeholder="可选备注" />
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit" :loading="saving">确定</ElButton>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { windowsCredentialsApi } from '@/api/amt'
defineOptions({ name: 'WindowsCredentials' })
const credentials = ref<any[]>([])
const loading = ref(false)
const saving = ref(false)
const dialogVisible = ref(false)
const isEdit = ref(false)
const form = reactive({
id: null as number | null,
name: '',
username: '',
password: '',
domain: '',
isDefault: false,
note: ''
})
const loadCredentials = async () => {
loading.value = true
try {
credentials.value = await windowsCredentialsApi.getAll()
} catch (error) {
ElMessage.error('加载凭据列表失败')
console.error(error)
} finally {
loading.value = false
}
}
const showAddDialog = () => {
isEdit.value = false
Object.assign(form, { id: null, name: '', username: '', password: '', domain: '', isDefault: false, note: '' })
dialogVisible.value = true
}
const showEditDialog = (row: any) => {
isEdit.value = true
Object.assign(form, {
id: row.id,
name: row.name,
username: row.username,
password: '',
domain: row.domain || '',
isDefault: row.isDefault,
note: row.note || ''
})
dialogVisible.value = true
}
const handleSubmit = async () => {
if (!form.name || !form.username) {
ElMessage.warning('请填写名称和用户名')
return
}
if (!isEdit.value && !form.password) {
ElMessage.warning('请填写密码')
return
}
saving.value = true
try {
if (isEdit.value && form.id) {
await windowsCredentialsApi.update(form.id, form)
} else {
await windowsCredentialsApi.create(form)
}
dialogVisible.value = false
loadCredentials()
} catch (error) {
ElMessage.error(isEdit.value ? '更新失败' : '添加失败')
console.error(error)
} finally {
saving.value = false
}
}
const handleSetDefault = async (row: any) => {
try {
await windowsCredentialsApi.setDefault(row.id)
loadCredentials()
} catch (error) {
ElMessage.error('设置失败')
console.error(error)
}
}
const handleDelete = async (row: any) => {
try {
await ElMessageBox.confirm(`确定要删除凭据 "${row.name}" 吗?`, '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await windowsCredentialsApi.delete(row.id)
loadCredentials()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
console.error(error)
}
}
}
onMounted(() => {
loadCredentials()
})
</script>
<style scoped>
.windows-credentials-page {
padding: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@ -1,256 +0,0 @@
<template>
<div class="devices-page">
<ElCard shadow="never">
<template #header>
<div class="card-header">
<span>远程桌面</span>
<div class="header-actions">
<ElTag v-if="isCheckingStatus" type="info" size="small" style="margin-right: 10px">
<el-icon class="is-loading"><Refresh /></el-icon>
检测中...
</ElTag>
<ElInput
v-model="searchKeyword"
placeholder="搜索 IP 地址"
style="width: 200px; margin-right: 10px"
clearable
@clear="handleSearch"
@keyup.enter="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</ElInput>
<ElButton type="primary" :icon="Refresh" @click="handleRefresh">刷新</ElButton>
</div>
</div>
</template>
<ElTable :data="devices" v-loading="loading" stripe style="width: 100%">
<ElTableColumn prop="ipAddress" label="IP 地址" width="140" />
<ElTableColumn prop="hostname" label="主机名" width="150" />
<ElTableColumn label="AMT状态" width="90">
<template #default="{ row }">
<ElTag :type="row.amtOnline ? 'success' : 'info'" size="small">
{{ row.amtOnline ? '在线' : '离线' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="系统状态" width="90">
<template #default="{ row }">
<ElTag :type="row.osOnline ? 'success' : 'danger'" size="small">
{{ row.osOnline ? '运行中' : '已关机' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="Windows账号" width="120">
<template #default="{ row }">
<ElTag v-if="row.windowsUsername" type="success" size="small">{{ row.windowsUsername }}</ElTag>
<ElTag v-else type="warning" size="small">未配置</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="description" label="备注" min-width="150" />
<ElTableColumn label="操作" width="380" fixed="right">
<template #default="{ row }">
<ElButton type="success" size="small" @click="handleRemoteDesktop(row)" :disabled="!row.osOnline || !row.windowsUsername">
远程桌面
</ElButton>
<ElButton type="info" size="small" @click="handleSetCredentials(row)">
配置账号
</ElButton>
<ElDropdown trigger="click" @command="(cmd: string) => handlePowerCommand(cmd, row)" style="margin-left: 8px">
<ElButton type="warning" size="small">
电源管理 <el-icon class="el-icon--right"><ArrowDown /></el-icon>
</ElButton>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem command="power-on" :icon="VideoPlay">开机</ElDropdownItem>
<ElDropdownItem command="power-off" :icon="VideoPause">关机</ElDropdownItem>
<ElDropdownItem command="restart" :icon="RefreshRight">重启</ElDropdownItem>
<ElDropdownItem divided command="force-off" :icon="CircleClose">强制关机</ElDropdownItem>
<ElDropdownItem command="force-restart" :icon="Refresh">强制重启</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</template>
</ElTableColumn>
</ElTable>
</ElCard>
<!-- 远程桌面弹窗 -->
<RemoteDesktopModal v-model="showRemoteDesktopModal" :device="selectedDevice" />
<!-- 配置 Windows 账号弹窗 -->
<ElDialog v-model="showCredentialsDialog" title="配置 Windows 登录账号" width="450px">
<ElForm :model="credentialsForm" label-width="100px">
<ElFormItem label="设备 IP">
<ElInput :model-value="selectedDevice?.ipAddress" disabled />
</ElFormItem>
<ElFormItem label="用户名">
<ElInput v-model="credentialsForm.username" placeholder="Windows 登录用户名" />
</ElFormItem>
<ElFormItem label="密码">
<ElInput v-model="credentialsForm.password" type="password" placeholder="Windows 登录密码" show-password />
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="showCredentialsDialog = false">取消</ElButton>
<ElButton type="primary" @click="saveCredentials" :loading="savingCredentials">保存</ElButton>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, ArrowDown, VideoPlay, VideoPause, RefreshRight, CircleClose } from '@element-plus/icons-vue'
import { deviceApi, powerApi } from '@/api/amt'
import RemoteDesktopModal from '@/views/amt/modules/remote-desktop-modal.vue'
defineOptions({ name: 'DesktopManageDevices' })
const devices = ref<any[]>([])
const loading = ref(false)
const isCheckingStatus = ref(false)
const searchKeyword = ref('')
const showRemoteDesktopModal = ref(false)
const showCredentialsDialog = ref(false)
const selectedDevice = ref<any>(null)
const credentialsForm = ref({ username: '', password: '' })
const savingCredentials = ref(false)
let statusCheckInterval: number | null = null
onMounted(() => {
fetchDevices()
startStatusCheck()
})
onUnmounted(() => {
stopStatusCheck()
})
const fetchDevices = async () => {
loading.value = true
try {
devices.value = await deviceApi.getAllDevices()
} catch (error) {
console.error('获取设备列表失败:', error)
} finally {
loading.value = false
}
}
const handleSearch = async () => {
if (searchKeyword.value) {
loading.value = true
try {
devices.value = await deviceApi.searchDevices(searchKeyword.value)
} catch (error) {
console.error('搜索设备失败:', error)
} finally {
loading.value = false
}
} else {
fetchDevices()
}
}
const handleRefresh = async () => {
await fetchDevices()
await checkAllDevicesStatus()
ElMessage.success('刷新成功')
}
const checkAllDevicesStatus = async () => {
if (isCheckingStatus.value || devices.value.length === 0) return
isCheckingStatus.value = true
try {
const statusList = await deviceApi.checkAllDevicesStatus()
const statusMap = new Map(statusList.map((s: any) => [s.id, { amtOnline: s.amtOnline, osOnline: s.osOnline }]))
devices.value.forEach(device => {
if (statusMap.has(device.id)) {
const status = statusMap.get(device.id)
device.amtOnline = status.amtOnline
device.osOnline = status.osOnline
}
})
} catch (error) {
console.error('检测设备状态失败:', error)
} finally {
isCheckingStatus.value = false
}
}
const startStatusCheck = () => {
checkAllDevicesStatus()
statusCheckInterval = window.setInterval(() => checkAllDevicesStatus(), 30000)
}
const stopStatusCheck = () => {
if (statusCheckInterval) { clearInterval(statusCheckInterval); statusCheckInterval = null }
}
const handleRemoteDesktop = (device: any) => {
if (!device.osOnline) { ElMessage.warning('设备操作系统未运行,无法连接远程桌面'); return }
if (!device.windowsUsername) { ElMessage.warning('请先配置该设备的 Windows 登录账号'); return }
selectedDevice.value = device
showRemoteDesktopModal.value = true
}
const handleSetCredentials = async (device: any) => {
selectedDevice.value = device
credentialsForm.value = { username: device.windowsUsername || '', password: '' }
showCredentialsDialog.value = true
}
const saveCredentials = async () => {
if (!credentialsForm.value.username) { ElMessage.warning('请输入用户名'); return }
savingCredentials.value = true
try {
await deviceApi.setDeviceCredentials(selectedDevice.value.id, credentialsForm.value)
const device = devices.value.find(d => d.id === selectedDevice.value.id)
if (device) device.windowsUsername = credentialsForm.value.username
showCredentialsDialog.value = false
ElMessage.success('账号配置成功')
} catch (error) {
ElMessage.error('保存失败')
} finally {
savingCredentials.value = false
}
}
const handlePowerCommand = async (command: string, device: any) => {
const actionMap: Record<string, { api: Function; name: string; confirmMsg: string }> = {
'power-on': { api: powerApi.powerOn, name: '开机', confirmMsg: '确定要开机吗?' },
'power-off': { api: powerApi.powerOff, name: '关机', confirmMsg: '确定要关机吗?' },
'restart': { api: powerApi.restart, name: '重启', confirmMsg: '确定要重启吗?' },
'force-off': { api: powerApi.forceOff, name: '强制关机', confirmMsg: '确定要强制关机吗?可能导致数据丢失!' },
'force-restart': { api: powerApi.forceRestart, name: '强制重启', confirmMsg: '确定要强制重启吗?可能导致数据丢失!' }
}
const action = actionMap[command]
if (!action) return
try {
await ElMessageBox.confirm(`设备: ${device.ipAddress}\n${action.confirmMsg}`, `确认${action.name}`, {
confirmButtonText: '确定', cancelButtonText: '取消', type: command.includes('force') ? 'warning' : 'info'
})
ElMessage.info(`正在执行${action.name}...`)
const response = await action.api(device.id)
if (response.success) {
ElMessage.success(response.message || `${action.name}命令已发送`)
setTimeout(() => checkAllDevicesStatus(), 3000)
} else {
ElMessage.error(response.error || `${action.name}失败`)
}
} catch (error: any) {
if (error !== 'cancel') ElMessage.error(`${action.name}失败`)
}
}
</script>
<style scoped>
.devices-page { padding: 0; }
.card-header { display: flex; justify-content: space-between; align-items: center; font-size: 16px; font-weight: 500; }
.header-actions { display: flex; align-items: center; }
</style>

View File

@ -1,208 +0,0 @@
<template>
<div class="remote-access-page">
<!-- 加载中 -->
<div v-if="loading" class="loading-container">
<el-icon class="is-loading" :size="48"><Loading /></el-icon>
<p>正在验证访问链接...</p>
</div>
<!-- 错误提示 -->
<div v-else-if="error" class="error-container">
<el-icon :size="64" color="#f56c6c"><CircleClose /></el-icon>
<h2>访问失败</h2>
<p>{{ error }}</p>
<el-button type="primary" @click="goHome">返回首页</el-button>
</div>
<!-- 连接信息 -->
<div v-else-if="!connectionUrl" class="info-container">
<el-card shadow="hover" style="max-width: 500px; margin: 0 auto;">
<template #header>
<div class="card-header">
<el-icon :size="24" color="#67c23a"><Monitor /></el-icon>
<span style="margin-left: 10px">远程桌面连接</span>
</div>
</template>
<el-descriptions :column="1" border>
<el-descriptions-item label="目标设备">{{ tokenInfo?.deviceIp }}</el-descriptions-item>
<el-descriptions-item label="链接有效期">{{ formatDate(tokenInfo?.expiresAt) }}</el-descriptions-item>
<el-descriptions-item label="剩余使用次数">
{{ tokenInfo?.remainingUses === -1 ? '无限制' : tokenInfo?.remainingUses }}
</el-descriptions-item>
</el-descriptions>
<div style="margin-top: 20px; text-align: center;">
<el-button type="primary" size="large" @click="connect" :loading="connecting">
开始连接
</el-button>
</div>
</el-card>
</div>
<!-- 远程桌面 iframe -->
<div v-else class="remote-desktop-container">
<div class="toolbar">
<span>远程桌面 - {{ tokenInfo?.deviceIp }}</span>
<div class="toolbar-actions">
<el-button type="danger" size="small" @click="disconnect">断开连接</el-button>
<el-button :type="isFullscreen ? 'warning' : 'primary'" size="small" @click="toggleFullscreen">
{{ isFullscreen ? '退出全屏' : '全屏显示' }}
</el-button>
</div>
</div>
<iframe ref="rdpFrame" :src="connectionUrl" class="rdp-iframe" allowfullscreen />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Loading, CircleClose, Monitor } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { remoteDesktopApi } from '@/api/amt'
defineOptions({ name: 'RemoteAccess' })
const route = useRoute()
const router = useRouter()
const loading = ref(true)
const error = ref('')
const tokenInfo = ref<any>(null)
const connectionUrl = ref('')
const connecting = ref(false)
const isFullscreen = ref(false)
const rdpFrame = ref<HTMLIFrameElement | null>(null)
onMounted(async () => {
const token = route.params.token as string
if (!token) {
error.value = '无效的访问链接'
loading.value = false
return
}
try {
// Token
const result = await remoteDesktopApi.validateToken(token)
if (!result.valid) {
error.value = result.error || '访问链接无效或已过期'
loading.value = false
return
}
tokenInfo.value = result
loading.value = false
} catch (err: any) {
error.value = err.message || '验证访问链接失败'
loading.value = false
}
})
const connect = async () => {
const token = route.params.token as string
connecting.value = true
try {
const result = await remoteDesktopApi.connectByToken(token)
if (result.success) {
connectionUrl.value = result.connectionUrl
ElMessage.success('正在连接远程桌面...')
} else {
ElMessage.error(result.error || '连接失败')
}
} catch (err: any) {
ElMessage.error(err.message || '连接远程桌面失败')
} finally {
connecting.value = false
}
}
const disconnect = () => {
connectionUrl.value = ''
ElMessage.info('已断开远程桌面连接')
}
const toggleFullscreen = () => {
isFullscreen.value = !isFullscreen.value
if (isFullscreen.value) {
document.documentElement.requestFullscreen?.()
} else {
document.exitFullscreen?.()
}
}
const goHome = () => {
router.push('/')
}
const formatDate = (dateStr: string) => {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleString('zh-CN')
}
</script>
<style scoped>
.remote-access-page {
min-height: 100vh;
background: #f5f7fa;
}
.loading-container,
.error-container,
.info-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px;
}
.loading-container p,
.error-container p {
margin-top: 20px;
color: #606266;
font-size: 16px;
}
.error-container h2 {
margin: 20px 0 10px;
color: #303133;
}
.card-header {
display: flex;
align-items: center;
font-size: 18px;
font-weight: 500;
}
.remote-desktop-container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
background: #1a1a1a;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background: #2d2d2d;
color: #fff;
}
.toolbar-actions {
display: flex;
gap: 10px;
}
.rdp-iframe {
flex: 1;
width: 100%;
border: none;
background: transparent;
}
</style>

View File

@ -19,7 +19,7 @@
@refresh="handleRefresh" @refresh="handleRefresh"
> >
<template #left> <template #left>
<ElButton @click="handleAddDirectory" v-ripple> 添加目录 </ElButton> <ElButton v-auth="'add'" @click="handleAddMenu" v-ripple> 添加菜单 </ElButton>
<ElButton @click="toggleExpand" v-ripple> <ElButton @click="toggleExpand" v-ripple>
{{ isExpanded ? '收起' : '展开' }} {{ isExpanded ? '收起' : '展开' }}
</ElButton> </ElButton>
@ -43,7 +43,6 @@
:type="dialogType" :type="dialogType"
:editData="editData" :editData="editData"
:lockType="lockMenuType" :lockType="lockMenuType"
:isDirectory="currentParentId === null && !editData?.id"
@submit="handleSubmit" @submit="handleSubmit"
/> />
</ElCard> </ElCard>
@ -56,7 +55,7 @@
import { useTableColumns } from '@/hooks/core/useTableColumns' import { useTableColumns } from '@/hooks/core/useTableColumns'
import type { AppRouteRecord } from '@/types/router' import type { AppRouteRecord } from '@/types/router'
import MenuDialog from './modules/menu-dialog.vue' import MenuDialog from './modules/menu-dialog.vue'
import { fetchGetMenuList, fetchCreateMenu, fetchUpdateMenu, fetchDeleteMenu } from '@/api/system-manage' import { fetchGetMenuList } from '@/api/system-manage'
import { ElTag, ElMessageBox } from 'element-plus' import { ElTag, ElMessageBox } from 'element-plus'
defineOptions({ name: 'Menus' }) defineOptions({ name: 'Menus' })
@ -125,8 +124,7 @@
row: AppRouteRecord row: AppRouteRecord
): 'primary' | 'success' | 'warning' | 'info' | 'danger' => { ): 'primary' | 'success' | 'warning' | 'info' | 'danger' => {
if (row.meta?.isAuthButton) return 'danger' if (row.meta?.isAuthButton) return 'danger'
// component /index/index / if (row.children?.length) return 'info'
if (row.children?.length || (row.component === '/index/index' && row.path?.startsWith('/'))) return 'info'
if (row.meta?.link && row.meta?.isIframe) return 'success' if (row.meta?.link && row.meta?.isIframe) return 'success'
if (row.path) return 'primary' if (row.path) return 'primary'
if (row.meta?.link) return 'warning' if (row.meta?.link) return 'warning'
@ -140,8 +138,7 @@
*/ */
const getMenuTypeText = (row: AppRouteRecord): string => { const getMenuTypeText = (row: AppRouteRecord): string => {
if (row.meta?.isAuthButton) return '按钮' if (row.meta?.isAuthButton) return '按钮'
// component /index/index if (row.children?.length) return '目录'
if (row.children?.length || (row.component === '/index/index' && row.path?.startsWith('/'))) return '目录'
if (row.meta?.link && row.meta?.isIframe) return '内嵌' if (row.meta?.link && row.meta?.isIframe) return '内嵌'
if (row.path) return '菜单' if (row.path) return '菜单'
if (row.meta?.link) return '外链' if (row.meta?.link) return '外链'
@ -216,8 +213,8 @@
return h('div', buttonStyle, [ return h('div', buttonStyle, [
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'add', type: 'add',
onClick: () => handleAddChildMenu(row), onClick: () => handleAddAuth(),
title: '新增菜单' title: '新增权限'
}), }),
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'edit', type: 'edit',
@ -225,7 +222,7 @@
}), }),
h(ArtButtonTable, { h(ArtButtonTable, {
type: 'delete', type: 'delete',
onClick: () => handleDeleteMenu(row) onClick: () => handleDeleteMenu()
}) })
]) ])
} }
@ -354,28 +351,12 @@
return convertAuthListToChildren(searchedData) return convertAuthListToChildren(searchedData)
}) })
// ID
const currentParentId = ref<number | null>(null)
/** /**
* 添加目录顶级菜单parentId=null * 添加菜单
*/ */
const handleAddDirectory = (): void => { const handleAddMenu = (): void => {
dialogType.value = 'menu' dialogType.value = 'menu'
editData.value = null editData.value = null
currentParentId.value = null //
lockMenuType.value = true
dialogVisible.value = true
}
/**
* 添加子菜单在目录下创建parentId=父级id
* @param parentRow 父级目录
*/
const handleAddChildMenu = (parentRow: AppRouteRecord): void => {
dialogType.value = 'menu'
editData.value = null // editData
currentParentId.value = parentRow.id || null // ID
lockMenuType.value = true lockMenuType.value = true
dialogVisible.value = true dialogVisible.value = true
} }
@ -425,11 +406,6 @@
icon?: string icon?: string
roles?: string[] roles?: string[]
sort?: number sort?: number
label?: string
isHide?: boolean
keepAlive?: boolean
link?: string
isIframe?: boolean
[key: string]: any [key: string]: any
} }
@ -437,99 +413,27 @@
* 提交表单数据 * 提交表单数据
* @param formData 表单数据 * @param formData 表单数据
*/ */
const handleSubmit = async (formData: MenuFormData): Promise<void> => { const handleSubmit = (formData: MenuFormData): void => {
try { console.log('提交数据:', formData)
// id // TODO: API
const isEdit = editData.value?.id
if (isEdit) {
//
await fetchUpdateMenu(editData.value.id, {
name: formData.label || formData.name,
path: formData.path,
component: formData.component,
title: formData.name,
icon: formData.icon,
sort: formData.sort,
isHide: formData.isHide,
keepAlive: formData.keepAlive,
link: formData.link,
isIframe: formData.isIframe,
roles: formData.roles
})
} else {
// /
// currentParentId.value=null
const isDirectory = currentParentId.value === null
// /
let menuPath = formData.path
if (!isDirectory && menuPath.startsWith('/')) {
// /
const pathParts = menuPath.split('/').filter(Boolean)
menuPath = pathParts[pathParts.length - 1] || menuPath.replace(/^\//, '')
}
// /
if (isDirectory && !menuPath.startsWith('/')) {
menuPath = '/' + menuPath
}
//
let componentPath = formData.component
if (!isDirectory && !componentPath) {
//
const parentMenu = tableData.value.find(m => m.id === currentParentId.value)
const parentPath = parentMenu?.path?.replace(/^\//, '') || 'pages'
componentPath = `/${parentPath}/${menuPath}`
console.log('自动生成组件路径:', componentPath)
}
const requestData = {
parentId: currentParentId.value, // null
name: formData.label || formData.name,
path: menuPath,
component: isDirectory ? '/index/index' : componentPath, // /index/index
title: formData.name,
icon: formData.icon,
sort: formData.sort || 0,
isHide: formData.isHide || false,
keepAlive: formData.keepAlive || false,
link: formData.link,
isIframe: formData.isIframe || false,
roles: formData.roles,
autoCreateComponent: !isDirectory //
}
console.log('创建菜单请求数据:', requestData, 'isDirectory:', isDirectory, 'currentParentId:', currentParentId.value)
await fetchCreateMenu(requestData)
}
dialogVisible.value = false
getMenuList() getMenuList()
} catch (error) {
console.error('保存失败:', error)
}
} }
/** /**
* 删除菜单 * 删除菜单
* @param row 菜单行数据
*/ */
const handleDeleteMenu = async (row: AppRouteRecord): Promise<void> => { const handleDeleteMenu = async (): Promise<void> => {
try { try {
await ElMessageBox.confirm('确定要删除该菜单吗?删除后无法恢复', '提示', { await ElMessageBox.confirm('确定要删除该菜单吗?删除后无法恢复', '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}) })
if (row.id) { ElMessage.success('删除成功')
await fetchDeleteMenu(row.id)
getMenuList() getMenuList()
} } catch (error) {
} catch (error: any) { if (error !== 'cancel') {
if (error !== 'cancel' && error?.message !== 'cancel') { ElMessage.error('删除失败')
// http
} }
} }
} }

View File

@ -8,33 +8,6 @@
class="menu-dialog" class="menu-dialog"
@closed="handleClosed" @closed="handleClosed"
> >
<!-- 填写说明 -->
<ElAlert
v-if="!isEdit && form.menuType === 'menu'"
:title="isDirectory ? '创建目录说明' : '创建子菜单说明'"
type="info"
:closable="false"
show-icon
style="margin-bottom: 16px"
>
<template #default>
<div v-if="isDirectory" style="line-height: 1.8">
<div><strong>菜单名称</strong>显示在侧边栏的名称"设备管理"</div>
<div><strong>路由地址</strong>必须以 / 开头 /device</div>
<div><strong>权限标识</strong>唯一标识 Device不能与现有的重复</div>
<div><strong>组件路径</strong>留空即可系统会自动设置</div>
<div><strong>图标</strong>使用 Remix Icon格式如 ri:home-line<a href="https://remixicon.com/" target="_blank" style="color: #409eff">点击查看图标库</a></div>
<div style="margin-top: 4px; color: #909399; font-size: 12px">常用图标ri:home-line(首页) ri:user-line(用户) ri:settings-line(设置) ri:folder-line(文件夹) ri:computer-line(电脑) ri:server-line(服务器)</div>
</div>
<div v-else style="line-height: 1.8">
<div><strong>菜单名称</strong>显示在侧边栏的名称"设备列表"</div>
<div><strong>路由地址</strong>相对路径不要以 / 开头 list</div>
<div><strong>权限标识</strong>唯一标识 DeviceList不能与现有的重复</div>
<div><strong>组件路径</strong>对应 views 目录下的组件 /device/list留空会自动生成</div>
</div>
</template>
</ElAlert>
<ArtForm <ArtForm
ref="formRef" ref="formRef"
v-model="form" v-model="form"
@ -65,7 +38,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FormRules } from 'element-plus' import type { FormRules } from 'element-plus'
import { ElIcon, ElTooltip, ElAlert } from 'element-plus' import { ElIcon, ElTooltip } from 'element-plus'
import { QuestionFilled } from '@element-plus/icons-vue' import { QuestionFilled } from '@element-plus/icons-vue'
import { formatMenuTitle } from '@/utils/router' import { formatMenuTitle } from '@/utils/router'
import type { AppRouteRecord } from '@/types/router' import type { AppRouteRecord } from '@/types/router'
@ -128,7 +101,6 @@
editData?: AppRouteRecord | any editData?: AppRouteRecord | any
type?: 'menu' | 'button' type?: 'menu' | 'button'
lockType?: boolean lockType?: boolean
isDirectory?: boolean //
} }
interface Emits { interface Emits {
@ -139,8 +111,7 @@
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
visible: false, visible: false,
type: 'menu', type: 'menu',
lockType: false, lockType: false
isDirectory: false
}) })
const emit = defineEmits<Emits>() const emit = defineEmits<Emits>()

View File

@ -1,13 +0,0 @@
# 【开发】环境变量
# 应用部署基础路径(如部署在子目录 /admin则设置为 /admin/
VITE_BASE_URL = /
# API 请求基础路径(开发环境设置为 / 使用代理,生产环境设置为完整后端地址)
VITE_API_URL = /
# 代理目标地址(开发环境通过 Vite 代理转发请求到此地址,解决跨域问题)
VITE_API_PROXY_URL = https://m1.apifoxmock.com/m1/6400575-6097373-default
# Delete console
VITE_DROP_CONSOLE = false

View File

@ -1,10 +0,0 @@
# 【生产】环境变量
# 应用部署基础路径(如部署在子目录 /admin则设置为 /admin/
VITE_BASE_URL = /
# API 地址前缀
VITE_API_URL = https://m1.apifoxmock.com/m1/6400575-6097373-default
# Delete console
VITE_DROP_CONSOLE = true

View File

@ -1,2 +0,0 @@
*.html linguist-detectable=false
*.vue linguist-detectable=true

View File

@ -1,11 +0,0 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.cursorrules
# Auto-generated files
src/types/import/auto-imports.d.ts
src/types/import/components.d.ts
.auto-import.json

View File

@ -1 +0,0 @@
pnpm dlx commitlint --edit $1

View File

@ -1 +0,0 @@
pnpm run lint:lint-staged

View File

@ -1,3 +0,0 @@
/node_modules/*
/dist/*
/src/main.ts

View File

@ -1,20 +0,0 @@
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"vueIndentScriptAndStyle": true,
"singleQuote": true,
"quoteProps": "as-needed",
"bracketSpacing": true,
"trailingComma": "none",
"bracketSameLine": false,
"jsxSingleQuote": false,
"arrowParens": "always",
"insertPragma": false,
"requirePragma": false,
"proseWrap": "never",
"htmlWhitespaceSensitivity": "strict",
"endOfLine": "auto",
"rangeStart": 0
}

View File

@ -1,9 +0,0 @@
dist
node_modules
public
.husky
.vscode
src/components/Layout/MenuLeft/index.vue
src/assets
stats.html

View File

@ -1,82 +0,0 @@
module.exports = {
// 继承推荐规范配置
extends: [
'stylelint-config-standard',
'stylelint-config-recommended-scss',
'stylelint-config-recommended-vue/scss',
'stylelint-config-html/vue',
'stylelint-config-recess-order'
],
// 指定不同文件对应的解析器
overrides: [
{
files: ['**/*.{vue,html}'],
customSyntax: 'postcss-html'
},
{
files: ['**/*.{css,scss}'],
customSyntax: 'postcss-scss'
}
],
// 自定义规则
rules: {
'import-notation': 'string', // 指定导入CSS文件的方式("string"|"url")
'selector-class-pattern': null, // 选择器类名命名规则
'custom-property-pattern': null, // 自定义属性命名规则
'keyframes-name-pattern': null, // 动画帧节点样式命名规则
'no-descending-specificity': null, // 允许无降序特异性
'no-empty-source': null, // 允许空样式
'property-no-vendor-prefix': null, // 允许属性前缀
// 允许 global 、export 、deep伪类
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['global', 'export', 'deep']
}
],
// 允许未知属性
'property-no-unknown': [
true,
{
ignoreProperties: []
}
],
// 允许未知规则
'at-rule-no-unknown': [
true,
{
ignoreAtRules: [
'apply',
'use',
'mixin',
'include',
'extend',
'each',
'if',
'else',
'for',
'while',
'reference'
]
}
],
'scss/at-rule-no-unknown': [
true,
{
ignoreAtRules: [
'apply',
'use',
'mixin',
'include',
'extend',
'each',
'if',
'else',
'for',
'while',
'reference'
]
}
]
}
}

View File

@ -1,650 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Released]
### v3.0.1 - 2025-11-15
#### Fixed
- Fixed repeated API calls issue when accessing non-existent interfaces during route registration
- Fixed one-click cleanup script build failure issue
#### Optimized
- Enhanced route configuration validation mechanism, automatically detects and prompts path configuration errors for non-first-level menus
- Optimized top progress bar ghosting issue
- Optimized vite pre-build configuration
- Unified border radius and border consistency
- Redesigned lock screen page, improved visual experience
- Optimized visual experience issue caused by logout menu disappearing prematurely
#### Added
- ArtForm and ArtSearchBar component labels support custom render functions, enabling complex labels with tooltips
- Menu management form key fields added Tooltip hints, lowering user configuration threshold
- Iconify added offline icon loading mode
- Logout added redirect property for redirecting to corresponding page after re-login
- Logout and re-entry preserves user tabs, identified by userId
- Dual menu added collapse button
- Enhanced menu icons
- Multi-tab pages added icon display
- Added WebSocket connection example
- Lock screen password error triggers input box shake animation
#### ⚠️ Important Notice
> This version requires re-login
---
### v3.0.0 - 2025-11-09 🎉
#### Refactored
- Style system refactored: Sass fully migrated to Tailwind CSS, improving development efficiency and style consistency
- Icon solution upgraded: Iconfont replaced with Iconify, supporting richer icon libraries and on-demand loading
- Route registration refactored: Comprehensive refactoring of route registration system with object-oriented design, improving code maintainability, testability and extensibility
- Refactored ArtTextScroll component, improving performance and maintainability
- Refactored color system, unified UI visual specifications, improving interface consistency
#### Optimized
- Build optimization: Complete package size reduced by 1.3 MB, significantly improving loading performance
- Architecture optimization: Optimized directory structure with clearer responsibility division, reducing user learning curve and onboarding difficulty
- Comment optimization: Unified module comment standards, improved component descriptions, feature explanations and usage examples, reducing user understanding cost and onboarding difficulty
- Performance improvement: Optimized core code logic, improving system runtime efficiency
- Menu optimization: Refined menu styles, optimized interaction experience and visual presentation
- Responsive optimization: Optimized ArtForm and ArtSearchBar grid layout, adapting to various screen sizes
- Configuration management optimization: Added setting.ts configuration file, supporting one-click copy and reset of system default settings
#### Fixed
- Fixed ArtForm and ArtSearchBar custom component rendering issues
#### Added
- ArtForm and ArtSearchBar added render property, supporting custom component rendering
- Festival feature enhancement: Fireworks configuration supports cross-date range settings and custom playback counts
#### Upgraded
- Dependency updates: Upgraded core dependencies to latest stable versions
#### ⚠️ Important Notice
> This upgrade involves fundamental refactoring of the style system (Sass → Tailwind CSS) and icon library (Iconfont → Iconify), which is a breaking change. It is recommended to use v3.0 for new projects. Upgrading existing projects is not recommended.
---
### v2.6.1 - 2025-10-19
#### Fixed
- Fixed repeated API calls issue when accessing invalid addresses for user info and menu endpoints
- Fixed ElButton circle mode styles
- Fixed ElSelect keyboard selection issue
- Fixed static routes with parameters redirecting to login page issue
#### Optimized
- Upgraded some dependencies to be compatible with tailwindcss
- Optimized external link menu click selection state
- Added theme color switching feature to authorization pages
---
### v2.6.0 - 2025-10-16
#### Added
- Extended registration and password reset page top component support
#### Optimized
- Optimized streamlined version menu data structure, improved data consistency
- Optimized local development environment network request proxy configuration
- Optimized ElTree component default styles
- Added VsCode recommended plugin related configuration
- Optimized ElDropdown component click trigger mode interaction styles
- Optimized menu filtering logic
- Optimized page switching animation, improved loading speed
- Optimized dark mode text color
#### Fixed
- Fixed static route custom home page path first visit redirecting to login page issue
- Fixed logout briefly jumping to 500 page issue
- Fixed v2.5.9 version home route redirect configuration failure issue
- Fixed v2.5.9 auto-import mechanism causing build exception
#### ⚠️ Important Notice
> This version requires re-login
---
### v2.5.9 - 2025-10-12
#### Code Optimized
- Optimized views file directory, file names, and code
- Added global configuration for useTable pagination request fields (tableConfig.ts)
- Optimized routing configuration to modular structure
- Menu API now uses apifox mock data (set VITE_ACCESS_MODE to backend mode in .env)
---
### v2.5.8 - 2025-09-29
#### Upgraded
- Upgraded core libraries including vue, vite, element-plus
#### Fixed
- Fixed rich text editor fullscreen top bar z-index issue
- Fixed table column sorting component text overflow issue
- Fixed statistics card condition judgment
- Fixed infinite redirect when root path / and HOME_PAGE_PATH are both /
#### Optimized
- Optimized el-tag styles
- Optimized top progress bar color
- Optimized custom theme configuration
- Optimized ElementPlus custom theme issues
#### ⚠️ Important Notice
> Due to dependency upgrades, Node version needs to be upgraded to v20.19.0 or above
---
### v2.5.7 - 2025-09-14
#### Added
- Added ArtForm component
#### Fixed
- Fixed theme switching flicker issue in new Chrome versions
#### Optimized
- Optimized form label height alignment issue
- First screen startup performance optimization
---
### v2.5.6 - 2025-09-06
#### Added
- ArtTableHeader added search button to control top search bar visibility
- Added permission demonstration examples
- Added global error handling framework
#### Optimized
- useTable type inference optimization, no need to manually pass types for type hints
- useTable removeColumn supports multiple data deletion
- useTable auto-recognizes response body with custom configuration (src/utils/table/tableConfig.ts)
- useTable empty data browser warning optimization
- API request code optimization, api.d.ts type optimization
- Optimized ArtTable top button wrapping adaptive issue
- ArtTable pagination component selection style optimization
- ArtTable empty state height fills by default
- ArtButtonMore component added icon and color configuration
- ArtSearchBar label doesn't occupy space when empty
- Table operation column drag prohibits fixed column dragging
- Role management page API integration and code optimization
- Menu management page optimization
- Optimized settings center scroll following issue
- First-level route external link component validation logic optimization
- Optimized map bottom-right drag issue
- Optimized dark mode page refresh white background issue
- Optimized left menu collapse button spacing issue
- Mobile displays left menu logo
- Network request added showSuccessMessage to configure success message display
- Global components use async loading strategy to improve first screen loading performance
#### Fixed
- Fixed bug where batch deleting entire page data doesn't return to previous page
- Fixed dynamic route parameter issues
- Fixed dynamic route configuration first-level route iframe page fullscreen issue
---
### v2.5.5 - 2025-08-17
#### Added
- Added ArtSearchBar component examples
- useTable added excludeParams to exclude certain parameters from requests
#### Refactored
- Refactored ArtSearchBar component, supports more components and form validation
#### Optimized
- useTable column configuration: supports dynamic update capability
- Optimized color picker border radius
- Unified el-radio and el-checkbox sizes
- art-stats-card added decimal places and separator configuration
- Route configuration example optimization
- Advanced table added custom data fetching example
- Optimized path alias type issues
- Local development CORS configuration optimization
- useTable property and method naming optimization
- Login page UI upgrade
- 403, 404, 500 page UI upgrade
#### Fixed
- Fixed multiple rich text editor icon inconsistency issue
- Fixed useTable deleting last entire page data not returning to previous page issue
- Fixed echarts chart data initialization and update browser error
- Network request supports logout on HTTP status code 401
- Optimized network request logout multiple prompt issue
#### Removed
- Removed art-chart-empty component
---
### v2.5.4 - 2025-07-27
#### Added
- Network request headers support custom configuration
- Expand row supports formatter rendering
#### Optimized
- Route registration added component validation
#### Fixed
- Fixed user info API timing issue causing route registration menu rendering error
- Fixed dynamic route validation issue causing iframe not to display
- Fixed reset file syntax error
- Fixed ArtTable data type error
- Fixed map scroll wheel zoom issue
---
### v2.5.3 - 2025-07-20
#### Added
- Added table left-right layout example
#### Refactored
- ArtTable component refactored
#### Upgraded
- Element Plus upgraded to v2.10.2
#### Optimized
- Optimized useTable pagination parameter issue
- Optimized network request example: initialization parameters, pagination carrying parameters
- Optimized search date range parameter handling
- Optimized el-date-picker component border radius
- Optimized el-select component hover style
- Search component and pagination component height reduced
- Optimized login page slider animation interval
- Optimized menu without submenu display issue
#### Fixed
- Fixed ArtTable switching page size executing two requests bug
---
### v2.5.2 - 2025-07-13
#### Added
- Added one-click cleanup script for quick development environment preparation
- useTable hooks support custom pagination field name mapping
- Horizontal menu, mixed menu, double column menu support badge display
- Quick entry supports configuration file mode
- Top bar function supports configuration file mode
- Support custom home page path
- Route supports redirect and other attributes configuration
#### Optimized
- Global event bus mittBus type safety optimization
- Optimized mobile settings container width style
- Optimized login page verification slider text centering effect
#### Fixed
- Fixed table no data header not displaying issue
- Fixed v2.5.0 top progress bar not displaying issue
- Fixed left menu mask abnormal display issue
- Fixed hiding all submenus still showing parent menu issue
- Fixed stylelint causing login page slider style abnormality
- Fixed old mobile device loading positioning issue
---
### v2.5.1 - 2025-07-08
#### Optimized
- el-card and el-table background color consistent with system
#### Fixed
- Fixed first login system loading closing early bug
- Fixed v2.5.0 version causing fullscreen page style z-index too low bug
- Fixed v2.5.0 version causing table expand row collapse bug
---
### v2.5.0 - 2025-07-06 🎉
#### Added
- Added useTable hooks table encapsulation, supports data fetching, transformation, response adaptation, intelligent caching (based on LRU algorithm), error handling, column configuration and slots, pagination control, refresh strategies and other core functions, comprehensively improving development efficiency and user experience
#### Refactored
- Refactored ArtTable, ArtTableHeader, ArtNotification components
#### Upgraded
- Echarts version upgraded to 5.6.0
#### Removed
- Removed CountTo plugin, replaced with ArtCountTo component
#### Fixed
- Fixed menu management search directly modifying pinia data issue
- Fixed route guard loading flicker issue
#### ⚠️ Important Notice
> Recommended upgrade for more efficient and intelligent table development experience
---
### v2.4.2.9 - 2025-07-02
#### Refactored
- Menu layout and top navigation code refactored
#### Optimized
- Optimized mobile menu scroll user experience
- Optimized top menu style issues
- Top menu width adaptive, can display more content, mixed menu supports mouse scroll
- asyncRoutes route configuration auth_mark field changed to authMark
- Removed duplicate components.d.ts file, components.d.ts and auto-imports.d.ts ignored from commits
- Optimized i18n language file loading method, changed from async to sync mode
- Optimized el-pagination size inconsistency issue
#### Fixed
- Fixed mobile lock screen page some browsers unable to unlock bug
---
### v2.4.2.8 - 2025-06-26
#### Fixed
- Fixed v2.4.2.7 version accessing / path showing 404 issue
---
### v2.4.2.7 - 2025-06-25
#### Added
- Route supports fullscreen mode configuration
- Route supports auto-jump to first valid route of menu
- Dynamic route added removeAllDynamicRoutes method for completely clearing all dynamic routes
- Permission custom directive optimization, added role permission directive v-roles for controlling element visibility
- Added tab operation examples
#### Optimized
- CORS request carrying cookie configuration from environment variables, disabled by default
- Some optimizations for SEO and accessibility
#### Fixed
- Fixed login page drag component ArtDragVerify width and color abnormal bug
- Fixed iframe page mixed mode and double column mode abnormal bug
- Optimized lock screen page being penetrated by el-loading bug
---
### v2.4.2.6 - 2025-06-23
#### Refactored
- Refactored form-related components in components/core/forms folder, improved maintainability and consistency
- Refactored ArtBreadcrumb breadcrumb navigation component, optimized logic structure and styles
- Refactored ArtFireworksEffect fireworks effect component, significantly improved rendering performance and animation smoothness
#### Optimized
- Optimized ArtChatWindow and ArtFastEnter component code, improved readability and performance
#### Added
- README documentation added official website link for easy access to project documentation
---
### v2.4.2.5 - 2025-06-22
#### Refactored
- Refactored chart components, optimized code structure and maintainability
- Fine-tuned chart animations and theme color schemes, improved visual consistency
---
### v2.4.2.4 - 2025-06-18
#### Refactored
- ArtMenuRight component refactored
- Components under components/core/cards refactored, code optimized
#### Optimized
- ArtWatermark added type annotations
---
### v2.4.2.3 - 2025-06-18
#### Refactored
- ArtResultPage component refactored
#### Optimized
- ArtTextScroll component code optimization
- ArtException component added type hints
- ArtCutterImg component style optimization, added type definitions
- ArtVideoPlayer component added type definitions
---
### v2.4.2.2 - 2025-06-16
#### Refactored
- Back to top component refactored
- Icon selector component refactored
#### Changed
- System Logo component property changes
---
### v2.4.2.1 - 2025-06-16
#### Refactored
- Banner component refactored and optimized
#### Fixed
- Fixed mixed menu first menu nested menu jump bug
---
### v2.4.2 - 2025-06-14
#### Refactored
- Refactored network request module, enhanced error handling, type safety and multi-language support
#### Added
- Route configuration added activePath active menu path property
#### Optimized
- Guide and column settings multi-language improvement
- Removed invalid code from user list and menu management pages
- Updated technical support link
#### Fixed
- Fixed mobile search bar unable to scroll, iPad page scroll abnormal issue
- Fixed el-dialog enabling draggable property causing custom animation failure issue
- Fixed v2.3.0 local storage refactoring causing login, registration page multi-language settings unable to persist issue
- Fixed table fixed column not working bug
- Fixed infinite redirect when root path / and HOME_PAGE_PATH are both /
#### ⚠️ Important Notice
> This version requires re-login
---
### v2.4.1.1 - 2025-06-07
#### Optimized
- Optimized role management page code
- el-dialog visual effect optimization, supports line configuration
- System theme mode changed from Light to follow system mode
#### Fixed
- Fixed menu management collapse bug
- Fixed table empty data height infinitely growing bug
---
### v2.4.1 - 2025-06-07
#### Added
- Prohibit using developer tools to crack lock screen when locked
#### Optimized
- Improved menu operation responsiveness
- Page entrance animation time reduced by 0.04s
- Echarts chart performance optimization, added visible area initialization, memory leak protection, debounce handling
#### Fixed
- Fixed Echarts chart component not displaying in dialog bug
---
### v2.4.0 - 2025-06-06 🎉
#### Refactored
- Global TypeScript type system refactored, improved type accuracy and maintainability
- Refactored utils toolkit, unified utility method structure, enhanced readability and reusability
#### Added
- utils added form validation and Cookie operation related utility functions
- Page components added defineOptions for explicit component naming
- HTTP request added token expiration auto-handling logic, improved security and user experience
#### Optimized
- Deleted unused utility modules and invalid resources, streamlined project size
- Optimized views page structure, removed redundant page files
- Exception page multi-language support, improved internationalization experience
- Image resources uniformly converted to webp format, overall resource size reduced by about 50%
- Build output reduced by about 1MB, improved loading efficiency
#### ⚠️ Important Notice
> This version requires re-login
---
## Version Guidelines
### Version Number Rules
This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html):
- **Major**: Incompatible API changes
- **Minor**: Backwards-compatible functionality additions
- **Patch**: Backwards-compatible bug fixes
### Change Types
- **Added**: New features
- **Changed**: Changes to existing functionality
- **Deprecated**: Soon-to-be removed features
- **Removed**: Removed features
- **Fixed**: Bug fixes
- **Security**: Security-related fixes
- **Optimized**: Performance or experience optimizations
- **Refactored**: Code refactoring
- **Upgraded**: Dependency upgrades
### Important Notice Markers
- 🎉 **Major Update**: Versions with important new features
- ⚠️ **Breaking Changes**: Updates requiring special attention for incompatibility
- 🔒 **Security Update**: Versions fixing security vulnerabilities
---
## Contributing
If you want to contribute to the project, please check [CONTRIBUTING.md](./CONTRIBUTING.md).
## License
[MIT License](./LICENSE)
---
**Note**: This changelog starts recording from version v2.4.0. For earlier version history, please check Git commit records.

View File

@ -1,654 +0,0 @@
# 更新日志
所有重要的项目变更都将记录在此文件中。
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
## [已发布]
### v3.0.1 - 2025-11-15
#### 修复
- 修复路由注册时不存在接口的重复请求问题
- 修复一键精简脚本打包失败的问题
#### 优化
- 完善路由配置验证机制,自动检测并提示非一级菜单的路径配置错误
- 优化顶部进度条残影问题
- 优化 vite 预构建配置
- 优化圆角、边框统一性
- 重新设计锁屏页面,提升视觉体验
- 优化退出登录菜单提前消失造成的视觉体验不好问题
#### 新增
- ArtForm 和 ArtSearchBar 组件的 label 支持自定义渲染函数,可实现带 tooltip 等复杂标签
- 菜单管理表单关键字段新增 Tooltip 提示,降低用户配置门槛
- iconify 新增离线图标加载模式
- 退出登录新增 redirect 属性,用于重新登录后跳转到对应页面
- 退出登录重新进入系统保留用户标签页,根据 userId 进行身份识别
- 双列菜单新增折叠按钮
- 完善菜单图标
- 多标签页增加图标显示
- 新增 WebSocket 连接示例
- 锁屏密码错误时增加输入框抖动动效
#### ⚠️ 重要提示
> 此版本需要重新登录
---
### v3.0.0 - 2025-11-09 🎉
#### 重构
- 样式系统重构Sass 全面迁移至 Tailwind CSS提升开发效率与样式一致性
- 图标方案升级Iconfont 替换为 Iconify支持更丰富的图标库与按需加载
- 路由注册重构:全面重构路由注册系统,引入面向对象设计,提高代码的可维护性、可测试性和扩展能力
- 重构 ArtTextScroll 组件,提升性能与可维护性
- 重构颜色体系,统一 UI 视觉规范,提升界面一致性
#### 优化
- 构建优化:完整包体积减少 1.3 MB显著提升加载性能
- 架构优化:优化目录结构,职责划分更清晰,降低用户学习成本与上手难度
- 注释优化:统一模块注释规范,完善每一个组件介绍、功能说明与使用示例,降低用户理解成本与上手难度
- 性能提升:优化核心代码逻辑,提升系统运行效率
- 菜单优化:细化菜单样式,优化交互体验与视觉呈现
- 响应式优化:优化 ArtForm、ArtSearchBar 栅格布局,适配多种屏幕尺寸
- 配置管理优化:新增 setting.ts 配置文件,支持一键复制与重置系统默认设置
#### 修复
- 修复 ArtForm、ArtSearchBar 自定义组件渲染异常
#### 新增
- ArtForm、ArtSearchBar 新增 render 属性,支持自定义组件渲染
- 节日功能增强:礼花配置支持跨日期范围设置与自定义播放次数
#### 升级
- 依赖更新:升级核心依赖至最新稳定版本
#### ⚠️ 重要提示
> 本次升级涉及样式系统Sass → Tailwind CSS与图标库Iconfont → Iconify的底层重构属于破坏性更新。建议新项目直接使用 v3.0,旧版本项目不建议升级。
---
### v2.6.1 - 2025-10-19
#### 修复
- 修复获取用户信息、获取菜单接口访问无效地址重复调用问题
- 修复 ElButton circle 模式样式
- 修复 ElSelect 无法通过键盘选择问题
- 修复带参数静态路由跳转登录页面问题
#### 优化
- 升级部分依赖兼容 tailwindcss
- 优化外部链接菜单点击选中状态
- 授权页增加主题色切换功能
---
### v2.6.0 - 2025-10-16
#### 新增
- 扩展注册、密码重置页面顶部组件支持
#### 优化
- 优化精简版本菜单数据结构,提升数据一致性
- 优化本地开发环境网络请求代理配置
- 优化 ElTree 组件默认样式
- 新增 VsCode 推荐插件相关配置
- 优化 ElDropdown 组件点击触发模式下的交互样式
- 优化菜单过滤逻辑
- 优化页面切换动画,提升加载速度
- 优化暗黑模式文字颜色
#### 修复
- 修复静态路由自定义首页路径首次访问跳转登录页问题
- 修复退出登录时短暂跳转至 500 页的问题
- 修复 v2.5.9 版本首页路由跳转配置失效问题
- 修复 v2.5.9 自动导包机制导致的构建异常
#### ⚠️ 重要提示
> 此版本需要重新登录
---
### v2.5.9 - 2025-10-12
#### 代码优化
- views 文件目录、文件名、代码优化
- useTable 分页请求字段增加全局配置 tableConfig.ts
- 优化路由配置为模块化结构
- 获取菜单接口使用 apifox mock 数据(需在 .env 中将 VITE_ACCESS_MODE 设为 backend 模式)
---
### v2.5.8 - 2025-09-29
#### 升级
- vue、vite、element-plus 等核心库升级
#### 修复
- 修复富文本编辑器全屏顶栏层级问题
- 修复表格列排序组件文字溢出问题
- 修复统计卡片条件判断
- 修复根路径 / 与 HOME_PAGE_PATH 同为 / 时出现的无限重定向
#### 优化
- 优化 el-tag 样式
- 优化顶部进度条颜色
- 优化自定义主题配置
- 优化 ElementPlus 自定义主题问题
#### ⚠️ 重要提示
> 由于项目依赖升级node 版本需要升级到 v20.19.0 或以上
---
### v2.5.7 - 2025-09-14
#### 新增
- 新增 ArtForm 组件
#### 修复
- 修复新版本谷歌浏览器切换主题闪烁问题
#### 优化
- 优化表单 label 高度没有对齐问题
- 首屏启动性能优化
---
### v2.5.6 - 2025-09-06
#### 新增
- ArtTableHeader 新增搜索按钮,用于控制顶部搜索栏的显示与隐藏
- 新增权限演示示例
- 添加全局错误处理基础框架
#### 优化
- useTable 类型推导优化,不需要手动传递类型即可实现类型提示
- useTable removeColumn 支持多数据删除
- useTable 自动识别响应体支持自定义配置 (src/utils/table/tableConfig.ts)
- useTable 空数据浏览器警告优化
- api 接口请求代码优化、api.d.ts 类型优化
- 优化 ArtTable 顶部按钮换行无法自适应问题(示例:功能示例 / 左右布局表格)
- ArtTable 分页组件选中样式优化
- ArtTable 空状态高度默认撑满
- ArtButtonMore 组件新增图标、颜色配置
- ArtSearchBar label 为空时不占空间
- 表格操作栏拖拽禁止固定列拖拽
- 角色管理页面接口对接、代码优化
- 菜单管理页面优化
- 优化设置中心滚动页面跟随滚动问题
- 一级路由是外链时component 校验逻辑优化
- 优化地图右下角拖动问题
- 优化暗黑模式刷新页面白色背景问题
- 优化左侧菜单折叠按钮间距问题
- 移动端显示左侧菜单 logo
- 网络请求新增 showSuccessMessage用于配置是否显示成功消息
- 全局组件采用异步加载策略,提升首屏加载性能
#### 修复
- 修复批量删除整页数据没有返回上一页的bug
- 修复动态路由参数导致的问题
- 修复动态路由配置一级路由是 iframe 页面时,全屏问题
---
### v2.5.5 - 2025-08-17
#### 新增
- 新增 ArtSearchBar 组件示例
- useTable 新增 excludeParams用于排除某些参数不参与请求
#### 重构
- 重构 ArtSearchBar 组件,支持更多组件、表单校验等能力
#### 优化
- useTable 列配置:支持动态更新能力
- 优化颜色选择器圆角
- el-radio、el-checkbox 统一大小
- art-stats-card 新增小数位、分隔符配置
- 路由配置示例优化
- 高级表格新增自定义获取数据示例(等待其他请求完成后执行 useTable 数据获取)
- 优化路径别名类型问题
- 本地开发跨域配置优化
- useTable 属性、方法命名优化
- 登录页UI升级
- 403、404、500 页面UI升级
#### 修复
- 修复多个富文本编辑器图标不统一问题
- 修复 useTable 删除最后一整页数据没有返回上一页的问题
- 修复 echarts 图表数据初始化、更新数据浏览器报错
- 网络请求支持 http 状态码为 401 时退出登录
- 优化网络请求退出登录多次提示问题
#### 移除
- 删除 art-chart-empty 组件
---
### v2.5.4 - 2025-07-27
#### 新增
- 网络请求 headers 支持自定义配置
- 展开行支持 formatter 渲染
#### 优化
- 路由注册新增 component 校验
#### 修复
- 修复获取用户信息接口时序问题导致路由注册菜单渲染错误bug
- 修复动态路由校验问题导致的 iframe 不显示bug
- 修复 reset 文件语法错误
- 修复 ArtTable 数据类型错误
- 修复地图滚轮滚动放大问题
---
### v2.5.3 - 2025-07-20
#### 新增
- 新增表格左右布局示例
#### 重构
- ArtTable 组件重构
#### 升级
- Element Plus 升级到 v2.10.2
#### 优化
- 优化 useTable 分页参数问题
- 优化网络请求示例:初始化参数、分页携带参数问题
- 优化搜索日期范围参数处理
- 优化 el-date-picker 组件圆角问题
- 优化 el-select 组件 hover 样式
- 搜索组件、分页组件高度降低
- 优化登录页面滑块动画间隔时长
- 优化菜单没有子菜单显示的问题
#### 修复
- 修复 ArtTable 切换分页大小时执行两次请求bug
---
### v2.5.2 - 2025-07-13
#### 新增
- 新增一键精简脚本,快速准备开发环境
- useTable hooks 支持分页字段名自定义映射
- 水平菜单、混合菜单、双列菜单支持徽章显示
- 快速入口支持配置文件模式
- 顶栏功能支持配置文件模式
- 支持自定义首页路径
- 路由支持配置 redirect 等属性
#### 优化
- 全局事件总线 mittBus 类型安全优化
- 优化移动端设置中容器宽度样式
- 优化登录页验证滑块文字居中效果
#### 修复
- 修复表格无数据时表头不显示问题
- 修复 v2.5.0 顶部进度条不显示问题
- 修复左侧菜单遮罩异常显示问题
- 修复隐藏所有子菜单时仍显示父级菜单的问题
- 修复 stylelint 导致的登录页滑块样式异常
- 修复老旧移动端设备 loading 定位问题
---
### v2.5.1 - 2025-07-08
#### 优化
- el-card、el-table 背景色跟系统保持一致
#### 修复
- 修复首次登录系统时 loading 提前关闭bug
- 修复 v2.5.0 版本引起的全屏页样式层级过低bug
- 修复 v2.5.0 版本引起的表格展开行折叠bug
---
### v2.5.0 - 2025-07-06 🎉
#### 新增
- 新增 useTable hooks 表格封装,支持数据获取、转换、响应适配、智能缓存(基于 LRU 算法)、错误处理、列配置与插槽、分页控制、刷新策略等核心功能,全面提升开发效率与用户体验
#### 重构
- 重构 ArtTable、ArtTableHeader、ArtNotification 组件
#### 升级
- Echarts 版本升级到 5.6.0
#### 移除
- 移除 CountTo 插件,替换为 ArtCountTo 组件
#### 修复
- 修复菜单管理搜索直接修改 pinia 数据的问题
- 修复路由守卫 loading 闪烁问题
#### ⚠️ 重要提示
> 建议升级,带来更高效、更智能的表格开发体验
---
### v2.4.2.9 - 2025-07-02
#### 重构
- 菜单布局、顶部导航代码重构
#### 优化
- 优化移动端菜单滚动用户体验
- 优化顶部菜单样式问题
- 顶部菜单宽度自适应,可显示更多内容,混合菜单支持鼠标滚动
- asyncRoutes 路由配置 auth_mark 字段改为 authMark
- 去除重复的 components.d.ts 文件components.d.ts、auto-imports.d.ts 忽略提交
- 优化国际化语言文件加载方式,异步改成同步模式
- 优化 el-pagination 大小不一致问题
#### 修复
- 修复移动端锁屏页部分浏览器无法解锁bug
---
### v2.4.2.8 - 2025-06-26
#### 修复
- 修复 v2.4.2.7 版本访问 / 路径时显示 404 的问题
---
### v2.4.2.7 - 2025-06-25
#### 新增
- 路由支持配置全屏模式
- 路由支持自动跳转到菜单的第一个有效路由
- 动态路由新增 removeAllDynamicRoutes 方法,可用于彻底清除所有动态路由
- 权限自定义指令优化、新增角色权限指令 v-roles、可用于控制元素的显示与隐藏
- 新增标签页操作示例
#### 优化
- 跨域请求携带 cookie 配置从环境变量中获取,默认关闭
- 针对SEO、可访问性做一些优化
#### 修复
- 修复登录页面拖拽组件 ArtDragVerify 宽度、颜色异常bug
- 修复 iframe 页面混合模式、双列模式异常bug
- 优化锁屏页面被 el-loading 穿透bug
---
### v2.4.2.6 - 2025-06-23
#### 重构
- 重构 components/core/forms 文件夹下的表单相关组件,提升可维护性与一致性
- 重构 ArtBreadcrumb 面包屑导航组件,优化逻辑结构与样式
- 重构 ArtFireworksEffect 烟花效果组件,显著提升渲染性能与动画流畅度
#### 优化
- 优化 ArtChatWindow 与 ArtFastEnter 组件代码,提升可读性与性能
#### 新增
- README 文档新增官方网站链接,便于用户查看项目文档
---
### v2.4.2.5 - 2025-06-22
#### 重构
- 重构图表组件,优化代码结构与可维护性
- 精细调整图表动画与主题配色方案,提升视觉一致性
---
### v2.4.2.4 - 2025-06-18
#### 重构
- ArtMenuRight 组件重构
- components/core/cards 下面的组件重构,代码优化
#### 优化
- ArtWatermark 增加类型注释
---
### v2.4.2.3 - 2025-06-18
#### 重构
- ArtResultPage 组件重构
#### 优化
- ArtTextScroll 组件代码优化
- ArtException 组件增加类型提示
- ArtCutterImg 组件样式优化、增加类型定义
- ArtVideoPlayer 组件增加类型定义
---
### v2.4.2.2 - 2025-06-16
#### 重构
- 返回顶部组件重构
- 图标选择器组件重构
#### 修改
- 系统Logo组件属性变更
---
### v2.4.2.1 - 2025-06-16
#### 重构
- 横幅组件重构以及优化
#### 修复
- 修复混合菜单下第一个菜单是嵌套菜单跳转bug
---
### v2.4.2 - 2025-06-14
#### 重构
- 重构网络请求模块,增强错误处理、类型安全与多语言支持
#### 新增
- 路由配置新增 activePath 激活菜单路径属性
#### 优化
- 引导、列设置多语言完善
- 去除用户列表、菜单管理页面无效代码
- 更新技术支持链接
#### 修复
- 修复移动端搜索栏无法滚动、iPad端页面滚动异常问题
- 修复 el-dialog 启用 draggable 属性后,自定义动画失效的问题
- 修复 2.3.0 版本本地存储重构后,导致登录、注册等页面多语言设置无法持久化的问题
- 修复表格固定列不起作用bug
- 修复根路径 / 与 HOME_PAGE_PATH 同为 / 时出现的无限重定向
#### ⚠️ 重要提示
> 此版本需要重新登录
---
### v2.4.1.1 - 2025-06-07
#### 优化
- 优化角色管理页面代码
- el-dialog视觉效果优化支持配置线条
- 系统主题模式从Light改成跟随系统模式
#### 修复
- 修复菜单管理折叠 bug
- 修复表格数据为空高度无限变大bug
---
### v2.4.1 - 2025-06-07
#### 新增
- 锁屏状态下禁止使用开发者工具破解锁屏
#### 优化
- 提升菜单操作跟手感
- 页面入场动画时间减少0.04s
- Echarts 图表性能优化,新增可视区域初始化、内存泄漏防护、防抖处理
#### 修复
- 修复 Echarts 图表组件在弹窗中不显示的 bug
---
### v2.4.0 - 2025-06-06 🎉
#### 重构
- 全局 TypeScript 类型体系重构,提升类型准确性与可维护性
- 重构 utils 工具包,统一工具方法结构,增强可读性与复用性
#### 新增
- utils 新增表单验证与 Cookie 操作相关工具函数
- 页面组件增加 defineOptions明确组件命名
- HTTP 请求增加 token 过期自动处理逻辑,提升安全性与用户体验
#### 优化
- 删除未使用的工具模块与无效资源,精简项目体积
- 优化 views 页面结构,移除冗余页面文件
- 异常页面多语言支持, 提升国际化体验
- 图片资源统一转换为 webp 格式,整体资源体积减少约 50%
- 打包产物减少约 1MB提高加载效率
#### ⚠️ 重要提示
> 此版本需要重新登录
---
## 版本说明
### 版本号规则
本项目遵循 [语义化版本 2.0.0](https://semver.org/lang/zh-CN/) 规范:
- **主版本号Major**:当你做了不兼容的 API 修改
- **次版本号Minor**:当你做了向下兼容的功能性新增
- **修订号Patch**:当你做了向下兼容的问题修正
### 变更类型
- **新增Added**:新功能
- **修改Changed**:对现有功能的变更
- **弃用Deprecated**:即将移除的功能
- **移除Removed**:已移除的功能
- **修复Fixed**Bug 修复
- **安全Security**:安全相关的修复
- **优化Optimized**:性能优化或体验优化
- **重构Refactored**:代码重构
- **升级Upgraded**:依赖升级
### 重要提示标记
- 🎉 **重大更新**:包含重要新特性的版本
- ⚠️ **破坏性变更**:需要特别注意的不兼容更新
- 🔒 **安全更新**:修复安全漏洞的版本
---
## 贡献指南
如果你想为项目做出贡献,请查看 [CONTRIBUTING.md](./CONTRIBUTING.md)。
## 许可证
[MIT License](./LICENSE)
---
**注意**:本更新日志从 v2.4.0 版本开始记录。更早的版本历史请查看 Git 提交记录。
---
[English Version](./CHANGELOG.en.md)

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 SuperManTT
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,104 +0,0 @@
<img src="https://www.qiniu.lingchen.kim/github-cover-light6.webp" />
<br />
<h1 align="center">Art Design Pro</h1>
<p align="center">A backend system template that combines design aesthetics with efficient development, helping you quickly build professional-grade applications</p>
<div align="center">English | <a href="./README.zh-CN.md">简体中文</a></div>
<br />
<div align="center">
[![license](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE) [![github stars](https://img.shields.io/github/stars/Daymychen/art-design-pro)](https://github.com/Daymychen/art-design-pro/stargazers) [![github forks](https://img.shields.io/github/forks/Daymychen/art-design-pro)](https://github.com/Daymychen/art-design-pro/network/members)
</div>
<br />
## What makes this project special?
**Interface Design**: Modern UI design with smooth interactions, focusing on user experience and visual design
**Quick Start**: Clean architecture + comprehensive documentation, easy for backend developers to use
**Rich Components**: Built-in high-quality components for data display, forms, and more to meet different business scenarios
**Smooth Interactions**: Button clicks, theme switching, page transitions, chart animations - experience comparable to commercial products
**Efficient Development**: Built-in practical APIs like useTable and ArtForm to significantly improve development efficiency
**Clean Scripts**: Built-in one-click cleanup script to quickly remove demo data and get a ready-to-develop base project
## Tech Stack
Development Framework: Vue3, TypeScript, Vite, Element-Plus, Tailwind CSS
Code Standards: Eslint, Prettier, Stylelint, Husky, Lint-staged, cz-git
## Preview
<kbd><img src="https://www.qiniu.lingchen.kim/github-c1.webp" alt="Light Theme"/></kbd>
<kbd><img src="https://www.qiniu.lingchen.kim/github-c2.webp" alt="Light Theme"/></kbd>
<kbd><img src="https://www.qiniu.lingchen.kim/github-c4.webp" alt="Dark Theme"/></kbd>
<kbd><img src="https://www.qiniu.lingchen.kim/github-c5.webp" alt="Dark Theme"/></kbd>
## Quick Access
[Live Demo](https://www.artd.pro) | [Official Documentation](https://www.artd.pro/docs) | [Changelog](./CHANGELOG.en.md)
## Installation & Setup
```bash
# Install dependencies
pnpm install
# If pnpm install fails, try using the command below
pnpm install --ignore-scripts
# Start local development environment
pnpm dev
# Build for production
pnpm build
```
## Clean Version
The project includes a cleanup script to quickly remove demo data and provide developers with a ready-to-develop base project
```bash
pnpm clean:dev
```
## Technical Support
QQ Group: <a href="https://qm.qq.com/cgi-bin/qm/qr?k=Gg6yzZLFaNgmRhK0T5Qcjf7-XcAFWWXm&jump_from=webapi&authKey=YpRKVJQyFKYbGTiKw0GJ/YQXnNF+GdXNZC5beQQqnGZTvuLlXoMO7nw5fNXvmVhA">821834289</a> (Click the link to join the group chat)
## Browser Compatibility
Supports modern mainstream browsers including Chrome, Safari, Firefox, and more.
## Contributing
We sincerely welcome and appreciate the support of every contributor! Whether you have new ideas, feature suggestions, or code optimizations, you can participate in the following ways:
Submit Pull Requests: Share your code and help the project grow.
Create GitHub Issues: Provide bug feedback or new feature suggestions to help us improve together.
Every contribution you make takes this project one step further! Come join our open source community!
## Continuous Optimization & Extension
The project maintains active updates, supports the latest frontend tech stack, is compatible with mainstream frameworks, and ensures long-term stability and extensibility. Community-driven feedback mechanisms allow your needs to be quickly integrated into project iterations.
## Donation
If you feel this project has reduced your development costs and solved problems in your work/life, you can support us through the following ways:
<img src="https://www.qiniu.lingchen.kim/%E7%BB%84%202%402x%202.png" alt="Donation QR Code"/>
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=Daymychen/art-design-pro&type=Date)](https://www.star-history.com/#Daymychen/art-design-pro&Date)

View File

@ -1,104 +0,0 @@
<img src="https://www.qiniu.lingchen.kim/github-cover-light6.webp" />
<br />
<h1 align="center">Art Design Pro</h1>
<p align="center">一款兼具设计美学与高效开发的后台系统模版,助你快速构建专业级应用</p>
<div align="center">简体中文 | <a href="./README.md">English</a></div>
<br />
<div align="center">
[![license](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE) [![github stars](https://img.shields.io/github/stars/Daymychen/art-design-pro)](https://github.com/Daymychen/art-design-pro/stargazers) [![github forks](https://img.shields.io/github/forks/Daymychen/art-design-pro)](https://github.com/Daymychen/art-design-pro/network/members)
</div>
<br />
## 这个项目有什么特别的呢?
**界面设计**:现代化 UI 设计,流畅交互,以用户体验与视觉设计为核心
**极速上手**:简洁架构 + 完整文档,后端开发者也能轻松使用
**丰富组件**:内置数据展示、表单等多种高质量组件,满足不同业务场景的需求
**丝滑交互**:按钮点击、主题切换、页面过渡、图表动画,体验媲美商业产品
**高效开发**:内置 useTable、ArtForm 等实用 API显著提升开发效率
**精简脚本**:内置一键清理脚本,可快速清理演示数据,立即得到可开发的基础项目
## 技术栈
开发框架Vue3、TypeScript、Vite、Element-Plus、Tailwind CSS
代码规范Eslint、Prettier、Stylelint、Husky、Lint-staged、cz-git
## 预览
<kbd><img src="https://www.qiniu.lingchen.kim/github-c1.webp" alt="浅色主题"/></kbd>
<kbd><img src="https://www.qiniu.lingchen.kim/github-c2.webp" alt="浅色主题"/></kbd>
<kbd><img src="https://www.qiniu.lingchen.kim/github-c4.webp" alt="暗黑主题"/></kbd>
<kbd><img src="https://www.qiniu.lingchen.kim/github-c5.webp" alt="暗黑主题"/></kbd>
## 快速访问
[演示地址](https://www.artd.pro) | [官方文档](https://www.artd.pro/docs) | [更新日志](./CHANGELOG.md)
## 安装运行
```bash
# 安装依赖
pnpm install
# 如果 pnpm install 安装失败,尝试使用下面的命令安装依赖
pnpm install --ignore-scripts
# 本地开发环境启动
pnpm dev
# 生产环境打包
pnpm build
```
## 精简版本
项目内置精简脚本,可快速移除项目中的演示数据,让开发者获得一个可快速开发的基础项目
```bash
pnpm clean:dev
```
## 技术支持
QQ群<a href="https://qm.qq.com/cgi-bin/qm/qr?k=Gg6yzZLFaNgmRhK0T5Qcjf7-XcAFWWXm&jump_from=webapi&authKey=YpRKVJQyFKYbGTiKw0GJ/YQXnNF+GdXNZC5beQQqnGZTvuLlXoMO7nw5fNXvmVhA">821834289</a>(点击链接加入群聊)
## 兼容性
支持 Chrome、Safari、Firefox 等现代主流浏览器。
## 贡献
我们真诚欢迎并感谢每一位贡献者的支持!无论您有新想法、功能建议还是代码优化,都可以通过以下方式参与:
提交 Pull Request分享您的代码助力项目成长。
创建 GitHub Issue提出 bug 反馈或新功能建议,让我们一起完善。
您的每一点贡献都让这个项目更进一步!快来加入我们的开源社区吧!
## 持续优化与扩展
项目保持活跃更新,支持最新前端技术栈,兼容主流框架,确保长期稳定性和扩展性。社区驱动的反馈机制,让你的需求快速融入项目迭代。
## 捐赠
如果你觉得这个项目为你减少了开发成本、化解了工作 / 生活里的难题,可以通过以下方式支持一下~
<img src="https://www.qiniu.lingchen.kim/%E7%BB%84%202%402x%202.png" alt="捐赠二维码"/>
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=Daymychen/art-design-pro&type=Date)](https://www.star-history.com/#Daymychen/art-design-pro&Date)

View File

@ -1,97 +0,0 @@
/**
* commitlint 配置文件
* 文档
* https://commitlint.js.org/#/reference-rules
* https://cz-git.qbb.sh/zh/guide/
*/
module.exports = {
// 继承的规则
extends: ['@commitlint/config-conventional'],
// 自定义规则
rules: {
// 提交类型枚举git提交type必须是以下类型
'type-enum': [
2,
'always',
[
'feat', // 新增功能
'fix', // 修复缺陷
'docs', // 文档变更
'style', // 代码格式(不影响功能,例如空格、分号等格式修正)
'refactor', // 代码重构(不包括 bug 修复、功能新增)
'perf', // 性能优化
'test', // 添加疏漏测试或已有测试改动
'build', // 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)
'ci', // 修改 CI 配置、脚本
'revert', // 回滚 commit
'chore', // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)
'wip' // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)
]
],
'subject-case': [0] // subject大小写不做校验
},
prompt: {
messages: {
type: '选择你要提交的类型 :',
scope: '选择一个提交范围(可选):',
customScope: '请输入自定义的提交范围 :',
subject: '填写简短精炼的变更描述 :\n',
body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
footerPrefixesSelect: '选择关联issue前缀可选:',
customFooterPrefix: '输入自定义issue前缀 :',
footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
generatingByAI: '正在通过 AI 生成你的提交简短描述...',
generatedSelectByAI: '选择一个 AI 生成的简短描述:',
confirmCommit: '是否提交或修改commit ?'
},
// prettier-ignore
types: [
{ value: "feat", name: "feat: 新增功能" },
{ value: "fix", name: "fix: 修复缺陷" },
{ value: "docs", name: "docs: 文档变更" },
{ value: "style", name: "style: 代码格式(不影响功能,例如空格、分号等格式修正)" },
{ value: "refactor", name: "refactor: 代码重构(不包括 bug 修复、功能新增)" },
{ value: "perf", name: "perf: 性能优化" },
{ value: "test", name: "test: 添加疏漏测试或已有测试改动" },
{ value: "build", name: "build: 构建流程、外部依赖变更(如升级 npm 包、修改 vite 配置等)" },
{ value: "ci", name: "ci: 修改 CI 配置、脚本" },
{ value: "revert", name: "revert: 回滚 commit" },
{ value: "chore", name: "chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)" },
],
useEmoji: true,
emojiAlign: 'center',
useAI: false,
aiNumber: 1,
themeColorCode: '',
scopes: [],
allowCustomScopes: true,
allowEmptyScopes: true,
customScopesAlign: 'bottom',
customScopesAlias: 'custom',
emptyScopesAlias: 'empty',
upperCaseSubject: false,
markBreakingChangeMode: false,
allowBreakingChanges: ['feat', 'fix'],
breaklineNumber: 100,
breaklineChar: '|',
skipQuestions: ['breaking', 'footerPrefix', 'footer'], // 跳过的步骤
issuePrefixes: [{ value: 'closed', name: 'closed: ISSUES has been processed' }],
customIssuePrefixAlign: 'top',
emptyIssuePrefixAlias: 'skip',
customIssuePrefixAlias: 'custom',
allowCustomIssuePrefix: true,
allowEmptyIssuePrefix: true,
confirmColorize: true,
maxHeaderLength: Infinity,
maxSubjectLength: Infinity,
minSubjectLength: 0,
scopeOverrides: undefined,
defaultBody: '',
defaultIssues: '',
defaultScope: '',
defaultSubject: ''
}
}

View File

@ -1,83 +0,0 @@
// 从 URL 和路径模块中导入必要的功能
import fs from 'fs'
import path, { dirname } from 'path'
import { fileURLToPath } from 'url'
// 从 ESLint 插件中导入推荐配置
import pluginJs from '@eslint/js'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import pluginVue from 'eslint-plugin-vue'
import globals from 'globals'
import tseslint from 'typescript-eslint'
// 使用 import.meta.url 获取当前模块的路径
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
// 读取 .auto-import.json 文件的内容,并将其解析为 JSON 对象
const autoImportConfig = JSON.parse(
fs.readFileSync(path.resolve(__dirname, '.auto-import.json'), 'utf-8')
)
export default [
// 指定文件匹配规则
{
files: ['**/*.{js,mjs,cjs,ts,vue}']
},
// 指定全局变量和环境
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
// 扩展配置
pluginJs.configs.recommended,
...tseslint.configs.recommended,
...pluginVue.configs['flat/essential'],
// 自定义规则
{
// 针对所有 JavaScript、TypeScript 和 Vue 文件应用以下配置
files: ['**/*.{js,mjs,cjs,ts,vue}'],
languageOptions: {
globals: {
// 合并从 autoImportConfig 中读取的全局变量配置
...autoImportConfig.globals,
// TypeScript 全局命名空间
Api: 'readonly'
}
},
rules: {
quotes: ['error', 'single'], // 使用单引号
semi: ['error', 'never'], // 语句末尾不加分号
'no-var': 'error', // 要求使用 let 或 const 而不是 var
'@typescript-eslint/no-explicit-any': 'off', // 禁用 any 检查
'vue/multi-word-component-names': 'off', // 禁用对 Vue 组件名称的多词要求检查
'no-multiple-empty-lines': ['warn', { max: 1 }], // 不允许多个空行
'no-unexpected-multiline': 'error' // 禁止空余的多行
}
},
// vue 规则
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: { parser: tseslint.parser }
}
},
// 忽略文件
{
ignores: [
'node_modules',
'dist',
'public',
'.vscode/**',
'src/assets/**',
'src/utils/console.ts'
]
},
// prettier 配置
eslintPluginPrettierRecommended
]

View File

@ -1,47 +0,0 @@
<!doctype html>
<html>
<head>
<title>Art Design Pro</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Art Design Pro - A modern admin dashboard template built with Vue 3, TypeScript, and Element Plus."
/>
<link rel="shortcut icon" type="image/x-icon" href="src/assets/images/favicon.ico" />
<style>
/* 防止页面刷新时白屏的初始样式 */
html {
background-color: #fafbfc;
}
html.dark {
background-color: #070707;
}
</style>
<script>
// 初始化 html class 主题属性
;(function () {
try {
if (typeof Storage === 'undefined' || !window.localStorage) {
return
}
const themeType = localStorage.getItem('sys-theme')
if (themeType === 'dark') {
document.documentElement.classList.add('dark')
}
} catch (e) {
console.warn('Failed to apply initial theme:', e)
}
})()
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -1,123 +0,0 @@
{
"name": "art-design-pro",
"version": "0.0.0",
"type": "module",
"engines": {
"node": ">=20.19.0",
"pnpm": ">=8.8.0"
},
"scripts": {
"dev": "vite --open",
"build": "vue-tsc --noEmit && vite build",
"serve": "vite preview",
"lint": "eslint",
"fix": "eslint --fix",
"lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,css,less,scss,vue,html,md}\"",
"lint:stylelint": "stylelint \"**/*.{css,scss,vue}\" --fix",
"lint:lint-staged": "lint-staged",
"prepare": "husky",
"commit": "git-cz",
"clean:dev": "tsx scripts/clean-dev.ts"
},
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
},
"lint-staged": {
"*.{js,ts,mjs,mts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{cjs,json,jsonc}": [
"prettier --write"
],
"*.vue": [
"eslint --fix",
"stylelint --fix --allow-empty-input",
"prettier --write"
],
"*.{html,htm}": [
"prettier --write"
],
"*.{scss,css,less}": [
"stylelint --fix --allow-empty-input",
"prettier --write"
],
"*.{md,mdx}": [
"prettier --write"
],
"*.{yaml,yml}": [
"prettier --write"
]
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@iconify/vue": "^5.0.0",
"@tailwindcss/vite": "^4.1.14",
"@vue/reactivity": "^3.5.21",
"@vueuse/core": "^13.9.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "next",
"axios": "^1.12.2",
"crypto-js": "^4.2.0",
"echarts": "^6.0.0",
"element-plus": "^2.11.2",
"file-saver": "^2.0.5",
"highlight.js": "^11.10.0",
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
"ohash": "^2.0.11",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.3.0",
"qrcode.vue": "^3.6.0",
"tailwindcss": "^4.1.14",
"vue": "^3.5.21",
"vue-draggable-plus": "^0.6.0",
"vue-i18n": "^9.14.0",
"vue-router": "^4.5.1",
"xgplayer": "^3.0.20",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@commitlint/cli": "^19.4.1",
"@commitlint/config-conventional": "^19.4.1",
"@eslint/js": "^9.9.1",
"@types/node": "^24.0.5",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/compiler-sfc": "^3.0.5",
"commitizen": "^4.3.0",
"cz-git": "^1.11.1",
"eslint": "^9.9.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.27.0",
"globals": "^15.9.0",
"husky": "^9.1.5",
"lint-staged": "^15.5.2",
"prettier": "^3.5.3",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.81.0",
"stylelint": "^16.20.0",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recess-order": "^4.6.0",
"stylelint-config-recommended-scss": "^14.1.0",
"stylelint-config-recommended-vue": "^1.5.0",
"stylelint-config-standard": "^36.0.1",
"terser": "^5.36.0",
"tsx": "^4.20.3",
"typescript": "~5.6.3",
"typescript-eslint": "^8.9.0",
"unplugin-auto-import": "^20.2.0",
"unplugin-element-plus": "^0.10.0",
"unplugin-vue-components": "^29.1.0",
"vite": "^7.1.5",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-devtools": "^7.7.6",
"vue-demi": "^0.14.9",
"vue-img-cutter": "^3.0.5",
"vue-tsc": "~2.1.6"
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,838 +0,0 @@
// scripts/clean-dev.ts
import fs from 'fs/promises'
import path from 'path'
// 现代化颜色主题
const theme = {
// 基础颜色
reset: '\x1b[0m',
bold: '\x1b[1m',
dim: '\x1b[2m',
// 前景色
primary: '\x1b[38;5;75m', // 亮蓝色
success: '\x1b[38;5;82m', // 亮绿色
warning: '\x1b[38;5;220m', // 亮黄色
error: '\x1b[38;5;196m', // 亮红色
info: '\x1b[38;5;159m', // 青色
purple: '\x1b[38;5;141m', // 紫色
orange: '\x1b[38;5;208m', // 橙色
gray: '\x1b[38;5;245m', // 灰色
white: '\x1b[38;5;255m', // 白色
// 背景色
bgDark: '\x1b[48;5;235m', // 深灰背景
bgBlue: '\x1b[48;5;24m', // 蓝色背景
bgGreen: '\x1b[48;5;22m', // 绿色背景
bgRed: '\x1b[48;5;52m' // 红色背景
}
// 现代化图标集
const icons = {
rocket: '🚀',
fire: '🔥',
star: '⭐',
gem: '💎',
crown: '👑',
magic: '✨',
warning: '⚠️',
success: '✅',
error: '❌',
info: '',
folder: '📁',
file: '📄',
image: '🖼️',
code: '💻',
data: '📊',
globe: '🌐',
map: '🗺️',
chat: '💬',
bolt: '⚡',
shield: '🛡️',
key: '🔑',
link: '🔗',
clean: '🧹',
trash: '🗑️',
check: '✓',
cross: '✗',
arrow: '→',
loading: '⏳'
}
// 格式化工具
const fmt = {
title: (text: string) => `${theme.bold}${theme.primary}${text}${theme.reset}`,
subtitle: (text: string) => `${theme.purple}${text}${theme.reset}`,
success: (text: string) => `${theme.success}${text}${theme.reset}`,
error: (text: string) => `${theme.error}${text}${theme.reset}`,
warning: (text: string) => `${theme.warning}${text}${theme.reset}`,
info: (text: string) => `${theme.info}${text}${theme.reset}`,
highlight: (text: string) => `${theme.bold}${theme.white}${text}${theme.reset}`,
dim: (text: string) => `${theme.dim}${theme.gray}${text}${theme.reset}`,
orange: (text: string) => `${theme.orange}${text}${theme.reset}`,
// 带背景的文本
badge: (text: string, bg: string = theme.bgBlue) =>
`${bg}${theme.white}${theme.bold} ${text} ${theme.reset}`,
// 渐变效果模拟
gradient: (text: string) => {
const colors = ['\x1b[38;5;75m', '\x1b[38;5;81m', '\x1b[38;5;87m', '\x1b[38;5;159m']
const chars = text.split('')
return chars.map((char, i) => `${colors[i % colors.length]}${char}`).join('') + theme.reset
}
}
// 创建现代化标题横幅
function createModernBanner() {
console.log()
console.log(
fmt.gradient(' ╔══════════════════════════════════════════════════════════════════╗')
)
console.log(
fmt.gradient(' ║ ║')
)
console.log(
`${icons.rocket} ${fmt.title('ART DESIGN PRO')} ${fmt.subtitle('· 代码精简程序')} ${icons.magic}`
)
console.log(
`${fmt.dim('为项目移除演示数据,快速切换至开发模式')}`
)
console.log(
fmt.gradient(' ║ ║')
)
console.log(
fmt.gradient(' ╚══════════════════════════════════════════════════════════════════╝')
)
console.log()
}
// 创建分割线
function createDivider(char = '─', color = theme.primary) {
console.log(`${color}${' ' + char.repeat(66)}${theme.reset}`)
}
// 创建卡片样式容器
function createCard(title: string, content: string[]) {
console.log(` ${fmt.badge('', theme.bgBlue)} ${fmt.title(title)}`)
console.log()
content.forEach((line) => {
console.log(` ${line}`)
})
console.log()
}
// 进度条动画
function createProgressBar(current: number, total: number, text: string, width = 40) {
const percentage = Math.round((current / total) * 100)
const filled = Math.round((current / total) * width)
const empty = width - filled
const filledBar = '█'.repeat(filled)
const emptyBar = '░'.repeat(empty)
process.stdout.write(
`\r ${fmt.info('进度')} [${theme.success}${filledBar}${theme.gray}${emptyBar}${theme.reset}] ${fmt.highlight(percentage + '%')})}`
)
if (current === total) {
console.log()
}
}
// 统计信息
const stats = {
deletedFiles: 0,
deletedPaths: 0,
failedPaths: 0,
startTime: Date.now(),
totalFiles: 0
}
// 清理目标
const targets = [
'README.md',
'README.zh-CN.md',
'CHANGELOG.md',
'CHANGELOG.zh-CN.md',
'src/views/change',
'src/views/safeguard',
'src/views/article',
'src/views/examples',
'src/views/system/nested',
'src/views/widgets',
'src/views/template',
'src/views/dashboard/analysis',
'src/views/dashboard/ecommerce',
'src/mock/json',
'src/mock/temp/articleList.ts',
'src/mock/temp/commentDetail.ts',
'src/mock/temp/commentList.ts',
'src/assets/images/cover',
'src/assets/images/safeguard',
'src/assets/images/3d',
'src/components/core/charts/art-map-chart',
'src/components/business/comment-widget'
]
// 递归统计文件数量
async function countFiles(targetPath: string): Promise<number> {
const fullPath = path.resolve(process.cwd(), targetPath)
try {
const stat = await fs.stat(fullPath)
if (stat.isFile()) {
return 1
} else if (stat.isDirectory()) {
const entries = await fs.readdir(fullPath)
let count = 0
for (const entry of entries) {
const entryPath = path.join(targetPath, entry)
count += await countFiles(entryPath)
}
return count
}
} catch {
return 0
}
return 0
}
// 统计所有目标的文件数量
async function countAllFiles(): Promise<number> {
let totalCount = 0
for (const target of targets) {
const count = await countFiles(target)
totalCount += count
}
return totalCount
}
// 删除文件和目录
async function remove(targetPath: string, index: number) {
const fullPath = path.resolve(process.cwd(), targetPath)
createProgressBar(index + 1, targets.length, targetPath)
try {
const fileCount = await countFiles(targetPath)
await fs.rm(fullPath, { recursive: true, force: true })
stats.deletedFiles += fileCount
stats.deletedPaths++
await new Promise((resolve) => setTimeout(resolve, 50))
} catch (err) {
stats.failedPaths++
console.log()
console.log(` ${icons.error} ${fmt.error('删除失败')}: ${fmt.highlight(targetPath)}`)
console.log(` ${fmt.dim('错误详情: ' + err)}`)
}
}
// 清理路由模块
async function cleanRouteModules() {
const modulesPath = path.resolve(process.cwd(), 'src/router/modules')
try {
// 删除演示相关的路由模块
const modulesToRemove = [
'template.ts',
'widgets.ts',
'examples.ts',
'article.ts',
'safeguard.ts',
'help.ts'
]
for (const module of modulesToRemove) {
const modulePath = path.join(modulesPath, module)
try {
await fs.rm(modulePath, { force: true })
} catch {
// 文件不存在时忽略错误
}
}
// 重写 dashboard.ts - 只保留 console
const dashboardContent = `import { AppRouteRecord } from '@/types/router'
export const dashboardRoutes: AppRouteRecord = {
name: 'Dashboard',
path: '/dashboard',
component: '/index/index',
meta: {
title: 'menus.dashboard.title',
icon: 'ri:pie-chart-line',
roles: ['R_SUPER', 'R_ADMIN']
},
children: [
{
path: 'console',
name: 'Console',
component: '/dashboard/console',
meta: {
title: 'menus.dashboard.console',
keepAlive: false,
fixedTab: true
}
}
]
}
`
await fs.writeFile(path.join(modulesPath, 'dashboard.ts'), dashboardContent, 'utf-8')
// 重写 system.ts - 移除 nested 嵌套菜单
const systemContent = `import { AppRouteRecord } from '@/types/router'
export const systemRoutes: AppRouteRecord = {
path: '/system',
name: 'System',
component: '/index/index',
meta: {
title: 'menus.system.title',
icon: 'ri:user-3-line',
roles: ['R_SUPER', 'R_ADMIN']
},
children: [
{
path: 'user',
name: 'User',
component: '/system/user',
meta: {
title: 'menus.system.user',
keepAlive: true,
roles: ['R_SUPER', 'R_ADMIN']
}
},
{
path: 'role',
name: 'Role',
component: '/system/role',
meta: {
title: 'menus.system.role',
keepAlive: true,
roles: ['R_SUPER']
}
},
{
path: 'user-center',
name: 'UserCenter',
component: '/system/user-center',
meta: {
title: 'menus.system.userCenter',
isHide: true,
keepAlive: true,
isHideTab: true
}
},
{
path: 'menu',
name: 'Menus',
component: '/system/menu',
meta: {
title: 'menus.system.menu',
keepAlive: true,
roles: ['R_SUPER'],
authList: [
{ title: '新增', authMark: 'add' },
{ title: '编辑', authMark: 'edit' },
{ title: '删除', authMark: 'delete' }
]
}
}
]
}
`
await fs.writeFile(path.join(modulesPath, 'system.ts'), systemContent, 'utf-8')
// 重写 index.ts - 只导入保留的模块
const indexContent = `import { AppRouteRecord } from '@/types/router'
import { dashboardRoutes } from './dashboard'
import { systemRoutes } from './system'
import { resultRoutes } from './result'
import { exceptionRoutes } from './exception'
/**
*
*/
export const routeModules: AppRouteRecord[] = [
dashboardRoutes,
systemRoutes,
resultRoutes,
exceptionRoutes
]
`
await fs.writeFile(path.join(modulesPath, 'index.ts'), indexContent, 'utf-8')
console.log(` ${icons.success} ${fmt.success('清理路由模块完成')}`)
} catch (err) {
console.log(` ${icons.error} ${fmt.error('清理路由模块失败')}`)
console.log(` ${fmt.dim('错误详情: ' + err)}`)
}
}
// 清理路由别名
async function cleanRoutesAlias() {
const routesAliasPath = path.resolve(process.cwd(), 'src/router/routesAlias.ts')
try {
const cleanedAlias = `/**
*
#
*/
export enum RoutesAlias {
Layout = '/index/index', // 布局容器
Login = '/auth/login' // 登录页
}
`
await fs.writeFile(routesAliasPath, cleanedAlias, 'utf-8')
console.log(` ${icons.success} ${fmt.success('重写路由别名配置完成')}`)
} catch (err) {
console.log(` ${icons.error} ${fmt.error('清理路由别名失败')}`)
console.log(` ${fmt.dim('错误详情: ' + err)}`)
}
}
// 清理变更日志
async function cleanChangeLog() {
const changeLogPath = path.resolve(process.cwd(), 'src/mock/upgrade/changeLog.ts')
try {
const cleanedChangeLog = `import { ref } from 'vue'
interface UpgradeLog {
version: string // 版本号
title: string // 更新标题
date: string // 更新日期
detail?: string[] // 更新内容
requireReLogin?: boolean // 是否需要重新登录
remark?: string // 备注
}
export const upgradeLogList = ref<UpgradeLog[]>([])
`
await fs.writeFile(changeLogPath, cleanedChangeLog, 'utf-8')
console.log(` ${icons.success} ${fmt.success('清空变更日志数据完成')}`)
} catch (err) {
console.log(` ${icons.error} ${fmt.error('清理变更日志失败')}`)
console.log(` ${fmt.dim('错误详情: ' + err)}`)
}
}
// 清理语言文件
async function cleanLanguageFiles() {
const languageFiles = [
{ path: 'src/locales/langs/zh.json', name: '中文语言文件' },
{ path: 'src/locales/langs/en.json', name: '英文语言文件' }
]
for (const { path: langPath, name } of languageFiles) {
try {
const fullPath = path.resolve(process.cwd(), langPath)
const content = await fs.readFile(fullPath, 'utf-8')
const langData = JSON.parse(content)
const menusToRemove = [
'widgets',
'template',
'article',
'examples',
'safeguard',
'plan',
'help'
]
if (langData.menus) {
menusToRemove.forEach((menuKey) => {
if (langData.menus[menuKey]) {
delete langData.menus[menuKey]
}
})
if (langData.menus.dashboard) {
if (langData.menus.dashboard.analysis) {
delete langData.menus.dashboard.analysis
}
if (langData.menus.dashboard.ecommerce) {
delete langData.menus.dashboard.ecommerce
}
}
if (langData.menus.system) {
const systemKeysToRemove = [
'nested',
'menu1',
'menu2',
'menu21',
'menu3',
'menu31',
'menu32',
'menu321'
]
systemKeysToRemove.forEach((key) => {
if (langData.menus.system[key]) {
delete langData.menus.system[key]
}
})
}
}
await fs.writeFile(fullPath, JSON.stringify(langData, null, 2), 'utf-8')
console.log(` ${icons.success} ${fmt.success(`清理${name}完成`)}`)
} catch (err) {
console.log(` ${icons.error} ${fmt.error(`清理${name}失败`)}`)
console.log(` ${fmt.dim('错误详情: ' + err)}`)
}
}
}
// 清理快速入口组件
async function cleanFastEnterComponent() {
const fastEnterPath = path.resolve(process.cwd(), 'src/config/fastEnter.ts')
try {
const cleanedFastEnter = `/**
*
*
*/
import { WEB_LINKS } from '@/utils/constants'
import type { FastEnterConfig } from '@/types/config'
const fastEnterConfig: FastEnterConfig = {
// 显示条件(屏幕宽度)
minWidth: 1200,
// 应用列表
applications: [
{
name: '工作台',
description: '系统概览与数据统计',
icon: 'ri:pie-chart-line',
iconColor: '#377dff',
enabled: true,
order: 1,
routeName: 'Console'
},
{
name: '官方文档',
description: '使用指南与开发文档',
icon: 'ri:bill-line',
iconColor: '#ffb100',
enabled: true,
order: 2,
link: WEB_LINKS.DOCS
},
{
name: '技术支持',
description: '技术支持与问题反馈',
icon: 'ri:user-location-line',
iconColor: '#ff6b6b',
enabled: true,
order: 3,
link: WEB_LINKS.COMMUNITY
},
{
name: '哔哩哔哩',
description: '技术分享与交流',
icon: 'ri:bilibili-line',
iconColor: '#FB7299',
enabled: true,
order: 4,
link: WEB_LINKS.BILIBILI
}
],
// 快速链接
quickLinks: [
{
name: '登录',
enabled: true,
order: 1,
routeName: 'Login'
},
{
name: '注册',
enabled: true,
order: 2,
routeName: 'Register'
},
{
name: '忘记密码',
enabled: true,
order: 3,
routeName: 'ForgetPassword'
},
{
name: '个人中心',
enabled: true,
order: 4,
routeName: 'UserCenter'
}
]
}
export default Object.freeze(fastEnterConfig)
`
await fs.writeFile(fastEnterPath, cleanedFastEnter, 'utf-8')
console.log(` ${icons.success} ${fmt.success('清理快速入口配置完成')}`)
} catch (err) {
console.log(` ${icons.error} ${fmt.error('清理快速入口配置失败')}`)
console.log(` ${fmt.dim('错误详情: ' + err)}`)
}
}
// 更新菜单接口
async function updateMenuApi() {
const apiPath = path.resolve(process.cwd(), 'src/api/system-manage.ts')
try {
const content = await fs.readFile(apiPath, 'utf-8')
const updatedContent = content.replace(
"url: '/api/v3/system/menus'",
"url: '/api/v3/system/menus/simple'"
)
await fs.writeFile(apiPath, updatedContent, 'utf-8')
console.log(` ${icons.success} ${fmt.success('更新菜单接口完成')}`)
} catch (err) {
console.log(` ${icons.error} ${fmt.error('更新菜单接口失败')}`)
console.log(` ${fmt.dim('错误详情: ' + err)}`)
}
}
// 用户确认函数
async function getUserConfirmation(): Promise<boolean> {
const { createInterface } = await import('readline')
return new Promise((resolve) => {
const rl = createInterface({
input: process.stdin,
output: process.stdout
})
console.log(
` ${fmt.highlight('请输入')} ${fmt.success('yes')} ${fmt.highlight('确认执行清理操作,或按 Enter 取消')}`
)
console.log()
process.stdout.write(` ${icons.arrow} `)
rl.question('', (answer: string) => {
rl.close()
resolve(answer.toLowerCase().trim() === 'yes')
})
})
}
// 显示清理警告
async function showCleanupWarning() {
createCard('安全警告', [
`${fmt.warning('此操作将永久删除以下演示内容,且无法恢复!')}`,
`${fmt.dim('请仔细阅读清理列表,确认后再继续操作')}`
])
const cleanupItems = [
{
icon: icons.image,
name: '图片资源',
desc: '演示用的封面图片、3D图片、运维图片等',
color: theme.orange
},
{
icon: icons.file,
name: '演示页面',
desc: 'widgets、template、article、examples、safeguard等页面',
color: theme.purple
},
{
icon: icons.code,
name: '路由模块文件',
desc: '删除演示路由模块只保留核心模块dashboard、system、result、exception',
color: theme.primary
},
{
icon: icons.link,
name: '路由别名',
desc: '重写routesAlias.ts移除演示路由别名',
color: theme.info
},
{
icon: icons.data,
name: 'Mock数据',
desc: '演示用的JSON数据、文章列表、评论数据等',
color: theme.success
},
{
icon: icons.globe,
name: '多语言文件',
desc: '清理中英文语言包中的演示菜单项',
color: theme.warning
},
{ icon: icons.map, name: '地图组件', desc: '移除art-map-chart地图组件', color: theme.error },
{ icon: icons.chat, name: '评论组件', desc: '移除comment-widget评论组件', color: theme.orange },
{
icon: icons.bolt,
name: '快速入口',
desc: '移除分析页、礼花效果、聊天、更新日志、定价、留言管理等无效项目',
color: theme.purple
}
]
console.log(` ${fmt.badge('', theme.bgRed)} ${fmt.title('将要清理的内容')}`)
console.log()
cleanupItems.forEach((item, index) => {
console.log(` ${item.color}${theme.reset} ${fmt.highlight(`${index + 1}. ${item.name}`)}`)
console.log(` ${fmt.dim(item.desc)}`)
})
console.log()
console.log(` ${fmt.badge('', theme.bgGreen)} ${fmt.title('保留的功能模块')}`)
console.log()
const preservedModules = [
{ name: 'Dashboard', desc: '工作台页面' },
{ name: 'System', desc: '系统管理模块' },
{ name: 'Result', desc: '结果页面' },
{ name: 'Exception', desc: '异常页面' },
{ name: 'Auth', desc: '登录注册功能' },
{ name: 'Core Components', desc: '核心组件库' }
]
preservedModules.forEach((module) => {
console.log(` ${icons.check} ${fmt.success(module.name)} ${fmt.dim(`- ${module.desc}`)}`)
})
console.log()
createDivider()
console.log()
}
// 显示统计信息
async function showStats() {
const duration = Date.now() - stats.startTime
const seconds = (duration / 1000).toFixed(2)
console.log()
createCard('清理统计', [
`${fmt.success('成功删除')}: ${fmt.highlight(stats.deletedFiles.toString())} 个文件`,
`${fmt.info('涉及路径')}: ${fmt.highlight(stats.deletedPaths.toString())} 个目录/文件`,
...(stats.failedPaths > 0
? [
`${icons.error} ${fmt.error('删除失败')}: ${fmt.highlight(stats.failedPaths.toString())} 个路径`
]
: []),
`${fmt.info('耗时')}: ${fmt.highlight(seconds)}`
])
}
// 创建成功横幅
function createSuccessBanner() {
console.log()
console.log(
fmt.gradient(' ╔══════════════════════════════════════════════════════════════════╗')
)
console.log(
fmt.gradient(' ║ ║')
)
console.log(
`${icons.star} ${fmt.success('清理完成!项目已准备就绪')} ${icons.rocket}`
)
console.log(
`${fmt.dim('现在可以开始您的开发之旅了!')}`
)
console.log(
fmt.gradient(' ║ ║')
)
console.log(
fmt.gradient(' ╚══════════════════════════════════════════════════════════════════╝')
)
console.log()
}
// 主函数
async function main() {
// 清屏并显示横幅
console.clear()
createModernBanner()
// 显示清理警告
await showCleanupWarning()
// 统计文件数量
console.log(` ${fmt.info('正在统计文件数量...')}`)
stats.totalFiles = await countAllFiles()
console.log(` ${fmt.info('即将清理')}: ${fmt.highlight(stats.totalFiles.toString())} 个文件`)
console.log(` ${fmt.dim(`涉及 ${targets.length} 个目录/文件路径`)}`)
console.log()
// 用户确认
const confirmed = await getUserConfirmation()
if (!confirmed) {
console.log(` ${fmt.warning('操作已取消,清理中止')}`)
console.log()
return
}
console.log()
console.log(` ${icons.check} ${fmt.success('确认成功,开始清理...')}`)
console.log()
// 开始清理过程
console.log(` ${fmt.badge('步骤 1/6', theme.bgBlue)} ${fmt.title('删除演示文件')}`)
console.log()
for (let i = 0; i < targets.length; i++) {
await remove(targets[i], i)
}
console.log()
console.log(` ${fmt.badge('步骤 2/6', theme.bgBlue)} ${fmt.title('清理路由模块')}`)
console.log()
await cleanRouteModules()
console.log()
console.log(` ${fmt.badge('步骤 3/6', theme.bgBlue)} ${fmt.title('重写路由别名')}`)
console.log()
await cleanRoutesAlias()
console.log()
console.log(` ${fmt.badge('步骤 4/6', theme.bgBlue)} ${fmt.title('清空变更日志')}`)
console.log()
await cleanChangeLog()
console.log()
console.log(` ${fmt.badge('步骤 5/6', theme.bgBlue)} ${fmt.title('清理语言文件')}`)
console.log()
await cleanLanguageFiles()
console.log()
console.log(` ${fmt.badge('步骤 6/7', theme.bgBlue)} ${fmt.title('清理快速入口')}`)
console.log()
await cleanFastEnterComponent()
console.log()
console.log(` ${fmt.badge('步骤 7/7', theme.bgBlue)} ${fmt.title('更新菜单接口')}`)
console.log()
await updateMenuApi()
// 显示统计信息
await showStats()
// 显示成功横幅
createSuccessBanner()
}
main().catch((err) => {
console.log()
console.log(` ${icons.error} ${fmt.error('清理脚本执行出错')}`)
console.log(` ${fmt.dim('错误详情: ' + err)}`)
console.log()
process.exit(1)
})

View File

@ -1,34 +0,0 @@
<template>
<ElConfigProvider size="default" :locale="locales[language]" :z-index="3000">
<RouterView></RouterView>
</ElConfigProvider>
</template>
<script setup lang="ts">
import { useUserStore } from './store/modules/user'
import zh from 'element-plus/es/locale/lang/zh-cn'
import en from 'element-plus/es/locale/lang/en'
import { systemUpgrade } from './utils/sys'
import { toggleTransition } from './utils/ui/animation'
import { checkStorageCompatibility } from './utils/storage'
import { initializeTheme } from './hooks/core/useTheme'
const userStore = useUserStore()
const { language } = storeToRefs(userStore)
const locales = {
zh: zh,
en: en
}
onBeforeMount(() => {
toggleTransition(true)
initializeTheme()
})
onMounted(() => {
checkStorageCompatibility()
toggleTransition(false)
systemUpgrade()
})
</script>

View File

@ -1,29 +0,0 @@
import request from '@/utils/http'
/**
*
* @param params
* @returns
*/
export function fetchLogin(params: Api.Auth.LoginParams) {
return request.post<Api.Auth.LoginResponse>({
url: '/api/auth/login',
params
// showSuccessMessage: true // 显示成功消息
// showErrorMessage: false // 不显示错误消息
})
}
/**
*
* @returns
*/
export function fetchGetUserInfo() {
return request.get<Api.Auth.UserInfo>({
url: '/api/user/info'
// 自定义请求头
// headers: {
// 'X-Custom-Header': 'your-custom-value'
// }
})
}

View File

@ -1,25 +0,0 @@
import request from '@/utils/http'
import { AppRouteRecord } from '@/types/router'
// 获取用户列表
export function fetchGetUserList(params: Api.SystemManage.UserSearchParams) {
return request.get<Api.SystemManage.UserList>({
url: '/api/user/list',
params
})
}
// 获取角色列表
export function fetchGetRoleList(params: Api.SystemManage.RoleSearchParams) {
return request.get<Api.SystemManage.RoleList>({
url: '/api/role/list',
params
})
}
// 获取菜单列表
export function fetchGetMenuList() {
return request.get<AppRouteRecord[]>({
url: '/api/v3/system/menus'
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 954 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 431 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 293 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 509 B

View File

@ -1 +0,0 @@
<svg viewBox="0 0 400 300" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="94" y="34" width="212" height="233"><path d="M306 34H94v233h212V34Z" fill="#fff"/></mask><g mask="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M234.427 155.64h38.36V69.6h-38.36v86.04ZM113.326 155.64h121.1V69.6h-121.1v86.04Z" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M130.126 155.354h104.2v-72.95h-104.2v72.95ZM236.369 71.05s0 3.3 1.65 5.05c2.33 2.52 7.38-.2 7.38-.2s-1.75 5.15-1.55 10.19c.29 8.24 6.99 9.51 10 4.75 4.56 4.85 8.94-.29 9.52-2.62 4.27 4.76 9.32-.87 9.32-.87v-6.3l-23.99-12.13-12.33 2.13Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M234.429 155.641h-121.1l-15.93 32.11h121.1l15.93-32.11Z" fill="#fff"/><path d="M234.427 69.6h38.46v86.04M113.326 146.52V69.6h121.1M234.429 155.641l-15.93 32.11h-121.1l15.93-32.11h111.39" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M226.37 159.715H116.82l-12.04 23.86H215l11.37-23.86Z" fill="#006EFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="m288.807 187.751-15.92-32.11h-38.46l16.02 32.11h38.36Z" fill="#fff"/><path d="m238.607 163.981 11.84 23.77h38.36l-15.92-32.11h-38.46" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M207.336 223.734c-3.69-13.77-15.44-23.86-29.33-23.86h-8.65s-27.09 14.94-27.09 33.27c0 18.34 25.44 33.18 25.44 33.18h10.4c13.79-.1 25.44-10.19 29.13-23.87 1.75-12.51 0-18.62.1-18.72Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M243.459 240.421c3.98 0 7.28-3.3 7.28-7.27 0-3.98-3.3-7.28-7.28-7.28h-31.08c-3.98 0-7.28 3.3-7.28 7.28 0 3.97 3.3 7.27 7.28 7.27h31.08Z" fill="#C7DEFF"/><path d="M210.342 223.737c-4.08-13.87-16.9-23.96-32.05-23.96H168.972s-29.62 14.94-29.62 33.37 27.87 33.37 27.87 33.37h11.27c15.05-.1 27.77-10.19 31.75-23.96" stroke="#071F4D"/><path d="M212.379 240.421c-3.98 0-7.28-3.3-7.28-7.27m0 0c0-3.98 3.3-7.28 7.28-7.28" stroke="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M168.781 199.777c-18.45 0-33.41 14.94-33.41 33.37s14.96 33.37 33.41 33.37c18.45 0 33.4-14.94 33.4-33.37s-14.95-33.37-33.4-33.37Z" fill="#006EFF"/><path d="M168.781 199.777c-18.45 0-33.41 14.94-33.41 33.37s14.96 33.37 33.41 33.37c18.45 0 33.4-14.94 33.4-33.37s-14.95-33.37-33.4-33.37Z" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M168.775 209.38c-13.14 0-23.79 10.64-23.79 23.77 0 13.12 10.65 23.76 23.79 23.76 13.14 0 23.8-10.64 23.8-23.76 0-13.13-10.66-23.77-23.8-23.77Z" fill="#00E4E5"/><path d="M162.174 223.736a17.48 17.48 0 0 1 14.76-8.05M159.455 231.982c.1-1.36.29-2.62.68-3.88" stroke="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M173.535 209.87c-1.55-.3-3.11-.49-4.76-.49-13.11 0-23.79 10.67-23.79 23.77 0 13.09 10.68 23.76 23.79 23.76 1.65 0 3.21-.19 4.76-.48-10.88-2.23-19.03-11.84-19.03-23.28 0-11.45 8.15-21.05 19.03-23.28Z" fill="#071F4D"/><path d="M219.957 225.774h23.6c4.08 0 7.38 3.3 7.38 7.37m0 0c0 4.08-3.3 7.37-7.38 7.37h-20.1M212.091 225.774h3.3" stroke="#071F4D"/><path d="m248.894 34.485-.19 18.24c0 4.07-.39 5.23-2.14 6.79-8.15 6.88-10.97 9.02-9.22 12.9 1.45 3.2 6.79 2.23 9.61-1.55-.39 4.56-5.24 15.32-.58 18.04 4.37 2.52 6.89-3.49 6.89-3.49s.49 3.49 4.47 3.49c3.69 0 5.24-4.75 5.24-4.75s2.14 3.49 6.22 1.35c3.11-1.55 5.44-7.08 5.44-26.67v-24.35" fill="#fff"/><path d="m248.894 34.485-.19 18.24c0 4.07-.39 5.23-2.14 6.79-8.15 6.88-10.97 9.02-9.22 12.9 1.45 3.2 6.79 2.23 9.61-1.55-.39 4.56-5.24 15.32-.58 18.04 4.37 2.52 6.89-3.49 6.89-3.49s.49 3.49 4.47 3.49c3.69 0 5.24-4.75 5.24-4.75s2.14 3.49 6.22 1.35c3.11-1.55 5.44-7.08 5.44-26.67v-24.35" stroke="#071F4D"/><path d="M255.307 75.71s-.39 5.43-2.04 9.6l2.04-9.6Z" fill="#fff"/><path d="M255.307 75.71s-.39 5.43-2.04 9.6" stroke="#071F4D"/><path d="M264.921 75.323s-.68 5.24-2.04 8.63l2.04-8.63Z" fill="#fff"/><path d="M264.921 75.323s-.68 5.24-2.04 8.63M147.801 34.485v34.92M121.775 34.485v34.92M102.546 204.724v13.97M102.546 222.379v.87M102.546 197.934v3.49M115.268 206.955v26.29M115.268 239.451v5.34M244.43 197.643v11.93M244.43 213.939v3.49M270.359 201.232v33.76M115.369 47.774h-13.6M94.486 47.774h3.4M241.516 47.774h-84.1M280.168 47.774h25.35" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m282.497 183.575-12.04-23.86h-27.29l11.36 23.86h27.97Z" fill="#00E4E5"/><path d="M234.427 134.88V69.6M234.427 140.412v7.66" stroke="#071F4D"/><path d="M220.831 228.684h16.99M240.934 228.684h2.43" stroke="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="m223.842 187.462 21.46-.2-10.97-20.66-10.49 20.86Z" fill="#071F4D"/></g></svg>

Before

Width:  |  Height:  |  Size: 4.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -1,5 +0,0 @@
<svg
viewBox="0 0 400 300"
fill="none"
xmlns="http://www.w3.org/2000/svg"
><mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="47" y="38" width="307" height="224"><path d="M353.3 38H47.5v223.8h305.8V38Z" fill="#fff"/></mask><g mask="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M299.2 200.6H61.6v5.1h240.3l-2.7-5.1Z" fill="#C7DEFF"/><path d="m308.9 185.8-6.5 20H183.7M332.3 127.6h10.6l-5 16.7-14.8-.1-7.2 21.1M328.8 127.4l13.6-39.6M307.6 166 337 84.7H180.6l-9.8 26.9h-10.5M296.6 196l4.3-11.8M157.2 149.2l6.4-17.7" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M324.8 93.1H188.5l-34.8 95.8h136.4l34.7-95.8ZM169.9 166.2l5-13.6-5 13.6Z" fill="#fff"/><path d="m169.9 166.2 5-13.6" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M324.8 93.1H188.5l-4 11.7h135.8l4.5-11.7Z" fill="#006EFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M102.6 159.5h38.3l2.7 36.6h-38.4c-10.1 0-20.9-8.2-20.9-18.3 0-10.1 8.2-18.3 18.3-18.3Z" fill="#DEEBFC"/><path fill-rule="evenodd" clip-rule="evenodd" d="M84.3 174.102c2.5 3.4 10 5 17.9 2.8 16.6-6.5 23.8-3.9 23.8-3.9s.5-3.4 1.3-5c-5.8-3-15.4.3-26.1 3.1-10.7 2.8-15.8-2.5-15.8-2.5-.4 0-1.1 2.8-1.1 5.5Z" fill="#fff"/><path d="M96.5 194.2c-7.2-3.3-12.2-10.5-12.2-19m0 0c0-11.5 9.3-20.8 20.8-20.8h29.4" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M140.3 195.1c-8.4-2.7-14.5-10.6-14.5-19.8l14.5 19.8Zm-14.5-19.8c0-11.5 9.3-20.8 20.8-20.8l-20.8 20.8Zm20.8-20.8c11.5 0 20.8 9.3 20.8 20.8l-20.8-20.8Zm20.8 20.8c0 8.4-5 15.6-12.1 18.9l12.1-18.9Z" fill="#fff"/><path d="M140.3 195.1c-8.4-2.7-14.5-10.6-14.5-19.8m0 0c0-11.5 9.3-20.8 20.8-20.8m0 0c11.5 0 20.8 9.3 20.8 20.8m0 0c0 8.4-5 15.6-12.1 18.9" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M161.5 177.2c0-7.7-6.3-14-14-14s-14 6.3-14 14c0 5.8 3.5 10.8 8.6 12.9.1 0 5.8 1.6 10.7 0 5.3-1.7 8.7-7.1 8.7-12.9Z" fill="#00E4E5"/><path d="M140.5 190.1c-5.8-2.4-9.9-8.2-9.9-14.9 0-8.9 7.2-16.1 16.1-16.1 8.9 0 16.1 7.2 16.1 16.1 0 6.8-4.2 12.5-10.1 14.9M88.4 170.604c2.9 1.3 7.7 2.6 13.6.3 14.7-5.7 22.3-4.3 24.6-3.5M84.5 174.599s5.9 6.5 19 1.7c9.2-3.4 15.3-3.9 18.8-3.8" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M340.6 112.3h-55.2l-2.7 6.2H338l2.6-6.2Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M236.8 117.9c-16.13 0-29.2 13.07-29.2 29.2s13.07 29.2 29.2 29.2 29.2-13.07 29.2-29.2-13.07-29.2-29.2-29.2Z" fill="#00E4E5"/><path d="M265 123.3c13.1 13.1 13.1 34.4 0 47.6M306 205.9h19.2M61.7 205.9h32.9M181.2 196.2h115.2M47.5 205.9h10v-9.7h73.8" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M146.7 179.2c-2.49 0-4.5 2.01-4.5 4.5s2.01 4.5 4.5 4.5 4.5-2.01 4.5-4.5-2.01-4.5-4.5-4.5Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M169.5 196.2c3.9 0 7.1 3.2 7.1 7.1 0 3.9-3.2 7.1-7.1 7.1H144c-2.1 0-3.9 1.7-3.9 3.9v1c0 2.1 1.7 3.9 3.9 3.9h48c5.1 0 9.2 4.1 9.2 9.2s-4.1 9.3-9.2 9.2h-33.8c-2.3 0-4.1 1.8-4.1 4.1s1.8 4.1 4.1 4.1h4.2c4.4 0 8 3.6 8 8s-3.6 8-8 8H111c-3.7 0-6.8-3-6.8-6.8 0-3.7 3-6.8 6.8-6.8h.3c2.3 0 4.1-1.8 4.1-4.1s-1.8-4.1-4.1-4.1H79c-4.5 0-8.1-3.6-8.1-8.1s3.6-8.1 8.1-8.1h37.7c2.1 0 3.9-1.7 3.9-3.9 0-2.1-1.7-3.9-3.9-3.9h-7.9c-4.4 0-7.9-3.5-7.9-7.9s3.5-7.9 7.9-7.9h30.4c2.2 0 3.9-1.8 3.9-3.9V187c0-1.9 1.6-3.5 3.5-3.5s3.5 1.6 3.5 3.5v5.3c0 2.2 1.8 3.9 3.9 3.9h15.5Z" fill="#006EFF"/><path d="m227.8 138.5 18.7 18.7M227.8 157.2l18.7-18.7" stroke="#fff" stroke-width="6"/><path fill-rule="evenodd" clip-rule="evenodd" d="M194.8 96.9c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8ZM202.9 96.9c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8Z" fill="#fff"/><path d="m291.7 184.3-1.6 4.6h-121M298.1 166.7l22.5-61.9" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m193 134.1 2.2-5.1h-19.4l-2.3 5.1H193ZM313.2 123.5l2.2-5.1h-24.5l-2.3 5.1h24.6Z" fill="#DEEBFC"/><path d="m164.5 159.2 19.8-54.6" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M199.6 119.8h-53.2l-4.4 9.3h53.2l4.4-9.3Z" fill="#00E4E5"/><path d="M151.3 129.1H142l4.4-9.3h16.9" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M353.3 169.4h-67.4l-4.8 12.2h67.3l4.9-12.2Z" fill="#006EFF"/><path d="M332.4 169.4h20.9l-4.9 12.2h-39.7M242.7 235.5v-4.8c0-3.8 3.1-7 7-7h20.2c3.8 0 7 3.1 7 7" stroke="#071F4D"/><path d="M261.1 235.5v-4.8c0-3.8 3.1-7 7-7h13.7c3.8 0 7 3.1 7 7v4.8M242.6 230.7h13.7M235.2 237.7h63.3M224 237.7h6.7" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M324.1 141.3H335l3.3-10.7h-10.2l-4 10.7Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M288.3 230.4c0-3.6-2.9-6.5-6.5-6.5h-14.2c-3.6 0-6.5 2.9-6.5 6.5v5.3h27.2v-5.3Z" fill="#071F4D"/><path d="M80.4 228.5H83M87.7 228.5h19.2M146.3 195.8v2c0 3.6-2.9 6.6-6.6 6.6H138M133.4 204.3h1.5M154 249.9h9.4" stroke="#DEEBFC"/><path d="m299.4 141.9 5.1-13.9" stroke="#071F4D"/></g></svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -1 +0,0 @@
<svg viewBox="0 0 400 300" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="44" y="42" width="312" height="217"><path d="M355.3 42H44v216.9h311.3V42Z" fill="#fff"/></mask><g mask="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M288.2 248.4h25.1v-30h-25.1v30Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M304.498 238.199c-1.5-3.9-5.9-15.4-4-21.6-2.9.8-3.3.1-5-.1-1.7-.1 0 10.7 2.2 16.4 1.7 4.5 2.1 11.1 2.1 13.6h5.4c.2-1.9.3-5.5-.7-8.3Z" fill="#fff"/><path d="M311.5 214.7v-1.6c0-.7-.6-1.3-1.3-1.3h-22.8c-.7 0-1.3.6-1.3 1.3v1.6" fill="#fff"/><path d="M311.5 214.7v-1.6c0-.7-.6-1.3-1.3-1.3h-22.8c-.7 0-1.3.6-1.3 1.3v1.6M290.2 214.7h21.4c1 0 1.8.8 1.8 1.8v29" stroke="#071F4D" stroke-width="1.096"/><path d="M284.3 245.6v-29c0-1 .8-1.8 1.8-1.8h1.6" fill="#fff"/><path d="M284.3 245.6v-29c0-1 .8-1.8 1.8-1.8h1.6" stroke="#071F4D" stroke-width="1.096"/><path d="M295.402 216.5c-.9 4.2-.4 9.7 2.8 17.5 2.4 5.9 1.9 10.2 1.8 12.3M300.502 216.5c-.9 4.2-.4 9.7 2.8 17.5 2.4 5.9 1.9 10.2 1.8 12.3" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m331 258.4-.3-5.2H88.5l-1.2 5.2H331Z" fill="#C7DEFF"/><path d="M252.9 248.7H331M216.6 258.4H331M47.1 139.3l-2.6 1.5 42.7 117.6h129.2v-6.6" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m247.2 248.6-40.4-111.3H50.5l40.3 111.3h156.4Z" fill="#fff"/><path d="m247.2 248.6-40.4-111.3H50.5l40.3 111.3h156.4Z" stroke="#071F4D"/><path d="m203.2 153.2 32.2 88.7H97.8l-32.3-88.7" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M72.2 146.9c-.77 0-1.4.63-1.4 1.4 0 .77.63 1.4 1.4 1.4.77 0 1.4-.63 1.4-1.4 0-.77-.63-1.4-1.4-1.4ZM79.3 146.9c-.77 0-1.4.63-1.4 1.4 0 .77.63 1.4 1.4 1.4.77 0 1.4-.63 1.4-1.4 0-.77-.63-1.4-1.4-1.4Z" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M263.5 171.2h80.3v-63.7h-80.3v63.7Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M290 143.9h-45.6l12.5 51.3H290v-51.3Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M286 117.4h-29.3v77.8h92.9v-67.6l-55.9.6-7.7-10.8Z" fill="#00E4E5"/><path d="m332.6 127.6-38.9.6-7.7-10.8h-11.7M308.9 195.2h45.9M250.3 195.2h28.5M287.3 195.2h12.3" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M130.5 211.4H186v-44h-55.5v44Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M148.7 192.5h-31.6l8.7 35.5h22.9v-35.5Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M145.9 174.2h-20.2V228h64.1v-46.7l-38.6.4-5.3-7.5Z" fill="#006EFF"/><path d="m179 181.3-27.8.4-5.3-7.5h-7.7M176.2 201.7h19.2M163.2 210.7H195M172.1 228h-54.2M184.8 228h8.1M174.9 228h5.4" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m293.2 155.7-6.4 6.3 15.3 15.3 22.7-22.6-6.4-6.4-16.3 16.3-8.9-8.9Z" fill="#fff"/><path d="M57.2 258.4h283.6M345.9 258.4h8.1M55.4 258.4h220.5M160.1 118.8l-1.2 2.7M156.7 127c-.3.8-.7 1.8-1.1 2.8M222 68.5c-1 .2-1.9.5-2.9.8M214.1 70.7c-5.8 1.9-11.3 4.4-16.5 7.4M195.4 79.5c-.9.5-1.7 1.1-2.5 1.6M314.2 98.5c-.6-.8-1.3-1.5-2-2.3M308.9 92.8c-4-4-8.3-7.6-13-10.8M293.9 80.7c-.8-.5-1.7-1.1-2.5-1.6" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M251.296 71.203c-3.6-1.5-18.5-2.9-21.8-1.9-1 5.8 4.9 13.5 4.9 13.5s6-9.9 16.9-11.6Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M251.3 42.704c-6.5 6.7-7.8 13-8.8 19.3 24.4-1.1 36.3 13 42.8 20 3.2-9.1 7.8-23 7.2-29-7.1-6.4-20-11.7-41.2-10.3Z" fill="#C7DEFF"/><path d="M230 69.3c36.2-3.8 52 21.1 52 21.1s11.4-28.2 10.5-37.4c-7.3-6.5-23.3-12-45.6-10.1-9 6.3-15.6 18.7-16.9 26.4Z" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M161.604 70.7c-6 8.4-9.9 21.9-8.8 33.8 8.4 5.3 32.3 10.5 43.6 11.5 6.1-7.9 15.9-26 15.9-26s-32-4.8-50.7-19.3Z" fill="#C7DEFF"/><path d="M193.103 119.5c4.8-2.7 19.2-29.5 19.2-29.5s-35.8-5.4-53.7-21.8c-9.3 6.1-16.4 24.3-15 40.1 10.6 6.7 45.8 13.3 49.5 11.2Z" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M189.5 111.6c-3 5.2-5.7 7.2-9.8 6.6 12.2 2.6 13.5 1.2 15.6-1.1 2.2-2.4 4.2-6.6 4.2-6.6s-3.1 2.5-10 1.1Z" fill="#071F4D"/><path d="M331 251.8v6.6M77 165.4l-2.7-6.7h7.8M222.8 228.9l2.8 6.6h-7.9" stroke="#071F4D"/></g></svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,292 +0,0 @@
// 全局样式
// 顶部进度条颜色
#nprogress .bar {
z-index: 2400;
background-color: color-mix(in srgb, var(--theme-color) 70%, white);
}
#nprogress .peg {
box-shadow:
0 0 10px var(--theme-color),
0 0 5px var(--theme-color) !important;
}
#nprogress .spinner-icon {
border-top-color: var(--theme-color) !important;
border-left-color: var(--theme-color) !important;
}
// 处理移动端组件兼容性
@media screen and (max-width: 640px) {
* {
cursor: default !important;
}
}
// 背景滤镜
*,
::before,
::after {
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
// 色弱模式
.color-weak {
filter: invert(80%);
-webkit-filter: invert(80%);
}
#noop {
display: none;
}
// 语言切换选中样式
.langDropDownStyle {
// 选中项背景颜色
.is-selected {
background-color: var(--art-el-active-color) !important;
}
// 语言切换按钮菜单样式优化
.lang-btn-item {
.el-dropdown-menu__item {
padding-left: 13px !important;
padding-right: 6px !important;
margin-bottom: 3px !important;
}
&:last-child {
.el-dropdown-menu__item {
margin-bottom: 0 !important;
}
}
.menu-txt {
min-width: 60px;
display: block;
}
i {
font-size: 10px;
margin-left: 10px;
}
}
}
// 盒子默认边框
.page-content {
border: 1px solid var(--art-card-border) !important;
}
@mixin art-card-base($border-color, $shadow: none, $radius-diff: 4px) {
background: var(--default-box-color);
border: 1px solid #{$border-color} !important;
border-radius: calc(var(--custom-radius) + #{$radius-diff}) !important;
box-shadow: #{$shadow} !important;
--el-card-border-color: var(--default-border) !important;
}
.art-card,
.art-card-sm,
.art-card-xs {
border: 1px solid var(--art-card-border);
}
// 盒子边框
[data-box-mode='border-mode'] {
.page-content,
.art-table-card {
border: 1px solid var(--art-card-border) !important;
}
.art-card {
@include art-card-base(var(--art-card-border), none, 4px);
}
.art-card-sm {
@include art-card-base(var(--art-card-border), none, 0px);
}
.art-card-xs {
@include art-card-base(var(--art-card-border), none, -4px);
}
}
// 盒子阴影
[data-box-mode='shadow-mode'] {
.page-content,
.art-table-card {
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.04) !important;
border: 1px solid var(--art-gray-200) !important;
}
.layout-sidebar {
border-right: 1px solid var(--art-card-border) !important;
}
.art-card {
@include art-card-base(
var(--art-gray-200),
(0 1px 3px 0 rgba(0, 0, 0, 0.03), 0 1px 2px -1px rgba(0, 0, 0, 0.08)),
4px
);
}
.art-card-sm {
@include art-card-base(
var(--art-gray-200),
(0 1px 3px 0 rgba(0, 0, 0, 0.03), 0 1px 2px -1px rgba(0, 0, 0, 0.08)),
2px
);
}
.art-card-xs {
@include art-card-base(
var(--art-gray-200),
(0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 1px -1px rgba(0, 0, 0, 0.08)),
-4px
);
}
}
// 元素全屏
.el-full-screen {
position: fixed;
top: 0;
left: 0;
right: 0;
width: 100vw !important;
height: 100% !important;
z-index: 2300;
margin-top: 0;
padding: 15px;
box-sizing: border-box;
background-color: var(--default-box-color);
display: flex;
flex-direction: column;
}
// 表格卡片
.art-table-card {
flex: 1;
display: flex;
flex-direction: column;
margin-top: 12px;
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
.el-card__body {
height: 100%;
overflow: hidden;
}
}
// 容器全高
.art-full-height {
height: var(--art-full-height);
display: flex;
flex-direction: column;
@media (max-width: 640px) {
height: auto;
}
}
// 徽章样式
.art-badge {
position: absolute;
top: 0;
right: 20px;
bottom: 0;
width: 6px;
height: 6px;
margin: auto;
background: #ff3860;
border-radius: 50%;
animation: breathe 1.5s ease-in-out infinite;
&.art-badge-horizontal {
right: 0;
}
&.art-badge-mixed {
right: 0;
}
&.art-badge-dual {
right: 5px;
top: 5px;
bottom: auto;
}
}
// 文字徽章样式
.art-text-badge {
position: absolute;
top: 0;
right: 12px;
bottom: 0;
min-width: 20px;
height: 18px;
line-height: 17px;
padding: 0 5px;
margin: auto;
font-size: 10px;
color: #fff;
text-align: center;
background: #fd4e4e;
border-radius: 4px;
}
@keyframes breathe {
0% {
opacity: 0.7;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.1);
}
100% {
opacity: 0.7;
transform: scale(1);
}
}
// 修复老机型 loading 定位问题
.art-loading-fix {
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100vw !important;
height: 100vh !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.art-loading-fix .el-loading-spinner {
position: static !important;
top: auto !important;
left: auto !important;
transform: none !important;
}
// 去除移动端点击背景色
@media screen and (max-width: 1180px) {
* {
-webkit-tap-highlight-color: transparent;
}
}

View File

@ -1,93 +0,0 @@
/*
* 深色主题
* 单页面移除深色主题 document.getElementsByTagName("html")[0].removeAttribute('class')
*/
$font-color: rgba(#ffffff, 0.85);
/* 覆盖element-plus默认深色背景色 */
html.dark {
// element-plus
--el-bg-color: var(--default-box-color);
--el-text-color-regular: #{$font-color};
// 富文本编辑器
// 工具栏背景颜色
--w-e-toolbar-bg-color: #18191c;
// 输入区域背景颜色
--w-e-textarea-bg-color: #090909;
// 工具栏文字颜色
--w-e-toolbar-color: var(--art-gray-600);
// 选中菜单颜色
--w-e-toolbar-active-bg-color: #25262b;
// 弹窗边框颜色
--w-e-toolbar-border-color: var(--default-border-dashed);
// 分割线颜色
--w-e-textarea-border-color: var(--default-border-dashed);
// 链接输入框边框颜色
--w-e-modal-button-border-color: var(--default-border-dashed);
// 表格头颜色
--w-e-textarea-slight-bg-color: #090909;
// 按钮背景颜色
--w-e-modal-button-bg-color: #090909;
// hover toolbar 背景颜色
--w-e-toolbar-active-color: var(--art-gray-800);
}
.dark {
.page-content .article-list .item .left .outer > div {
border-right-color: var(--dark-border-color) !important;
}
// 富文本编辑器
.editor-wrapper {
*:not(pre code *) {
color: inherit !important;
}
}
// 分隔线
.w-e-bar-divider {
background-color: var(--art-gray-300) !important;
}
.w-e-select-list,
.w-e-drop-panel,
.w-e-bar-item-group .w-e-bar-item-menus-container,
.w-e-text-container [data-slate-editor] pre > code {
border: 1px solid var(--default-border) !important;
}
// 下拉选择框
.w-e-select-list {
background-color: var(--default-box-color) !important;
}
/* 下拉选择框 hover 样式调整 */
.w-e-select-list ul li:hover,
/* 工具栏 hover 按钮背景颜色 */
.w-e-bar-item button:hover {
background-color: #090909 !important;
}
/* 代码块 */
.w-e-text-container [data-slate-editor] pre > code {
background-color: #25262b !important;
text-shadow: none !important;
}
/* 引用 */
.w-e-text-container [data-slate-editor] blockquote {
border-left: 4px solid var(--default-border-dashed) !important;
background-color: var(--art-color);
}
.editor-wrapper {
.w-e-text-container [data-slate-editor] .table-container th:last-of-type {
border-right: 1px solid var(--default-border-dashed) !important;
}
.w-e-modal {
background-color: var(--art-color);
}
}
}

View File

@ -1,2 +0,0 @@
// 导入暗黑主题
@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;

View File

@ -1,34 +0,0 @@
// https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/common/var.scss
// 自定义Element 亮色主题
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'white': #ffffff,
'black': #000000,
'success': (
'base': #13deb9
),
'warning': (
'base': #ffae1f
),
'danger': (
'base': #ff4d4f
),
'error': (
'base': #fa896b
)
),
$button: (
'hover-bg-color': var(--el-color-primary-light-9),
'hover-border-color': var(--el-color-primary),
'border-color': var(--el-color-primary),
'text-color': var(--el-color-primary)
),
$messagebox: (
'border-radius': '12px'
),
$popover: (
'padding': '14px',
'border-radius': '10px'
)
);

View File

@ -1,524 +0,0 @@
// 优化 Element Plus 组件库默认样式
:root {
// 系统主色
--main-color: var(--el-color-primary);
--el-color-white: white !important;
--el-color-black: white !important;
// 输入框边框颜色
// --el-border-color: #E4E4E7 !important; // DCDFE6
// 按钮粗度
--el-font-weight-primary: 400 !important;
--el-component-custom-height: 36px !important;
--el-component-size: var(--el-component-custom-height) !important;
// 边框按钮圆角...
--el-border-radius-base: calc(var(--custom-radius) / 3 + 2px) !important;
--el-border-radius-small: calc(var(--custom-radius) / 3 + 4px) !important;
--el-messagebox-border-radius: calc(var(--custom-radius) / 3 + 4px) !important;
--el-popover-border-radius: calc(var(--custom-radius) / 3 + 4px) !important;
.region .el-radio-button__original-radio:checked + .el-radio-button__inner {
color: var(--theme-color);
}
}
// 优化 el-form-item 标签高度
.el-form-item__label {
height: var(--el-component-custom-height) !important;
line-height: var(--el-component-custom-height) !important;
}
// 日期选择器
.el-date-range-picker {
--el-datepicker-inrange-bg-color: var(--art-gray-200) !important;
}
// el-card 背景色跟系统背景色保持一致
html.dark .el-card {
--el-card-bg-color: var(--default-box-color) !important;
}
// 修改 el-pagination 大小
.el-pagination--default {
& {
--el-pagination-button-width: 32px !important;
--el-pagination-button-height: var(--el-pagination-button-width) !important;
}
@media (max-width: 1180px) {
& {
--el-pagination-button-width: 28px !important;
}
}
.el-select--default .el-select__wrapper {
min-height: var(--el-pagination-button-width) !important;
}
.el-pagination__jump .el-input {
height: var(--el-pagination-button-width) !important;
}
}
.el-pager li {
padding: 0 10px !important;
// border: 1px solid red !important;
}
// 优化菜单折叠展开动画提升动画流畅度
.el-menu.el-menu--inline {
transition: max-height 0.26s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
// 优化菜单 item hover 动画提升鼠标跟手感
.el-sub-menu__title,
.el-menu-item {
transition: background-color 0s !important;
}
// -------------------------------- 修改 el-size=default 组件默认高度 start --------------------------------
// 修改 el-button 高度
.el-button--default {
height: var(--el-component-custom-height) !important;
}
// circle 按钮宽度优化
.el-button--default.is-circle {
width: var(--el-component-custom-height) !important;
}
// 修改 el-select 高度
.el-select--default {
.el-select__wrapper {
min-height: var(--el-component-custom-height) !important;
}
}
// 修改 el-checkbox-button 高度
.el-checkbox-button--default .el-checkbox-button__inner,
// 修改 el-radio-button 高度
.el-radio-button--default .el-radio-button__inner {
padding: 10px 15px !important;
}
// -------------------------------- 修改 el-size=default 组件默认高度 end --------------------------------
.el-pagination.is-background .btn-next,
.el-pagination.is-background .btn-prev,
.el-pagination.is-background .el-pager li {
border-radius: 6px;
}
.el-popover {
min-width: 80px;
border-radius: var(--el-border-radius-small) !important;
}
.el-dialog {
border-radius: 100px !important;
border-radius: calc(var(--custom-radius) / 1.2 + 2px) !important;
overflow: hidden;
}
.el-dialog__header {
.el-dialog__title {
font-size: 16px;
}
}
.el-dialog__body {
padding: 25px 0 !important;
position: relative; // 为了兼容 el-pagination 样式需要设置 relative不然会影响 el-pagination 的样式比如 el-pagination__jump--small 会被影响导致 el-pagination__jump--small 按钮无法点击详见 URL_ADDRESS.com/element-plus/element-plus/issues/5684#issuecomment-1176299275;
}
.el-dialog.el-dialog-border {
.el-dialog__body {
// 上边框
&::before,
// 下边框
&::after {
content: '';
position: absolute;
left: -16px;
width: calc(100% + 32px);
height: 1px;
background-color: var(--art-gray-300);
}
&::before {
top: 0;
}
&::after {
bottom: 0;
}
}
}
// el-message 样式优化
.el-message {
background-color: var(--default-box-color) !important;
border: 0 !important;
box-shadow:
0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05) !important;
p {
font-size: 13px;
}
}
// 修改 el-dropdown 样式
.el-dropdown-menu {
padding: 6px !important;
border-radius: 10px !important;
border: none !important;
.el-dropdown-menu__item {
padding: 6px 16px !important;
border-radius: 6px !important;
&:hover:not(.is-disabled) {
color: var(--art-gray-900) !important;
background-color: var(--art-el-active-color) !important;
}
&:focus:not(.is-disabled) {
color: var(--art-gray-900) !important;
background-color: var(--art-gray-200) !important;
}
}
}
// 隐藏 selectdropdown 的三角
.el-select__popper,
.el-dropdown__popper {
margin-top: -6px !important;
.el-popper__arrow {
display: none;
}
}
.el-dropdown-selfdefine:focus {
outline: none !important;
}
// 处理移动端组件兼容性
@media screen and (max-width: 640px) {
.el-message-box,
.el-message,
.el-dialog {
width: calc(100% - 24px) !important;
}
.el-date-picker.has-sidebar.has-time {
width: calc(100% - 24px);
left: 12px !important;
}
.el-picker-panel *[slot='sidebar'],
.el-picker-panel__sidebar {
display: none;
}
.el-picker-panel *[slot='sidebar'] + .el-picker-panel__body,
.el-picker-panel__sidebar + .el-picker-panel__body {
margin-left: 0;
}
}
// 修改el-button样式
.el-button {
&.el-button--text {
background-color: transparent !important;
padding: 0 !important;
span {
margin-left: 0 !important;
}
}
}
// 修改el-tag样式
.el-tag {
font-weight: 500;
transition: all 0s !important;
&.el-tag--default {
height: 26px !important;
}
}
.el-checkbox-group {
&.el-table-filter__checkbox-group label.el-checkbox {
height: 17px !important;
.el-checkbox__label {
font-weight: 400 !important;
}
}
}
.el-radio--default {
// 优化单选按钮大小
.el-radio__input {
.el-radio__inner {
width: 16px;
height: 16px;
&::after {
width: 6px;
height: 6px;
}
}
}
}
.el-checkbox {
.el-checkbox__inner {
border-radius: 2px !important;
}
}
// 优化复选框样式
.el-checkbox--default {
.el-checkbox__inner {
width: 16px !important;
height: 16px !important;
border-radius: 4px !important;
&::before {
content: '';
height: 4px !important;
top: 5px !important;
background-color: #fff !important;
transform: scale(0.6) !important;
}
}
.is-checked {
.el-checkbox__inner {
&::after {
width: 3px;
height: 8px;
margin: auto;
border: 2px solid var(--el-checkbox-checked-icon-color);
border-left: 0;
border-top: 0;
transform: translate(-45%, -60%) rotate(45deg) scale(0.86) !important;
transform-origin: center;
}
}
}
}
.el-notification .el-notification__icon {
font-size: 22px !important;
}
// 修改 el-message-box 样式
.el-message-box__headerbtn .el-message-box__close,
.el-dialog__headerbtn .el-dialog__close {
top: 7px;
right: 7px;
width: 30px;
height: 30px;
border-radius: 5px;
transition: all 0.3s;
&:hover {
background-color: var(--art-hover-color) !important;
color: var(--art-gray-900) !important;
}
}
.el-message-box {
padding: 25px 20px !important;
}
.el-message-box__title {
font-weight: 500 !important;
}
.el-table__column-filter-trigger i {
color: var(--theme-color) !important;
margin: -3px 0 0 2px;
}
// 去除 el-dropdown 鼠标放上去出现的边框
.el-tooltip__trigger:focus-visible {
outline: unset;
}
// ipad 表单右侧按钮优化
@media screen and (max-width: 1180px) {
.el-table-fixed-column--right {
padding-right: 0 !important;
.el-button {
margin: 5px 10px 5px 0 !important;
}
}
}
.login-out-dialog {
padding: 30px 20px !important;
border-radius: 10px !important;
}
// 修改 dialog 动画
.dialog-fade-enter-active {
.el-dialog:not(.is-draggable) {
animation: dialog-open 0.3s cubic-bezier(0.32, 0.14, 0.15, 0.86);
// 修复 el-dialog 动画后宽度不自适应问题
.el-select__selected-item {
display: inline-block;
}
}
}
.dialog-fade-leave-active {
animation: fade-out 0.2s linear;
.el-dialog:not(.is-draggable) {
animation: dialog-close 0.5s;
}
}
@keyframes dialog-open {
0% {
opacity: 0;
transform: scale(0.2);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes dialog-close {
0% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.2);
}
}
// 遮罩层动画
@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
// 修改 el-select 样式
.el-select__popper:not(.el-tree-select__popper) {
.el-select-dropdown__list {
padding: 5px !important;
.el-select-dropdown__item {
height: 34px !important;
line-height: 34px !important;
border-radius: 6px !important;
&.is-selected {
color: var(--art-gray-900) !important;
font-weight: 400 !important;
background-color: var(--art-el-active-color) !important;
margin-bottom: 4px !important;
}
&:hover {
background-color: var(--art-hover-color) !important;
}
}
.el-select-dropdown__item:hover ~ .is-selected,
.el-select-dropdown__item.is-selected:has(~ .el-select-dropdown__item:hover) {
background-color: transparent !important;
}
}
}
// 修改 el-tree-select 样式
.el-tree-select__popper {
.el-select-dropdown__list {
padding: 5px !important;
.el-tree-node {
.el-tree-node__content {
height: 36px !important;
border-radius: 6px !important;
&:hover {
background-color: var(--art-gray-200) !important;
}
}
}
}
}
// 实现水波纹在文字下面效果
.el-button > span {
position: relative;
z-index: 10;
}
// 优化颜色选择器圆角
.el-color-picker__color {
border-radius: 2px !important;
}
// 优化日期时间选择器底部圆角
.el-picker-panel {
.el-picker-panel__footer {
border-radius: 0 0 var(--el-border-radius-base) var(--el-border-radius-base);
}
}
// 优化树型菜单样式
.el-tree-node__content {
border-radius: 4px;
margin-bottom: 4px;
padding: 1px 0;
&:hover {
background-color: var(--art-hover-color) !important;
}
}
.dark {
.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
background-color: var(--art-gray-300) !important;
}
}
// 隐藏折叠菜单弹窗 hover 出现的边框
.menu-left-popper:focus-within,
.horizontal-menu-popper:focus-within {
box-shadow: none !important;
outline: none !important;
}
// 数字输入组件右侧按钮高度跟随自定义组件高度
.el-input-number--default.is-controls-right {
.el-input-number__decrease,
.el-input-number__increase {
height: calc((var(--el-component-size) / 2)) !important;
}
}

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