chore: 整理项目资源和配置

This commit is contained in:
Ray.Hao
2026-06-12 21:21:49 +08:00
parent 830651c0d7
commit 288ea9b43d
30 changed files with 128 additions and 199 deletions

View File

@@ -1,5 +1,6 @@
MIT License
Copyright (c) 2017-present PanJiaChen
Copyright (c) 2021-present 有来开源组织
Permission is hereby granted, free of charge, to any person obtaining a copy

View File

@@ -226,17 +226,17 @@ Thanks to all the contributors!
<table align="center">
<tr>
<td align="center">
<img src="./src/assets/images/qr/wechat-offical.png" height="180" alt="WeChat Official Account"><br>
<img src="./public/images/qr/wechat-official.png" height="180" alt="WeChat Official Account"><br>
<sub>WeChat Official Account</sub>
</td>
<td>&nbsp;&nbsp;&nbsp;&nbsp;</td>
<td align="center">
<img src="./src/assets/images/qr/wechat-mp.jpg" height="180" alt="Mini Program"><br>
<img src="./public/images/qr/wechat-mp.jpg" height="180" alt="Mini Program"><br>
<sub>Mini Program</sub>
</td>
<td>&nbsp;&nbsp;&nbsp;&nbsp;</td>
<td align="center">
<img src="./src/assets/images/qr/wechat-personal.png" height="180" alt="Add Author WeChat"><br>
<img src="./public/images/qr/wechat-personal.png" height="180" alt="Add Author WeChat"><br>
<sub>Add Author WeChat</sub>
</td>
</tr>

View File

@@ -223,17 +223,17 @@ Thanks to all the contributors!
<table align="center">
<tr>
<td align="center">
<img src="./src/assets/images/qr/wechat-official.png" height="180" alt="公众号「有来技术」"><br>
<img src="./public/images/qr/wechat-official.png" height="180" alt="公众号「有来技术」"><br>
<sub>公众号「有来技术」</sub>
</td>
<td>&nbsp;&nbsp;&nbsp;&nbsp;</td>
<td align="center">
<img src="./src/assets/images/qr/wechat-mp.jpg" height="180" alt="小程序「有来技术」"><br>
<img src="./public/images/qr/wechat-mp.jpg" height="180" alt="小程序「有来技术」"><br>
<sub>小程序「有来技术」</sub>
</td>
<td>&nbsp;&nbsp;&nbsp;&nbsp;</td>
<td align="center">
<img src="./src/assets/images/qr/wechat-personal.png" height="180" alt="添加作者微信"><br>
<img src="./public/images/qr/wechat-personal.png" height="180" alt="添加作者微信"><br>
<sub>添加作者微信</sub>
</td>
</tr>

33
deploy.mjs Normal file
View File

@@ -0,0 +1,33 @@
/**
* 部署管理后台到服务器 | 用法pnpm run deploy
*
* 前置SSH 免密登录(只需一次)
* 1. 如果 %USERPROFILE%\.ssh\id_rsa 已存在跳过,否则 ssh-keygen -t rsa 回车到底
*
* 2. 拷贝公钥到服务器(会提示输一次服务器密码):
* Windows: type %USERPROFILE%\.ssh\id_rsa.pub | ssh root@server "cat >> ~/.ssh/authorized_keys"
* Mac: ssh-copy-id root@server
*
* 3. ssh root@server 输完不弹密码直接连上即成功
*/
import { execSync } from "node:child_process";
// ==================== 改为你的服务器信息 ====================
const HOST = "127.0.0.1"; // 服务器 IP改为你的
const USER = "root"; // SSH 用户名,改为你的
const PORT = 22; // SSH 端口,改为你的
const TARGET = "/data/www/vue3-element-admin"; // 部署目录,改为你的
// =========================================================
console.log("构建中...");
execSync("pnpm build-only", { stdio: "inherit" });
console.log(`部署 → ${USER}@${HOST}:${TARGET}`);
execSync(
`scp -o StrictHostKeyChecking=no -P ${PORT} -r "dist/." "${USER}@${HOST}:${TARGET}/"`,
{ stdio: "inherit", shell: true }
);
console.log("部署完成");

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2017-present PanJiaChen
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,21 +0,0 @@
MIT License
Copyright (c) 2021-present 有来开源组织
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

