refactor: 界面样式重构优化

This commit is contained in:
Ray.Hao
2026-03-13 00:04:42 +08:00
parent 5348ddad36
commit 306d167a55
52 changed files with 3391 additions and 1459 deletions

View File

@@ -1,39 +1,404 @@
<template>
<view class="page">
<wd-cell-group border>
<wd-cell title="菜单管理" is-link to="/pages/work/menu/index" />
</wd-cell-group>
<view class="placeholder">
<text class="placeholder__text">菜单管理功能开发中...</text>
<view class="page page--padding page--pt">
<view>
<wd-search
v-model="queryParams.keywords"
placeholder="搜索菜单名称"
hide-cancel
@search="handleSearch"
/>
</view>
<!-- 菜单树形列表 -->
<view class="mt-16rpx">
<custom-tree
:data="treeData"
:default-expand-all="true"
:show-action="true"
@action="handleNodeAction"
>
<!-- 自定义节点内容ID + 名称 + 状态 -->
<template #content="{ node }">
<view class="flex-1 flex flex-nowrap items-center gap-16rpx">
<text class="w-120rpx text-24rpx color-text-secondary">{{ node.id }}</text>
<wd-icon v-if="node.icon" :name="node.icon" size="16" class="color-primary" />
<text class="flex-1 truncate">{{ node.name }}</text>
<wd-tag class="flex-shrink-0" :type="getMenuTypeTag(node.type)" size="small">
{{ getMenuTypeText(node.type) }}
</wd-tag>
<wd-tag
class="flex-shrink-0"
:type="node.visible === 1 ? 'success' : 'primary'"
size="small"
>
{{ node.visible === 1 ? "显示" : "隐藏" }}
</wd-tag>
</view>
</template>
</custom-tree>
<wd-status-tip v-if="menuList.length === 0" image="search" tip="暂无数据" />
</view>
<!-- 弹窗表单 -->
<wd-popup
v-model="dialog.visible"
position="bottom"
custom-style="border-radius: 24rpx 24rpx 0 0; max-height: 80vh"
@close="closeMenuDialog"
>
<view class="p-4">
<view class="text-center font-bold text-32rpx mb-4">
{{ formData.id ? "编辑菜单" : "新增菜单" }}
</view>
<scroll-view scroll-y class="max-h-60vh">
<wd-form ref="formRef" :model="formData" :rules="rules">
<wd-cell-group border>
<wd-col-picker
v-model="parentSelected"
label="上级菜单"
:columns="parentColumns"
:column-change="handleParentColumnChange"
required
:display-format="displayParentFormat"
@confirm="handleParentConfirm"
/>
<wd-input v-model="formData.name" label="菜单名称" required />
<wd-cell title="菜单类型" required>
<wd-radio-group v-model="formData.type" size="smalll" shape="button" cell>
<wd-radio value="C">目录</wd-radio>
<wd-radio value="M">菜单</wd-radio>
<wd-radio value="B">按钮</wd-radio>
</wd-radio-group>
</wd-cell>
<wd-input
v-if="formData.type !== 'B'"
v-model="formData.routePath"
label="路由路径"
placeholder="system 或 /system"
/>
<wd-input
v-if="formData.type === 'M'"
v-model="formData.component"
label="组件路径"
placeholder="system/menu/index"
/>
<wd-input
v-if="formData.type === 'B'"
v-model="formData.perm"
label="权限标识"
placeholder="sys:menu:create"
/>
<wd-input v-model="formData.icon" label="图标" />
<wd-cell title="排序">
<wd-input-number v-model="formData.sort!" :min="0" />
</wd-cell>
<wd-cell title="状态">
<wd-switch v-model="formData.visible" :active-value="1" :inactive-value="0" />
</wd-cell>
</wd-cell-group>
</wd-form>
</scroll-view>
<view class="popup-actions">
<wd-button type="info" plain @click="closeMenuDialog">取消</wd-button>
<wd-button type="primary" :loading="submitting" @click="submitMenuForm">保存</wd-button>
</view>
</view>
</wd-popup>
<!-- 浮动新增按钮 -->
<view
v-if="hasPermission('sys:menu:create') && !dialog.visible"
class="fab-add"
hover-class="fab-add:active"
@click.stop="openMenuDialog()"
>
<wd-icon name="add" size="44rpx" />
</view>
</view>
</template>
<script setup lang="ts">
<script lang="ts" setup>
import { onLoad } from "@dcloudio/uni-app";
import { FormRules } from "wot-design-uni/components/wd-form/types";
import { useToast } from "wot-design-uni";
import MenuAPI, { type MenuQuery, MenuVO, MenuForm } from "@/api/menu";
import { hasPermission } from "@/utils/permission";
import CustomTree from "@/components/custom-tree/index.vue";
const toast = useToast();
const formRef = ref();
const submitting = ref(false);
const queryParams = reactive<MenuQuery>({ keywords: "" });
const menuList = ref<MenuVO[]>([]);
const dialog = reactive({ visible: false });
const initialFormData: MenuForm = {
id: undefined,
parentId: "0",
name: undefined,
type: "M",
routePath: undefined,
component: undefined,
perm: undefined,
icon: undefined,
sort: 1,
visible: 1,
};
const formData = reactive<MenuForm>({ ...initialFormData });
// 转换为树组件数据格式
const treeData = computed(() => menuList.value.map((menu) => transformMenuToTree(menu)));
function transformMenuToTree(menu: MenuVO): any {
return {
value: menu.id,
label: menu.name,
id: menu.id,
name: menu.name,
type: menu.type,
icon: menu.icon,
visible: menu.visible,
perm: menu.perm,
children: menu.children?.map((child) => transformMenuToTree(child)) || [],
};
}
// 菜单类型标签
function normalizeMenuType(type?: string | number) {
if (type === 1 || type === "1") return "C";
if (type === 2 || type === "2") return "M";
if (type === 3 || type === "3") return "B";
return type;
}
function getMenuTypeTag(type?: string | number): "primary" | "success" | "warning" | "danger" {
const map: Record<string, "primary" | "success" | "warning" | "danger"> = {
C: "warning",
M: "success",
B: "danger",
};
const key = String(normalizeMenuType(type) || "M");
return map[key] || "primary";
}
function getMenuTypeText(type?: string | number) {
const map: Record<string, string> = { C: "目录", M: "菜单", B: "按钮" };
const key = String(normalizeMenuType(type) || "M");
return map[key] || "菜单";
}
// 上级菜单选择器
const parentSelected = ref<(string | number)[]>([]);
const parentColumns = ref<OptionType[][]>([]);
const parentOptions = ref<OptionType[]>([]);
const displayParentFormat = (selectedItems: Record<string, any>[]) => {
if (!selectedItems || selectedItems.length === 0) return "";
return selectedItems
.map((item) => item?.label)
.filter((label) => !!label)
.join("/");
};
function findMenuPath(
data: Record<string, any>[],
targetId: string,
path: (string | number)[] = []
): (string | number)[] | null {
for (const item of data) {
const currentPath = [...path, item.value];
if (String(item.value) === targetId) {
return currentPath;
}
if (item.children && item.children.length > 0) {
const found = findMenuPath(item.children, targetId, currentPath);
if (found) return found;
}
}
return null;
}
const handleParentColumnChange = ({ selectedItem, resolve, finish }: any) => {
if (String(selectedItem?.value) === "0") {
finish();
return;
}
const children = selectedItem.children;
if (children && children.length > 0) {
resolve(children);
} else {
finish();
}
};
const handleParentConfirm = ({ value }: any) => {
parentSelected.value = value;
formData.parentId = String(value[value.length - 1]) || "0";
};
const rules: FormRules = {
name: [{ required: true, message: "请输入菜单名称" }],
type: [{ required: true, message: "请选择菜单类型" }],
parentId: [{ required: true, message: "请选择上级菜单" }],
};
const handleSearch = () => loadMenuList();
function loadMenuList() {
MenuAPI.getList(queryParams).then((data) => {
menuList.value = data;
});
}
// 处理节点操作按钮点击
function handleNodeAction(node: any) {
const menu: MenuVO = {
id: node.id,
name: node.label,
type: node.type,
visible: node.visible,
children: node.children,
} as MenuVO;
const actions: string[] = [];
const actionMap: Record<string, () => void> = {};
if (hasPermission("sys:menu:create") && node.type !== 3) {
actions.push("新增子菜单");
actionMap["新增子菜单"] = () => handleAddChild(menu);
}
if (hasPermission("sys:menu:update")) {
actions.push("编辑");
actionMap["编辑"] = () => openMenuDialog(menu);
}
if (hasPermission("sys:menu:delete")) {
actions.push("删除");
actionMap["删除"] = async () => {
const { confirm } = await uni.showModal({
title: "确认删除",
content: `确定要删除菜单「${menu.name}」吗?`,
});
if (confirm) {
await MenuAPI.deleteById(menu.id!);
toast.success("删除成功");
loadMenuList();
}
};
}
if (actions.length === 0) {
toast.warning("暂无操作权限");
return;
}
uni.showActionSheet({
itemList: actions,
success: ({ tapIndex }) => {
const action = actions[tapIndex];
actionMap[action]?.();
},
});
}
// 打开弹窗(新增/编辑)
async function openMenuDialog(menu?: MenuVO) {
formRef.value?.reset();
Object.assign(formData, initialFormData);
parentSelected.value = [];
dialog.visible = true;
const data = await MenuAPI.getOptions(true);
parentOptions.value = data;
const firstColumn: OptionType[] = [{ value: "0", label: "顶级菜单" }, ...data];
parentColumns.value = [firstColumn];
if (menu) {
formData.id = menu.id;
const form = await MenuAPI.getFormData(menu.id!);
Object.assign(formData, form, { id: menu.id });
if (form.parentId && String(form.parentId) !== "0") {
const path = findMenuPath(data, String(form.parentId));
if (path) {
parentSelected.value = path;
const columns: OptionType[][] = [firstColumn];
let currentLevel = data;
for (let i = 0; i < path.length - 1; i++) {
const found = currentLevel.find((item) => String(item.value) === String(path[i]));
if (found && found.children && found.children.length > 0) {
columns.push(found.children);
currentLevel = found.children;
} else {
break;
}
}
parentColumns.value = columns;
} else {
parentSelected.value = [String(form.parentId)];
}
}
}
}
// 新增子菜单
function handleAddChild(menu: MenuVO) {
openMenuDialog({ id: undefined, parentId: menu.id } as MenuVO);
formData.parentId = menu.id!;
parentSelected.value = [menu.id!];
}
// 提交表单
function submitMenuForm() {
formRef.value.validate().then(({ valid }: { valid: boolean }) => {
if (!valid) return;
submitting.value = true;
const action = formData.id ? MenuAPI.update(formData.id, formData) : MenuAPI.add(formData);
action
.then(() => {
toast.success("操作成功");
closeMenuDialog();
loadMenuList();
})
.finally(() => {
submitting.value = false;
});
});
}
// 关闭弹窗
function closeMenuDialog() {
dialog.visible = false;
formRef.value?.reset();
Object.assign(formData, initialFormData);
}
onLoad(() => {
loadMenuList();
});
</script>
<script lang="ts">
export default { options: { styleIsolation: "shared" } };
</script>
<route lang="json">
{
"name": "menu",
"style": { "navigationBarTitleText": "菜单管理" }
"style": {
"navigationBarTitleText": "菜单管理"
}
}
</route>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background-color: #f8fafc;
}
.placeholder {
.popup-actions {
display: flex;
justify-content: center;
align-items: center;
height: 300rpx;
&__text {
font-size: 14px;
color: #94a3b8;
}
gap: 24rpx;
margin-top: 32rpx;
}
</style>