@@ -17,7 +17,8 @@
"lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:stylelint",
"preinstall": "npx only-allow pnpm",
"prepare": "husky",
"commit": "git-cz"
"commit": "git-cz",
"deploy": "node deploy.mjs"
},
"config": {
"commitizen": {

View File

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

Before

Width:  |  Height:  |  Size: 487 KiB

After

Width:  |  Height:  |  Size: 487 KiB

View File

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View File

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View File

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

View File

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 MiB

After

Width:  |  Height:  |  Size: 4.1 MiB

View File

Before

Width:  |  Height:  |  Size: 306 KiB

After

Width:  |  Height:  |  Size: 306 KiB

View File

@@ -49,10 +49,8 @@
</span>
<!-- 内容全屏 -->
<span class="tags-actions__btn" @click="appStore.toggleContentFullscreen()">
<el-icon :size="16">
<FullScreen v-if="!appStore.contentFullscreen" />
<Close v-else />
</el-icon>
<div v-if="!appStore.contentFullscreen" class="i-svg:fullscreen icon-16" />
<div v-else class="i-svg:fullscreen-exit icon-16" />
</span>
<!-- 下拉菜单 -->
<el-dropdown trigger="click" @command="handleActionCommand">
@@ -62,39 +60,27 @@
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="refresh">
<el-icon :size="14" class="tags-actions__dropdown-icon">
<Refresh />
</el-icon>
<el-icon :size="14" class="icon-14"><Refresh /></el-icon>
<span>刷新当前</span>
</el-dropdown-item>
<el-dropdown-item v-if="currentTag && !currentTag.affix" command="closeCurrent">
<el-icon :size="14" class="tags-actions__dropdown-icon">
<Close />
</el-icon>
<div class="i-svg:close icon-14" />
<span>关闭当前</span>
</el-dropdown-item>
<el-dropdown-item divided command="closeOthers">
<el-icon :size="14" class="tags-actions__dropdown-icon">
<Remove />
</el-icon>
<el-dropdown-item divided command="closeOtherTags">
<div class="i-svg:close_other icon-14" />
<span>关闭其它</span>
</el-dropdown-item>
<el-dropdown-item command="closeLeft">
<el-icon :size="14" class="tags-actions__dropdown-icon">
<DArrowLeft />
</el-icon>
<el-dropdown-item command="closeLeftTags">
<div class="i-svg:close_left icon-14" />
<span>关闭左侧</span>
</el-dropdown-item>
<el-dropdown-item command="closeRight">
<el-icon :size="14" class="tags-actions__dropdown-icon">
<DArrowRight />
</el-icon>
<el-dropdown-item command="closeRightTags">
<div class="i-svg:close_right icon-14" />
<span>关闭右侧</span>
</el-dropdown-item>
<el-dropdown-item divided command="closeAll">
<el-icon :size="14" class="tags-actions__dropdown-icon">
<CircleClose />
</el-icon>
<el-dropdown-item divided command="closeAllTags">
<div class="i-svg:close_all icon-14" />
<span>关闭所有</span>
</el-dropdown-item>
</el-dropdown-menu>
@@ -114,25 +100,25 @@
class="contextmenu__danger"
@click="closeSelectedTag(selectedTag)"
>
<el-icon :size="16" class="contextmenu__icon"><Close /></el-icon>
<div class="i-svg:close contextmenu__icon" />
<span>关闭</span>
</li>
<li class="contextmenu__divider" />
<li @click="closeOtherTags">
<el-icon :size="16" class="contextmenu__icon"><Remove /></el-icon>
<div class="i-svg:close_other contextmenu__icon" />
<span>关闭其它</span>
</li>
<li v-if="!isFirstView" @click="closeLeftTags">
<el-icon :size="16" class="contextmenu__icon"><DArrowLeft /></el-icon>
<div class="i-svg:close_left contextmenu__icon" />
<span>关闭左侧</span>
</li>
<li v-if="!isLastView" @click="closeRightTags">
<el-icon :size="16" class="contextmenu__icon"><DArrowRight /></el-icon>
<div class="i-svg:close_right contextmenu__icon" />
<span>关闭右侧</span>
</li>
<li class="contextmenu__divider" />
<li @click="closeAllTags(selectedTag)">
<el-icon :size="16" class="contextmenu__icon"><CircleClose /></el-icon>
<div class="i-svg:close_all contextmenu__icon" />
<span>关闭所有</span>
</li>
</ul>
@@ -212,16 +198,16 @@ const handleActionCommand = (command: string) => {
case "closeCurrent":
closeSelectedTag(currentTag.value);
break;
case "closeOthers":
case "closeOtherTags":
closeOtherTagsForActive();
break;
case "closeLeft":
case "closeLeftTags":
closeLeftTagsForActive();
break;
case "closeRight":
case "closeRightTags":
closeRightTagsForActive();
break;
case "closeAll":
case "closeAllTags":
closeAllTags(currentTag.value);
break;
}
@@ -379,9 +365,7 @@ const closeSelectedTag = (tag: TagView | null) => {
});
};
/* ------------------------------------------------------------------ */
/* 批量关闭标签(左/右/其它方向) */
/* ------------------------------------------------------------------ */
// 批量关闭标签(左/右/其它方向)
/**
* 按方向批量关闭标签的通用执行器。
@@ -407,7 +391,9 @@ function closeDirectional(
});
}
/** 关闭当前激活标签之外的其它标签 */
/**
* 关闭当前激活标签之外的其它标签。
*/
const closeOtherTagsForActive = () => {
if (!currentTag.value) return;
tagsViewStore.delOtherViews(currentTag.value).then(() => {
@@ -646,19 +632,33 @@ useContextMenuManager();
background-color: var(--el-fill-color-light);
}
:deep(.el-icon) {
:deep(.el-icon),
:deep(div) {
opacity: 0.45;
}
&:hover :deep(.el-icon) {
&:hover :deep(.el-icon),
&:hover :deep(div) {
opacity: 0.7;
}
}
}
&__dropdown-icon {
flex-shrink: 0;
margin-right: 6px;
opacity: 0.55;
.icon-16 {
width: 16px;
height: 16px;
}
.icon-14 {
flex-shrink: 0;
width: 14px;
height: 14px;
margin-right: 6px;
opacity: 0.55;
&.el-icon {
width: 14px;
height: 14px;
}
}
@@ -697,6 +697,8 @@ useContextMenuManager();
&__icon {
flex-shrink: 0;
width: 16px;
height: 16px;
opacity: 0.55;
}

View File

@@ -1,13 +1,5 @@
/**
* 混合布局菜单逻辑
*
* 从 MixLayout.vue 提取,负责:
* - 顶部菜单项扁平化(单子菜单向上合并 title/icon
* - 侧边菜单路径解析与激活判定
* - 菜单切换导航(自动跳转到第一个可访问子菜单)
* - 路由变化时同步顶部菜单激活态与侧边菜单数据
*
* 模板只绑定返回值,无需关心内部实现。
*/
import type { LocationQueryRaw, RouteRecordRaw } from "vue-router";
import { useLayout } from "../useLayout";
@@ -15,10 +7,6 @@ import { useAppStore, usePermissionStore, useSettingsStore } from "@/stores";
import { isExternal } from "@/utils/index";
import { ElIcon } from "element-plus";
/* ------------------------------------------------------------------ */
/* MenuIcon — 菜单图标渲染组件Element Plus / SVG 双模式) */
/* ------------------------------------------------------------------ */
const MenuIcon = defineComponent({
props: { icon: String },
setup(props) {
@@ -39,9 +27,6 @@ const MenuIcon = defineComponent({
},
});
/* ------------------------------------------------------------------ */
/* useMixMenu */
/* ------------------------------------------------------------------ */
export function useMixMenu() {
const route = useRoute();
@@ -53,7 +38,6 @@ export function useMixMenu() {
const { activeTopMenuPath, sideMenuRoutes } = useLayout();
/* ------ 顶部菜单 ------------------------------------------------- */
/**
* 顶部菜单项。
@@ -84,9 +68,12 @@ export function useMixMenu() {
});
});
/* ------ 侧边菜单 ------------------------------------------------- */
/** 当前路由对应的侧边菜单激活路径(优先取 meta.activeMenu */
/**
* 当前路由对应的侧边菜单激活路径。
*
* 优先取 meta.activeMenu否则使用当前路由路径。
*/
const activeSideMenuPath = computed(() => {
const { meta, path } = route;
return typeof meta?.activeMenu === "string" ? meta.activeMenu : path;
@@ -105,8 +92,6 @@ export function useMixMenu() {
return `${activeTopMenuPath.value}/${routePath}`;
}
/* ------ 路径工具 ------------------------------------------------- */
/**
* 从完整路径中提取第一段作为顶级菜单路径。
*
@@ -114,14 +99,15 @@ export function useMixMenu() {
* "/dashboard" → "/"
*/
function extractTopMenuPath(path: string): string {
return path.split("/").filter(Boolean).length > 1
? path.match(/^\/[^/]+/)?.[0] || "/"
: "/";
return path.split("/").filter(Boolean).length > 1 ? path.match(/^\/[^/]+/)?.[0] || "/" : "/";
}
/* ------ 菜单切换 ------------------------------------------------- */
/** 顶部菜单点击:切换侧边菜单数据,并导航到第一个可访问叶子菜单 */
/**
* 顶部菜单点击。
*
* 切换侧边菜单数据,并导航到第一个可访问叶子菜单。
*/
function handleTopMenuSelect(menuPath: string) {
if (menuPath === activeTopMenuPath.value) return;
@@ -153,7 +139,7 @@ export function useMixMenu() {
}
}
/* ------ 路由同步 ------------------------------------------------- */
// 路由同步
/**
* 监听路由变化,保持顶部菜单激活态和侧边菜单数据同步。

View File

@@ -58,16 +58,30 @@ export function useLayout() {
// 菜单数据
// ============================================
/** 路由列表(左侧/顶部菜单) */
/**
* 路由列表。
*
* 左侧/顶部菜单共用,由权限 store 生成。
*/
const routes = computed(() => permissionStore.routes);
/** 混合布局侧边菜单 */
/**
* 混合布局侧边菜单。
*
* 根据当前激活的顶部菜单路径动态计算。
*/
const sideMenuRoutes = computed(() => permissionStore.mixLayoutSideMenus);
/** 顶部菜单激活路径 */
/**
* 顶部菜单激活路径。
*/
const activeTopMenuPath = computed(() => appStore.activeTopMenuPath);
/** 当前激活菜单 */
/**
* 当前激活菜单。
*
* 优先取路由 meta.activeMenu否则取当前路径。
*/
const activeMenu = computed(() => {
const { meta, path } = route;
return meta?.activeMenu || path;

View File

@@ -1,41 +0,0 @@
export interface PageQueryParams {
pageNum: number;
pageSize: number;
[key: string]: any;
}
export interface OptionItem {
label: string;
value: string | number;
meta?: any;
children?: OptionItem[];
}
export interface ExcelResult {
/** 状态码 */
code: number;
/** 无效数据条数 */
invalidCount: number;
/** 有效数据条数 */
validCount: number;
/** 错误信息 */
messageList: Array<string>;
}
export interface PageResult<T> {
total: number;
list: T[];
params: PageQueryParams;
}
export interface ApiResponse<T = any> {
code: number | string;
msg: string;
data: T;
}
export interface BaseEntity {
id: number;
createTime: string;
updateTime: string;
}

View File

@@ -4,11 +4,6 @@
* @deprecated 请使用 @/types 下的具名导出
*/
declare global {
type ApiResponse<T = unknown> = import("@/types/api").ApiResponse<T>;
type BaseQueryParams = import("@/types/api").BaseQueryParams;
type PageResult<T> = import("@/types/api").PageResult<T>;
type OptionItem = import("@/types/api").OptionItem;
type ExcelResult = import("@/types/api").ExcelResult;
type TagView = import("@/types/ui").TagView;
type AppSettings = import("@/types/ui").AppSettings;
}

View File

@@ -44,7 +44,7 @@ function extractFileName(contentDisposition: string): string {
* downloadFile(response, "用户列表.xlsx");
* ```
*/
export function downloadFile(response: any, customFileName?: string): void {
export function downloadFile(response: { data: any; headers: Record<string, string> }, customFileName?: string): void {
try {
const fileData = response.data;
const contentDisposition = response.headers["content-disposition"];

View File

@@ -579,11 +579,8 @@ $radius: 10px;
&__end {
display: flex;
gap: 16px;
gap: 20px;
align-items: center;
padding: 10px 14px;
background: var(--el-color-primary-light-9);
border-radius: 8px;
}
}

View File

@@ -31,7 +31,7 @@
</el-button>
<el-button
v-hasPerm="['sys:config:refresh']"
color="#626aef"
type="primary"
icon="RefreshLeft"
@click="refreshCache"
>

View File

@@ -328,17 +328,10 @@ function handleQuery(): void {
}
/**
* 重置查询条件
*/
function resetQuery(): void {
queryFormRef.value?.resetFields();
}
/**
* 重置查询条件并重新查询
* 重置搜索条件并重新查询。
*/
function handleResetQuery(): void {
resetQuery();
queryFormRef.value?.resetFields();
handleQuery();
}

View File

@@ -78,7 +78,7 @@
导入
</el-button>
<el-button v-hasPerm="'sys:user:export'" icon="download" @click="exportUsers">
<el-button v-hasPerm="'sys:user:export'" icon="download" @click="handleExport">
导出
</el-button>
</div>
@@ -347,19 +347,12 @@ function handleQuery(): void {
}
/**
* 重置查询条件
* 重置搜索条件并重新查询。
*/
function resetQuery(): void {
function handleResetQuery(): void {
queryFormRef.value?.resetFields();
tableData.params.deptId = undefined;
tableData.params.createTime = undefined;
}
/**
* 重置查询条件并重新查询
*/
function handleResetQuery(): void {
resetQuery();
handleQuery();
}
@@ -509,10 +502,7 @@ function handleDelete(id?: string): void {
);
}
/**
* 导出用户列表
*/
async function exportUsers(): Promise<void> {
async function handleExport(): Promise<void> {
const response = await UserAPI.export(tableData.params);
downloadFile(response);
ElMessage.success("导出成功");