mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-02 16:37:53 +00:00
chore: 完成多批次代码优化与重构
- 重构工作流模块目录结构,迁移代码文件 - 修复类型断言空值安全问题,添加 ! 操作符 - 优化样式类名,替换 flex-cc 为标准 flex 工具类 - 更新路由标签简化文案,移除冗余注释 - 调整 ruff 配置,放宽行长度限制 - 更新 README 与多语言文案,优化项目描述 - 修复表单、图表组件的类型与样式问题 - 简化搜索表单、数据卡片的布局代码
This commit is contained in:
@@ -24,7 +24,7 @@
|
||||
>
|
||||
<ElScrollbar>
|
||||
<div
|
||||
class="login-page-panel__scroll min-h-0 flex-1 overflow-y-auto overflow-x-hidden pb-6 [-webkit-overflow-scrolling:touch]"
|
||||
class="login-page-panel__scroll pb-6"
|
||||
:class="panelAlign === 'center' && 'login-page-panel__scroll--centered'"
|
||||
>
|
||||
<div
|
||||
@@ -176,7 +176,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElScrollbar } from "element-plus";
|
||||
import type { LocationQuery, RouteLocationRaw } from "vue-router";
|
||||
import AuthAPI, {
|
||||
type CaptchaInfo,
|
||||
@@ -189,13 +188,13 @@ import { useConfigStore, useAppStore, useSettingsStore, useUserStore } from "@st
|
||||
import { Auth, HttpError, startOAuthLogin } from "@utils";
|
||||
import { ElMessage, ElNotification, type FormRules } from "element-plus";
|
||||
import type { Account, AccountKey } from "./types";
|
||||
import FaLoginAccountForm from "@/components/views/fa-login/FaLoginAccountForm.vue";
|
||||
import FaLoginForgetPanel from "@/components/views/fa-login/FaLoginForgetPanel.vue";
|
||||
import FaLoginMobilePanel from "@/components/views/fa-login/FaLoginMobilePanel.vue";
|
||||
import FaLoginQrPanel from "@/components/views/fa-login/FaLoginQrPanel.vue";
|
||||
import FaLoginRegisterPanel from "@/components/views/fa-login/FaLoginRegisterPanel.vue";
|
||||
import FaAuthTopBar from "@/components/views/fa-login/FaAuthTopBar.vue";
|
||||
import { useLoginPanelAlign } from "@/components/views/fa-login/useLoginPanelAlign";
|
||||
import FaLoginAccountForm from "@/components/views/fa-login/forms/FaLoginAccountForm.vue";
|
||||
import FaLoginForgetPanel from "@/components/views/fa-login/panels/FaLoginForgetPanel.vue";
|
||||
import FaLoginMobilePanel from "@/components/views/fa-login/panels/FaLoginMobilePanel.vue";
|
||||
import FaLoginQrPanel from "@/components/views/fa-login/panels/FaLoginQrPanel.vue";
|
||||
import FaLoginRegisterPanel from "@/components/views/fa-login/panels/FaLoginRegisterPanel.vue";
|
||||
import FaAuthTopBar from "@/components/views/fa-login/widgets/FaAuthTopBar.vue";
|
||||
import { useLoginPanelAlign } from "@/components/views/fa-login/composables/useLoginPanelAlign";
|
||||
|
||||
defineOptions({ name: "Login" });
|
||||
|
||||
@@ -681,5 +680,5 @@ async function submitForget() {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "@styles/custom/fa-login";
|
||||
@use "../../../../components/views/fa-login/fa-login";
|
||||
</style>
|
||||
|
||||
@@ -119,12 +119,17 @@ import DeptAPI, {
|
||||
} from "@/api/module_system/dept";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { useUserStore } from "@stores";
|
||||
import { formatTree, renderTableOperationCell, type TableOperationAction } from "@utils";
|
||||
import {
|
||||
formatTree,
|
||||
renderTableOperationCell,
|
||||
type TableOperationAction,
|
||||
resolveStatusColumns,
|
||||
} from "@utils";
|
||||
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
||||
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import FaForm from "@/components/forms/fa-form/index.vue";
|
||||
import { ElTag, ElMessage } from "element-plus";
|
||||
import type FaForm from "@/components/forms/fa-form/index.vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
defineOptions({
|
||||
name: "Dept",
|
||||
@@ -136,7 +141,7 @@ const userStore = useUserStore();
|
||||
|
||||
type DeptSearchForm = {
|
||||
name?: string;
|
||||
status?: string;
|
||||
status?: number;
|
||||
created_time?: string[];
|
||||
};
|
||||
|
||||
@@ -381,33 +386,35 @@ const opCtx = {
|
||||
onDelete: deleteDeptRow,
|
||||
};
|
||||
|
||||
const { columnChecks, columns } = useTableColumns<DeptTable>(() => [
|
||||
{ type: "selection", width: 48, fixed: "left" },
|
||||
{ type: "globalIndex", width: 56, label: "序号" },
|
||||
{ prop: "name", label: "部门名称", minWidth: 120, showOverflowTooltip: true },
|
||||
{ prop: "code", label: "部门编码", minWidth: 120, showOverflowTooltip: true },
|
||||
{
|
||||
prop: "status",
|
||||
label: "状态",
|
||||
width: 88,
|
||||
formatter: (row: DeptTable) =>
|
||||
h(ElTag, { type: row.status === 0 ? "success" : "danger" }, () =>
|
||||
row.status === 0 ? "启用" : "停用"
|
||||
),
|
||||
},
|
||||
{ prop: "order", label: "排序", width: 88, showOverflowTooltip: true },
|
||||
{ prop: "description", label: "描述", minWidth: 100, showOverflowTooltip: true },
|
||||
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
|
||||
{ prop: "updated_time", label: "更新时间", width: 168, showOverflowTooltip: true },
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
width: 220,
|
||||
fixed: "right",
|
||||
align: "right",
|
||||
formatter: (row: DeptTable) => formatDeptOperationCell(row, opCtx),
|
||||
},
|
||||
]);
|
||||
const { columnChecks, columns } = useTableColumns<DeptTable>(
|
||||
resolveStatusColumns(() => [
|
||||
{ type: "selection", width: 48, fixed: "left" },
|
||||
{ type: "globalIndex", width: 56, label: "序号" },
|
||||
{ prop: "name", label: "部门名称", minWidth: 120, showOverflowTooltip: true },
|
||||
{ prop: "code", label: "部门编码", minWidth: 120, showOverflowTooltip: true },
|
||||
{
|
||||
prop: "status",
|
||||
label: "状态",
|
||||
width: 88,
|
||||
status: {
|
||||
0: { type: "success", text: "启用" },
|
||||
1: { type: "danger", text: "停用" },
|
||||
},
|
||||
},
|
||||
{ prop: "order", label: "排序", width: 88, showOverflowTooltip: true },
|
||||
{ prop: "description", label: "描述", minWidth: 100, showOverflowTooltip: true },
|
||||
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
|
||||
{ prop: "updated_time", label: "更新时间", width: 168, showOverflowTooltip: true },
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
width: 220,
|
||||
fixed: "right",
|
||||
align: "right",
|
||||
formatter: (row: DeptTable) => formatDeptOperationCell(row, opCtx),
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
const deptDialogFormItems = computed<FormItem[]>(() => [
|
||||
{
|
||||
|
||||
@@ -196,6 +196,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from "vue";
|
||||
import { useTable } from "@/hooks/core/useTable";
|
||||
import { useImportExport } from "@/hooks/core/useImportExport";
|
||||
import { useCrudDialog } from "@/hooks/core/useCrudDialog";
|
||||
@@ -209,15 +210,16 @@ import DictAPI, {
|
||||
type DictDataTable,
|
||||
} from "@/api/module_system/dict";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@utils";
|
||||
import { renderTableOperationCell, type TableOperationAction, resolveStatusColumns } from "@utils";
|
||||
import { useAppStore, useDictStore } from "@stores";
|
||||
import { DeviceEnum } from "@/enums/settings/device.enum";
|
||||
import type { IObject } from "@/components/modal/types";
|
||||
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
||||
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import FaForm from "@/components/forms/fa-form/index.vue";
|
||||
import { ElTag, ElMessage } from "element-plus";
|
||||
import type FaForm from "@/components/forms/fa-form/index.vue";
|
||||
import FaStatusTag from "@/components/others/fa-status-tag/index.vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
defineOptions({ name: "DictDataDrawer", inheritAttrs: false });
|
||||
|
||||
@@ -281,7 +283,7 @@ function getTagPreviewStyle(value?: string) {
|
||||
|
||||
type DictDataSearchForm = {
|
||||
dict_label?: string;
|
||||
status?: string;
|
||||
status?: number;
|
||||
created_time?: string[];
|
||||
};
|
||||
|
||||
@@ -392,7 +394,7 @@ const {
|
||||
dict_type: props.dictType,
|
||||
dict_type_id: props.dictTypeId,
|
||||
},
|
||||
columnsFactory: (): ColumnOption<DictDataTable>[] => [
|
||||
columnsFactory: resolveStatusColumns<DictDataTable>(() => [
|
||||
{ type: "selection", width: 48, fixed: "left" },
|
||||
{ type: "globalIndex", width: 56, label: "序号" },
|
||||
{ prop: "dict_label", label: "标签", minWidth: 150, showOverflowTooltip: true },
|
||||
@@ -400,19 +402,17 @@ const {
|
||||
prop: "status",
|
||||
label: "状态",
|
||||
width: 100,
|
||||
formatter: (row: DictDataTable) => {
|
||||
const ok = row.status === 0;
|
||||
const cfg = ok
|
||||
? { type: "success" as const, text: "启用" }
|
||||
: { type: "danger" as const, text: "停用" };
|
||||
return h(ElTag, { type: cfg.type }, () => cfg.text);
|
||||
status: {
|
||||
0: { type: "success", text: "启用" },
|
||||
1: { type: "danger", text: "停用" },
|
||||
},
|
||||
},
|
||||
{
|
||||
prop: "dict_type",
|
||||
label: "类型",
|
||||
minWidth: 180,
|
||||
formatter: (row: DictDataTable) => h(ElTag, { type: "primary" }, () => row.dict_type ?? ""),
|
||||
formatter: (row: DictDataTable) =>
|
||||
h(FaStatusTag, { type: "primary", label: row.dict_type ?? "" }),
|
||||
},
|
||||
{ prop: "dict_value", label: "值", minWidth: 100, showOverflowTooltip: true },
|
||||
{ prop: "css_class", label: "样式属性", minWidth: 100, showOverflowTooltip: true },
|
||||
@@ -422,10 +422,10 @@ const {
|
||||
prop: "is_default",
|
||||
label: "是否默认",
|
||||
width: 100,
|
||||
formatter: (row: DictDataTable) =>
|
||||
h(ElTag, { type: row.is_default ? "success" : "danger" }, () =>
|
||||
row.is_default ? "是" : "否"
|
||||
),
|
||||
status: {
|
||||
true: { type: "success", text: "是" },
|
||||
false: { type: "danger", text: "否" },
|
||||
},
|
||||
},
|
||||
{ prop: "description", label: "描述", minWidth: 100, showOverflowTooltip: true },
|
||||
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
|
||||
@@ -438,7 +438,7 @@ const {
|
||||
align: "right",
|
||||
formatter: (row: DictDataTable) => formatDictDataOperationCell(row),
|
||||
},
|
||||
],
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
max-height="70vh"
|
||||
>
|
||||
<template #dict_type="{ row }">
|
||||
<ElTag type="primary">{{ row?.dict_type }}</ElTag>
|
||||
<FaStatusTag type="primary" :label="(row as unknown as DictTable)?.dict_type" />
|
||||
</template>
|
||||
</FaDescriptions>
|
||||
</template>
|
||||
@@ -126,6 +126,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from "vue";
|
||||
import { useTable } from "@/hooks/core/useTable";
|
||||
import { useImportExport } from "@/hooks/core/useImportExport";
|
||||
import { useCrudDialog } from "@/hooks/core/useCrudDialog";
|
||||
@@ -141,14 +142,15 @@ import DictAPI, {
|
||||
} from "@/api/module_system/dict";
|
||||
import { useDictStore } from "@stores";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@utils";
|
||||
import { renderTableOperationCell, type TableOperationAction, resolveStatusColumns } from "@utils";
|
||||
import type { IObject } from "@/components/modal/types";
|
||||
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
||||
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import FaForm from "@/components/forms/fa-form/index.vue";
|
||||
import type FaForm from "@/components/forms/fa-form/index.vue";
|
||||
import FaStatusTag from "@/components/others/fa-status-tag/index.vue";
|
||||
import DataDrawer from "./components/DataDrawer.vue";
|
||||
import { ElTag, ElMessage } from "element-plus";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
defineOptions({
|
||||
name: "Dict",
|
||||
@@ -158,7 +160,7 @@ defineOptions({
|
||||
type DictTypeSearchForm = {
|
||||
dict_name?: string;
|
||||
dict_type?: string;
|
||||
status?: string;
|
||||
status?: number;
|
||||
created_time?: string[];
|
||||
};
|
||||
|
||||
@@ -364,7 +366,7 @@ const {
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
},
|
||||
columnsFactory: (): ColumnOption<DictTable>[] => [
|
||||
columnsFactory: resolveStatusColumns<DictTable>(() => [
|
||||
{ type: "selection", width: 48, fixed: "left" },
|
||||
{ type: "globalIndex", width: 56, label: "序号" },
|
||||
{ prop: "dict_name", label: "字典名称", minWidth: 140, showOverflowTooltip: true },
|
||||
@@ -372,18 +374,16 @@ const {
|
||||
prop: "dict_type",
|
||||
label: "字典类型",
|
||||
minWidth: 180,
|
||||
formatter: (row: DictTable) => h(ElTag, { type: "primary" }, () => row.dict_type ?? ""),
|
||||
formatter: (row: DictTable) =>
|
||||
h(FaStatusTag, { type: "primary", label: row.dict_type ?? "" }),
|
||||
},
|
||||
{
|
||||
prop: "status",
|
||||
label: "状态",
|
||||
width: 88,
|
||||
formatter: (row: DictTable) => {
|
||||
const ok = row.status === 0;
|
||||
const cfg = ok
|
||||
? { type: "success" as const, text: "启用" }
|
||||
: { type: "danger" as const, text: "停用" };
|
||||
return h(ElTag, { type: cfg.type }, () => cfg.text);
|
||||
status: {
|
||||
0: { type: "success", text: "启用" },
|
||||
1: { type: "danger", text: "停用" },
|
||||
},
|
||||
},
|
||||
{ prop: "description", label: "描述", minWidth: 140, showOverflowTooltip: true },
|
||||
@@ -397,7 +397,7 @@ const {
|
||||
align: "right",
|
||||
formatter: (row: DictTable) => formatDictOperationCell(row),
|
||||
},
|
||||
],
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -90,10 +90,16 @@
|
||||
}}</ElTag>
|
||||
</template>
|
||||
<template #request_payload="{ row }">
|
||||
<FaJsonPretty :value="row?.request_payload as any" height="80px" />
|
||||
<FaJsonPretty
|
||||
:value="(row as unknown as OperationLogTable)?.request_payload"
|
||||
height="80px"
|
||||
/>
|
||||
</template>
|
||||
<template #response_json="{ row }">
|
||||
<FaJsonPretty :value="row?.response_json as any" height="140px" />
|
||||
<FaJsonPretty
|
||||
:value="(row as unknown as OperationLogTable)?.response_json"
|
||||
height="140px"
|
||||
/>
|
||||
</template>
|
||||
</FaDescriptions>
|
||||
</FaDialog>
|
||||
@@ -187,6 +193,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from "vue";
|
||||
import { useTable } from "@/hooks/core/useTable";
|
||||
import { useImportExport } from "@/hooks/core/useImportExport";
|
||||
import { useCrudDialog } from "@/hooks/core/useCrudDialog";
|
||||
@@ -201,13 +208,17 @@ import OperationLogAPI, {
|
||||
LoginLogAPI,
|
||||
} from "@/api/module_system/log";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@utils";
|
||||
import {
|
||||
renderTableOperationCell,
|
||||
type TableOperationAction,
|
||||
resolveStatusColumns,
|
||||
type StatusType,
|
||||
} from "@utils";
|
||||
import type { IObject } from "@/components/modal/types";
|
||||
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
|
||||
import FaUserTableSelect from "@/components/forms/fa-search-bar/FaUserTableSelect.vue";
|
||||
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import FaStatusTag from "@/components/others/fa-status-tag/index.vue";
|
||||
import FaCopyButton from "@/components/others/fa-copy-button/index.vue";
|
||||
import { ElTag } from "element-plus";
|
||||
|
||||
defineOptions({
|
||||
name: "Log",
|
||||
@@ -306,16 +317,20 @@ const {
|
||||
label: "请求方法",
|
||||
minWidth: 100,
|
||||
formatter: (row: OperationLogTable) =>
|
||||
h(ElTag, { type: getMethodType(row.request_method) }, () => row.request_method ?? ""),
|
||||
h(FaStatusTag, {
|
||||
type: getMethodType(row.request_method),
|
||||
label: row.request_method ?? "",
|
||||
}),
|
||||
},
|
||||
{
|
||||
prop: "response_code",
|
||||
label: "状态码",
|
||||
minWidth: 100,
|
||||
formatter: (row: OperationLogTable) =>
|
||||
h(ElTag, { type: getStatusCodeType(row.response_code) }, () =>
|
||||
String(row.response_code ?? "")
|
||||
),
|
||||
h(FaStatusTag, {
|
||||
type: getStatusCodeType(row.response_code),
|
||||
label: String(row.response_code ?? ""),
|
||||
}),
|
||||
},
|
||||
{ prop: "process_time", label: "处理时间", minWidth: 120 },
|
||||
{ prop: "description", label: "描述", minWidth: 120, showOverflowTooltip: true },
|
||||
@@ -351,11 +366,11 @@ const opExportContentConfig = computed(() => ({
|
||||
permPrefix: "module_system:log",
|
||||
cols: opCrudCols.value,
|
||||
exportsBlobAction: async (params: IObject) => {
|
||||
const res = await (OperationLogAPI as any).exportLog(
|
||||
const res = await OperationLogAPI.export(
|
||||
cleanEmptyArrayParams({
|
||||
...(opExportQueryParams.value as unknown as Record<string, unknown>),
|
||||
...params,
|
||||
})
|
||||
}) as OperationLogPageQuery
|
||||
);
|
||||
return res.data as Blob;
|
||||
},
|
||||
@@ -535,17 +550,17 @@ const {
|
||||
core: {
|
||||
apiFn: LoginLogAPI.list,
|
||||
apiParams: { page_no: 1, page_size: 10 },
|
||||
columnsFactory: (): ColumnOption<LoginLogTable>[] => [
|
||||
columnsFactory: resolveStatusColumns<LoginLogTable>(() => [
|
||||
{ type: "selection", width: 48, fixed: "left" },
|
||||
{ type: "globalIndex", width: 56, label: "序号" },
|
||||
{
|
||||
prop: "status",
|
||||
label: "登录状态",
|
||||
width: 88,
|
||||
formatter: (row: LoginLogTable) =>
|
||||
h(ElTag, { type: row.status === 1 ? "success" : "danger" }, () =>
|
||||
row.status === 1 ? "成功" : "失败"
|
||||
),
|
||||
status: {
|
||||
1: { type: "success", text: "成功" },
|
||||
0: { type: "danger", text: "失败" },
|
||||
},
|
||||
},
|
||||
{ prop: "username", label: "用户名", minWidth: 120, showOverflowTooltip: true },
|
||||
{
|
||||
@@ -573,7 +588,7 @@ const {
|
||||
align: "right",
|
||||
formatter: (row: LoginLogTable) => formatLoginActionCell(row),
|
||||
},
|
||||
],
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -685,14 +700,14 @@ watch(activeTab, (tab) => {
|
||||
|
||||
// ==================== 通用 ====================
|
||||
|
||||
function getStatusCodeType(code?: number) {
|
||||
function getStatusCodeType(code?: number): StatusType {
|
||||
if (code === undefined) return "info";
|
||||
if (code >= 200 && code < 300) return "success";
|
||||
if (code >= 300 && code < 400) return "warning";
|
||||
return "danger";
|
||||
}
|
||||
|
||||
function getMethodType(method?: string) {
|
||||
function getMethodType(method?: string): StatusType {
|
||||
if (method === undefined) return "info";
|
||||
if (method === "GET") return "info";
|
||||
if (method === "POST") return "success";
|
||||
|
||||
@@ -85,17 +85,18 @@
|
||||
max-height="75vh"
|
||||
>
|
||||
<template #notice_type="{ row }">
|
||||
<ElTag :type="row?.notice_type === '1' ? 'primary' : 'warning'">
|
||||
{{ noticeTypeLabel(row?.notice_type as string) }}
|
||||
</ElTag>
|
||||
<FaStatusTag
|
||||
:type="row?.notice_type === '1' ? 'primary' : 'warning'"
|
||||
:label="noticeTypeLabel(row?.notice_type as string)"
|
||||
/>
|
||||
</template>
|
||||
<template #notice_content>
|
||||
<div class="notice-html-preview">
|
||||
<ElScrollbar class="notice-html-preview" view-class="p-3">
|
||||
<template v-if="detailHasRenderableContent">
|
||||
<div v-html="detailContentHtml" />
|
||||
</template>
|
||||
<p v-else class="notice-html-empty">暂无内容</p>
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
</template>
|
||||
</FaDescriptions>
|
||||
</template>
|
||||
@@ -161,15 +162,14 @@ import NoticeAPI, {
|
||||
type NoticeTable,
|
||||
} from "@/api/module_system/notice";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@utils";
|
||||
import { renderTableOperationCell, type TableOperationAction, resolveStatusColumns } from "@utils";
|
||||
import { useDictStore, useNoticeStore } from "@stores";
|
||||
import type { IObject } from "@/components/modal/types";
|
||||
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
||||
import FaUserTableSelect from "@/components/forms/fa-search-bar/FaUserTableSelect.vue";
|
||||
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import FaForm from "@/components/forms/fa-form/index.vue";
|
||||
import { ElTag, ElMessage } from "element-plus";
|
||||
import type FaForm from "@/components/forms/fa-form/index.vue";
|
||||
import { ElMessage, ElScrollbar } from "element-plus";
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
defineOptions({
|
||||
@@ -184,7 +184,7 @@ const { hasAuth } = useAuth();
|
||||
type NoticeSearchForm = {
|
||||
notice_title?: string;
|
||||
notice_type?: string;
|
||||
status?: string;
|
||||
status?: number;
|
||||
created_time?: string[];
|
||||
created_id?: number;
|
||||
};
|
||||
@@ -457,7 +457,7 @@ const {
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
},
|
||||
columnsFactory: (): ColumnOption<NoticeTable>[] => [
|
||||
columnsFactory: resolveStatusColumns<NoticeTable>(() => [
|
||||
{ type: "selection", width: 48, fixed: "left" },
|
||||
{ type: "globalIndex", width: 56, label: "序号" },
|
||||
{ prop: "notice_title", label: "通知标题", minWidth: 140, showOverflowTooltip: true },
|
||||
@@ -465,22 +465,19 @@ const {
|
||||
prop: "status",
|
||||
label: "状态",
|
||||
width: 88,
|
||||
formatter: (row: NoticeTable) => {
|
||||
const ok = row.status === 0;
|
||||
const cfg = ok
|
||||
? { type: "success" as const, text: "启用" }
|
||||
: { type: "danger" as const, text: "停用" };
|
||||
return h(ElTag, { type: cfg.type }, () => cfg.text);
|
||||
status: {
|
||||
0: { type: "success", text: "启用" },
|
||||
1: { type: "danger", text: "停用" },
|
||||
},
|
||||
},
|
||||
{
|
||||
prop: "notice_type",
|
||||
label: "类型",
|
||||
minWidth: 100,
|
||||
formatter: (row: NoticeTable) =>
|
||||
h(ElTag, { type: row.notice_type === "1" ? "primary" : "warning" }, () =>
|
||||
noticeTypeLabel(row.notice_type)
|
||||
),
|
||||
status: {
|
||||
"1": { type: "primary", text: "通知" },
|
||||
"2": { type: "warning", text: "公告" },
|
||||
},
|
||||
},
|
||||
{ prop: "notice_content", label: "内容", minWidth: 200, showOverflowTooltip: true },
|
||||
{ prop: "description", label: "描述", minWidth: 140, showOverflowTooltip: true },
|
||||
@@ -506,7 +503,7 @@ const {
|
||||
align: "right",
|
||||
formatter: (row: NoticeTable) => formatNoticeOperationCell(row),
|
||||
},
|
||||
],
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -678,8 +675,6 @@ onMounted(async () => {
|
||||
box-sizing: border-box;
|
||||
min-height: 120px;
|
||||
max-height: min(360px, 45vh);
|
||||
padding: 12px 16px;
|
||||
overflow-y: auto;
|
||||
background-color: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: calc(var(--custom-radius) / 3 + 2px);
|
||||
|
||||
@@ -130,10 +130,9 @@ import { renderTableOperationCell, type TableOperationAction } from "@utils";
|
||||
import { useConfigStore } from "@stores";
|
||||
import type { IObject } from "@/components/modal/types";
|
||||
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
||||
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import FaForm from "@/components/forms/fa-form/index.vue";
|
||||
import { ElTag } from "element-plus";
|
||||
import type FaForm from "@/components/forms/fa-form/index.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "Params",
|
||||
@@ -386,10 +385,10 @@ const {
|
||||
prop: "config_type",
|
||||
label: "系统内置",
|
||||
minWidth: 100,
|
||||
formatter: (row: ConfigTable) =>
|
||||
h(ElTag, { type: row.config_type ? "success" : "danger" }, () =>
|
||||
row.config_type ? "是" : "否"
|
||||
),
|
||||
status: {
|
||||
true: { type: "success", text: "是" },
|
||||
false: { type: "danger", text: "否" },
|
||||
},
|
||||
},
|
||||
{ prop: "description", label: "描述", minWidth: 120, showOverflowTooltip: true },
|
||||
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
|
||||
|
||||
@@ -123,6 +123,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from "vue";
|
||||
import { useTable } from "@/hooks/core/useTable";
|
||||
import { useImportExport } from "@/hooks/core/useImportExport";
|
||||
import { useCrudDialog } from "@/hooks/core/useCrudDialog";
|
||||
@@ -140,19 +141,12 @@ import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { useUserStore } from "@stores";
|
||||
import type { IObject } from "@/components/modal/types";
|
||||
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
||||
import FaUserTableSelect from "@/components/forms/fa-search-bar/FaUserTableSelect.vue";
|
||||
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import FaForm from "@/components/forms/fa-form/index.vue";
|
||||
import type FaForm from "@/components/forms/fa-form/index.vue";
|
||||
import FaButtonTable from "@/components/forms/fa-button-table/index.vue";
|
||||
import {
|
||||
ElTag,
|
||||
ElMessage,
|
||||
ElTooltip,
|
||||
ElDropdown,
|
||||
ElDropdownMenu,
|
||||
ElDropdownItem,
|
||||
} from "element-plus";
|
||||
import { resolveStatusColumns } from "@utils";
|
||||
import { ElMessage, ElTooltip, ElDropdown, ElDropdownMenu, ElDropdownItem } from "element-plus";
|
||||
|
||||
defineOptions({
|
||||
name: "Position",
|
||||
@@ -165,7 +159,7 @@ const userStore = useUserStore();
|
||||
|
||||
type PositionSearchForm = {
|
||||
name?: string;
|
||||
status?: string;
|
||||
status?: number;
|
||||
created_time?: string[];
|
||||
created_id?: number;
|
||||
};
|
||||
@@ -390,7 +384,7 @@ const {
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
},
|
||||
columnsFactory: (): ColumnOption<PositionTable>[] => [
|
||||
columnsFactory: resolveStatusColumns<PositionTable>(() => [
|
||||
{ type: "selection", width: 48, fixed: "left" },
|
||||
{ type: "globalIndex", width: 56, label: "序号" },
|
||||
{ prop: "name", label: "岗位名称", minWidth: 100, showOverflowTooltip: true },
|
||||
@@ -398,10 +392,10 @@ const {
|
||||
prop: "status",
|
||||
label: "状态",
|
||||
width: 88,
|
||||
formatter: (row: PositionTable) =>
|
||||
h(ElTag, { type: row.status === 0 ? "success" : "danger" }, () =>
|
||||
row.status === 0 ? "启用" : "停用"
|
||||
),
|
||||
status: {
|
||||
0: { type: "success", text: "启用" },
|
||||
1: { type: "danger", text: "停用" },
|
||||
},
|
||||
},
|
||||
{ prop: "order", label: "岗位排序", width: 100, showOverflowTooltip: true },
|
||||
{ prop: "description", label: "描述", minWidth: 120, showOverflowTooltip: true },
|
||||
@@ -427,7 +421,7 @@ const {
|
||||
align: "right",
|
||||
formatter: (row: PositionTable) => formatPositionOperationCell(row, opCtx),
|
||||
},
|
||||
],
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -109,10 +109,9 @@
|
||||
import { computed, ref, watch, onMounted, nextTick } from "vue";
|
||||
import { QuestionFilled } from "@element-plus/icons-vue";
|
||||
import type { TreeInstance } from "element-plus";
|
||||
import FaDrawer from "@/components/modal/fa-drawer/index.vue";
|
||||
import FaMenuTreeTable from "@/components/others/fa-menu-tree-drawer/index.vue";
|
||||
import { listToTree, formatTree } from "@utils";
|
||||
import RoleAPI, { permissionDataType, permissionDeptType } from "@/api/module_system/role";
|
||||
import { listToTree, formatTree, type CascaderNode } from "@utils";
|
||||
import FaMenuTreeTable from "@/components/others/fa-menu-tree-table/index.vue";
|
||||
import RoleAPI, { permissionDataType } from "@/api/module_system/role";
|
||||
import DeptAPI from "@/api/module_system/dept";
|
||||
import MenuAPI, { MenuTable } from "@/api/module_platform/menu";
|
||||
import { DeviceEnum } from "@/enums/settings/device.enum";
|
||||
@@ -146,7 +145,7 @@ const deptTreeRef = ref<TreeInstance>();
|
||||
const deptFilterText = ref("");
|
||||
const dataFormRef = ref();
|
||||
const loading = ref(false);
|
||||
const deptTreeData = ref<permissionDeptType[]>([]);
|
||||
const deptTreeData = ref<CascaderNode[]>([]);
|
||||
const rawMenuTree = ref<MenuTable[]>([]);
|
||||
const menuCheckedIds = ref<number[]>([]);
|
||||
const menuTreeTableRef = ref<InstanceType<typeof FaMenuTreeTable>>();
|
||||
|
||||
-621
@@ -1,621 +0,0 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
|
||||
/* ============================================================
|
||||
测试数据:与组件中 MenuTable 结构一致
|
||||
============================================================ */
|
||||
const DIR_TYPE = 1;
|
||||
const MENU_TYPE = 2;
|
||||
const BTN_TYPE = 3;
|
||||
const LINK_TYPE = 4;
|
||||
const PERM_TYPES = [BTN_TYPE, LINK_TYPE];
|
||||
|
||||
type MenuTable = {
|
||||
id?: number;
|
||||
name?: string;
|
||||
type?: number;
|
||||
icon?: string;
|
||||
children?: MenuTable[];
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
function isPermType(type: number | undefined): boolean {
|
||||
return type != null && PERM_TYPES.includes(type);
|
||||
}
|
||||
|
||||
function filterTableData(nodes: MenuTable[]): MenuTable[] {
|
||||
return nodes
|
||||
.filter((n) => n.type === DIR_TYPE || n.type === MENU_TYPE)
|
||||
.map((n) => ({
|
||||
...n,
|
||||
children: n.children ? filterTableData(n.children) : undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
function findTableNode(id: number, nodes: MenuTable[]): MenuTable | undefined {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node;
|
||||
if (node.children) {
|
||||
const found = findTableNode(id, node.children);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findRawNode(id: number, nodes: any[]): any | undefined {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node;
|
||||
if (node.children) {
|
||||
const found = findRawNode(id, node.children);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findAncestors(nodeId: number, nodes: any[]): any[] {
|
||||
for (const node of nodes) {
|
||||
if ((node.type === DIR_TYPE || node.type === MENU_TYPE) && node.children) {
|
||||
const childIndex = node.children.findIndex((c: any) => c.id === nodeId);
|
||||
if (childIndex !== -1) {
|
||||
const tableNode = findTableNode(node.id!, mockTableData);
|
||||
const parentAncestors = findAncestors(node.id!, mockRawTree);
|
||||
if (tableNode) {
|
||||
return [tableNode, ...parentAncestors];
|
||||
}
|
||||
return parentAncestors;
|
||||
}
|
||||
const ancestors = findAncestors(nodeId, node.children);
|
||||
if (ancestors.length > 0) {
|
||||
return ancestors;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function getMenuBtns(row: MenuTable, rawTree: MenuTable[]): MenuTable[] {
|
||||
const fullNode = findRawNode(row.id!, rawTree);
|
||||
if (!fullNode?.children) return [];
|
||||
return (fullNode.children as MenuTable[]).filter((c) => isPermType(c.type));
|
||||
}
|
||||
|
||||
function findParentMenu(menuId: number, menuTreeData: MenuTable[]): MenuTable | null {
|
||||
for (const menu of menuTreeData) {
|
||||
if (menu.children) {
|
||||
for (const child of menu.children) {
|
||||
if (child.id === menuId) return menu;
|
||||
const found = findParentMenu(menuId, [child]);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasAnySelectedDescendant(
|
||||
node: MenuTable,
|
||||
checkedBtns: Record<number, number[]>,
|
||||
tableData: MenuTable[]
|
||||
): boolean {
|
||||
if (checkedBtns[node.id!]?.length > 0) return true;
|
||||
const tableNode = findTableNode(node.id!, tableData);
|
||||
if (tableNode?.children) {
|
||||
for (const child of tableNode.children) {
|
||||
if (
|
||||
(child.type === MENU_TYPE || child.type === DIR_TYPE) &&
|
||||
hasAnySelectedDescendant(child, checkedBtns, tableData)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function checkParentChildLinked(menuIds: number[], menuTreeData: MenuTable[]): boolean {
|
||||
if (!menuIds.length || !menuTreeData.length) return false;
|
||||
const menuMap = new Map<number, MenuTable>();
|
||||
const buildMenuMap = (menus: MenuTable[]) => {
|
||||
menus.forEach((menu) => {
|
||||
menuMap.set(menu.id!, menu);
|
||||
if (menu.children) buildMenuMap(menu.children);
|
||||
});
|
||||
};
|
||||
buildMenuMap(menuTreeData);
|
||||
let hasParentChildConflict = false;
|
||||
for (const menuId of menuIds) {
|
||||
const menu = menuMap.get(menuId);
|
||||
if (!menu) continue;
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const hasUnselectedChildren = menu.children.some((child) => !menuIds.includes(child.id!));
|
||||
if (hasUnselectedChildren) {
|
||||
hasParentChildConflict = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const parentMenu = findParentMenu(menuId, menuTreeData);
|
||||
if (parentMenu && !menuIds.includes(parentMenu.id!)) {
|
||||
hasParentChildConflict = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return !hasParentChildConflict;
|
||||
}
|
||||
|
||||
function expandMenuIdsWithAncestors(checkedIds: number[], roots: MenuTable[]): number[] {
|
||||
const parentById = new Map<number, number | undefined>();
|
||||
const walk = (nodes: MenuTable[], parent: number | undefined) => {
|
||||
for (const n of nodes) {
|
||||
const id = n.id!;
|
||||
parentById.set(id, parent);
|
||||
if (n.children?.length) walk(n.children as MenuTable[], id);
|
||||
}
|
||||
};
|
||||
walk(roots, undefined);
|
||||
const out = new Set<number>();
|
||||
for (const id of checkedIds) {
|
||||
let cur: number | undefined = id;
|
||||
while (cur !== undefined) {
|
||||
out.add(cur);
|
||||
cur = parentById.get(cur);
|
||||
}
|
||||
}
|
||||
return [...out];
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
测试数据
|
||||
============================================================ */
|
||||
const mockRawTree: MenuTable[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "系统管理",
|
||||
type: DIR_TYPE,
|
||||
icon: "Setting",
|
||||
children: [
|
||||
{
|
||||
id: 2,
|
||||
name: "用户管理",
|
||||
type: MENU_TYPE,
|
||||
children: [
|
||||
{ id: 21, name: "新增用户", type: BTN_TYPE },
|
||||
{ id: 22, name: "编辑用户", type: BTN_TYPE },
|
||||
{ id: 23, name: "删除用户", type: BTN_TYPE },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "角色管理",
|
||||
type: MENU_TYPE,
|
||||
children: [
|
||||
{ id: 31, name: "新增角色", type: BTN_TYPE },
|
||||
{ id: 32, name: "编辑角色", type: BTN_TYPE },
|
||||
],
|
||||
},
|
||||
{ id: 4, name: "菜单管理", type: MENU_TYPE, children: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "业务管理",
|
||||
type: DIR_TYPE,
|
||||
icon: "DataAnalysis",
|
||||
children: [
|
||||
{
|
||||
id: 6,
|
||||
name: "订单管理",
|
||||
type: MENU_TYPE,
|
||||
children: [
|
||||
{ id: 61, name: "查看订单", type: BTN_TYPE },
|
||||
{ id: 62, name: "导出订单", type: LINK_TYPE },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const mockTableData = filterTableData(mockRawTree);
|
||||
|
||||
/* ============================================================
|
||||
Test Suite: 纯逻辑函数
|
||||
============================================================ */
|
||||
describe("纯逻辑函数", () => {
|
||||
describe("isPermType", () => {
|
||||
it("应识别按钮类型", () => {
|
||||
expect(isPermType(BTN_TYPE)).toBe(true);
|
||||
});
|
||||
it("应识别链接类型", () => {
|
||||
expect(isPermType(LINK_TYPE)).toBe(true);
|
||||
});
|
||||
it("应排除目录类型", () => {
|
||||
expect(isPermType(DIR_TYPE)).toBe(false);
|
||||
});
|
||||
it("应排除菜单类型", () => {
|
||||
expect(isPermType(MENU_TYPE)).toBe(false);
|
||||
});
|
||||
it("应处理 undefined", () => {
|
||||
expect(isPermType(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("filterTableData", () => {
|
||||
it("应过滤掉所有按钮和链接节点", () => {
|
||||
const result = filterTableData(mockRawTree);
|
||||
const ids = flattenIds(result);
|
||||
expect(ids).not.toContain(21);
|
||||
expect(ids).not.toContain(22);
|
||||
expect(ids).not.toContain(31);
|
||||
expect(ids).not.toContain(61);
|
||||
expect(ids).not.toContain(62);
|
||||
});
|
||||
it("应保留目录和菜单节点", () => {
|
||||
const result = filterTableData(mockRawTree);
|
||||
const ids = flattenIds(result);
|
||||
expect(ids).toContain(1);
|
||||
expect(ids).toContain(2);
|
||||
expect(ids).toContain(3);
|
||||
expect(ids).toContain(4);
|
||||
expect(ids).toContain(5);
|
||||
expect(ids).toContain(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findTableNode", () => {
|
||||
it("应找到根节点", () => {
|
||||
const node = findTableNode(1, mockTableData);
|
||||
expect(node).toBeDefined();
|
||||
expect(node!.name).toBe("系统管理");
|
||||
});
|
||||
it("应找到深层节点", () => {
|
||||
const node = findTableNode(3, mockTableData);
|
||||
expect(node).toBeDefined();
|
||||
expect(node!.name).toBe("角色管理");
|
||||
});
|
||||
it("找不到时返回 undefined", () => {
|
||||
expect(findTableNode(999, mockTableData)).toBeUndefined();
|
||||
});
|
||||
it("按钮ID应返回 undefined(已在过滤后的数据中移除)", () => {
|
||||
expect(findTableNode(21, mockTableData)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("findRawNode", () => {
|
||||
it("应找到原始树中的按钮节点", () => {
|
||||
const node = findRawNode(21, mockRawTree);
|
||||
expect(node).toBeDefined();
|
||||
expect(node!.name).toBe("新增用户");
|
||||
});
|
||||
it("应找到原始树中的根节点", () => {
|
||||
const node = findRawNode(1, mockRawTree);
|
||||
expect(node).toBeDefined();
|
||||
expect(node!.name).toBe("系统管理");
|
||||
});
|
||||
it("找不到时返回 undefined", () => {
|
||||
expect(findRawNode(999, mockRawTree)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("findAncestors", () => {
|
||||
it("应返回节点 2 的祖先 [节点 1]", () => {
|
||||
const ancestors = findAncestors(2, mockRawTree);
|
||||
expect(ancestors.length).toBe(1);
|
||||
expect(ancestors[0].id).toBe(1);
|
||||
});
|
||||
it("应返回节点 6 的祖先 [节点 5]", () => {
|
||||
const ancestors = findAncestors(6, mockRawTree);
|
||||
expect(ancestors.length).toBe(1);
|
||||
expect(ancestors[0].id).toBe(5);
|
||||
});
|
||||
it("根节点无祖先", () => {
|
||||
const ancestors = findAncestors(1, mockRawTree);
|
||||
expect(ancestors.length).toBe(0);
|
||||
});
|
||||
it("按钮节点 21 的祖先链应包含节点 1", () => {
|
||||
const ancestors = findAncestors(21, mockRawTree);
|
||||
const ancestorIds = ancestors.map((n) => n.id);
|
||||
expect(ancestorIds).toContain(1);
|
||||
expect(ancestorIds).toContain(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMenuBtns", () => {
|
||||
it("应返回菜单 2 下的按钮列表", () => {
|
||||
const row = findTableNode(2, mockTableData)!;
|
||||
const btns = getMenuBtns(row, mockRawTree);
|
||||
expect(btns.length).toBe(3);
|
||||
expect(btns.map((b) => b.id)).toEqual([21, 22, 23]);
|
||||
});
|
||||
it("菜单 4(无子节点)应返回空数组", () => {
|
||||
const row = findTableNode(4, mockTableData)!;
|
||||
const btns = getMenuBtns(row, mockRawTree);
|
||||
expect(btns.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findParentMenu", () => {
|
||||
it("应找到按钮 21 的父菜单为 2", () => {
|
||||
const parent = findParentMenu(21, mockRawTree);
|
||||
expect(parent).not.toBeNull();
|
||||
expect(parent!.id).toBe(2);
|
||||
});
|
||||
it("应找到菜单 2 的父目录为 1", () => {
|
||||
const parent = findParentMenu(2, mockRawTree);
|
||||
expect(parent).not.toBeNull();
|
||||
expect(parent!.id).toBe(1);
|
||||
});
|
||||
it("根节点无父节点", () => {
|
||||
expect(findParentMenu(1, mockRawTree)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasAnySelectedDescendant", () => {
|
||||
it("节点自身有选中按钮时应返回 true", () => {
|
||||
const checkedBtns: Record<number, number[]> = { 2: [21] };
|
||||
const node = findTableNode(2, mockTableData)!;
|
||||
expect(hasAnySelectedDescendant(node, checkedBtns, mockTableData)).toBe(true);
|
||||
});
|
||||
it("子节点有选中按钮时应返回 true", () => {
|
||||
const checkedBtns: Record<number, number[]> = { 3: [31] };
|
||||
const node = findTableNode(1, mockTableData)!;
|
||||
expect(hasAnySelectedDescendant(node, checkedBtns, mockTableData)).toBe(true);
|
||||
});
|
||||
it("无选中时应返回 false", () => {
|
||||
const checkedBtns: Record<number, number[]> = {};
|
||||
const node = findTableNode(2, mockTableData)!;
|
||||
expect(hasAnySelectedDescendant(node, checkedBtns, mockTableData)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkParentChildLinked", () => {
|
||||
it("父子节点全部选中时返回 true", () => {
|
||||
const ids = [1, 2, 3, 4, 5, 6, 21, 22, 23, 31, 32, 61, 62];
|
||||
expect(checkParentChildLinked(ids, mockRawTree)).toBe(true);
|
||||
});
|
||||
it("子选中但父未选中时返回 false", () => {
|
||||
const ids = [2];
|
||||
expect(checkParentChildLinked(ids, mockRawTree)).toBe(false);
|
||||
});
|
||||
it("父选中但子未完全选中时返回 false", () => {
|
||||
const ids = [1, 2];
|
||||
// 节点 2 有三个子按钮未选
|
||||
expect(checkParentChildLinked(ids, mockRawTree)).toBe(false);
|
||||
});
|
||||
it("空数组返回 false", () => {
|
||||
expect(checkParentChildLinked([], mockRawTree)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("expandMenuIdsWithAncestors", () => {
|
||||
it("应补充所有祖先节点到结果中", () => {
|
||||
const ids = expandMenuIdsWithAncestors([21], mockRawTree);
|
||||
expect(ids).toContain(21);
|
||||
expect(ids).toContain(2);
|
||||
expect(ids).toContain(1);
|
||||
});
|
||||
it("根节点本身应保留", () => {
|
||||
const ids = expandMenuIdsWithAncestors([1], mockRawTree);
|
||||
expect(ids).toEqual([1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* ============================================================
|
||||
Test Suite: handleSelectAll 行为
|
||||
============================================================ */
|
||||
describe("handleSelectAll 行为模拟", () => {
|
||||
let checkedBtns: Record<number, number[]>;
|
||||
let selectedMenuIds: Set<number>;
|
||||
|
||||
function simulateHandleSelectAll(selectionIds: number[], parentChildLinked: boolean) {
|
||||
const selection = selectionIds
|
||||
.map((id) => findTableNode(id, mockTableData))
|
||||
.filter(Boolean) as MenuTable[];
|
||||
|
||||
if (!selection.length) {
|
||||
checkedBtns = {};
|
||||
selectedMenuIds.clear();
|
||||
} else if (parentChildLinked) {
|
||||
for (const row of selection) {
|
||||
if (row.type === DIR_TYPE || row.type === MENU_TYPE) {
|
||||
selectedMenuIds.add(row.id!);
|
||||
const btns = getMenuBtns(row, mockRawTree);
|
||||
if (btns.length > 0) {
|
||||
checkedBtns[row.id!] = btns.map((b) => b.id!);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const row of selection) {
|
||||
if (row.type === DIR_TYPE || row.type === MENU_TYPE) {
|
||||
selectedMenuIds.add(row.id!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
checkedBtns = {};
|
||||
selectedMenuIds = new Set();
|
||||
});
|
||||
|
||||
describe("取消全选", () => {
|
||||
it("选中行为空时应清空所有按钮状态", () => {
|
||||
// 预设一些按钮选中状态
|
||||
checkedBtns = { 2: [21, 22], 6: [61] };
|
||||
selectedMenuIds = new Set([2, 6]);
|
||||
|
||||
simulateHandleSelectAll([], false);
|
||||
expect(Object.keys(checkedBtns).length).toBe(0);
|
||||
expect(selectedMenuIds.size).toBe(0);
|
||||
});
|
||||
|
||||
it("即使有父子联动,取消全选也应清空按钮", () => {
|
||||
checkedBtns = { 2: [21, 22, 23], 3: [31, 32] };
|
||||
selectedMenuIds = new Set([1, 2, 3]);
|
||||
|
||||
simulateHandleSelectAll([], true);
|
||||
expect(Object.keys(checkedBtns).length).toBe(0);
|
||||
expect(selectedMenuIds.size).toBe(0);
|
||||
});
|
||||
|
||||
it("选中行为空时再次取消全选应保持清空状态", () => {
|
||||
simulateHandleSelectAll([], false);
|
||||
expect(Object.keys(checkedBtns).length).toBe(0);
|
||||
expect(selectedMenuIds.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("全选(无父子联动)", () => {
|
||||
it("应选中所有菜单行但不清空按钮", () => {
|
||||
simulateHandleSelectAll([1, 2, 3, 4, 5, 6], false);
|
||||
// 所有菜单行应被记录
|
||||
expect(selectedMenuIds.has(1)).toBe(true);
|
||||
expect(selectedMenuIds.has(2)).toBe(true);
|
||||
expect(selectedMenuIds.has(6)).toBe(true);
|
||||
// 无联动时不应自动收集按钮
|
||||
expect(checkedBtns[2]).toBeUndefined();
|
||||
expect(checkedBtns[6]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("全选(有父子联动)", () => {
|
||||
it("应为每个菜单行收集其下的功能按钮", () => {
|
||||
simulateHandleSelectAll([1, 2, 3, 4, 5, 6], true);
|
||||
// 应收集菜单 2 的按钮
|
||||
expect(checkedBtns[2]).toEqual([21, 22, 23]);
|
||||
// 应收集菜单 3 的按钮
|
||||
expect(checkedBtns[3]).toEqual([31, 32]);
|
||||
// 应收集菜单 6 的按钮(含链接类型)
|
||||
expect(checkedBtns[6]).toEqual([61, 62]);
|
||||
// 目录 1 和 5 没有功能按钮,应为空数组或 undefined
|
||||
expect(checkedBtns[1]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("菜单下无按钮时不应设置 checkedBtns", () => {
|
||||
simulateHandleSelectAll([4], true);
|
||||
expect(checkedBtns[4]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("部分选择", () => {
|
||||
it("只选中部分菜单行时只收集这些行的按钮", () => {
|
||||
simulateHandleSelectAll([2, 6], true);
|
||||
expect(selectedMenuIds.has(2)).toBe(true);
|
||||
expect(selectedMenuIds.has(6)).toBe(true);
|
||||
expect(selectedMenuIds.has(1)).toBe(false);
|
||||
expect(checkedBtns[2]).toEqual([21, 22, 23]);
|
||||
expect(checkedBtns[6]).toEqual([61, 62]);
|
||||
// 未选中的菜单不应有按钮
|
||||
expect(checkedBtns[3]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* ============================================================
|
||||
Test Suite: handleSelectionChange 行为(安全网)
|
||||
============================================================ */
|
||||
describe("handleSelectionChange 安全网", () => {
|
||||
let checkedBtns: Record<number, number[]>;
|
||||
|
||||
function simulateHandleSelectionChange(selectionIds: number[]) {
|
||||
const newSelected = new Set(selectionIds);
|
||||
if (newSelected.size === 0) {
|
||||
checkedBtns = {};
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
checkedBtns = { 2: [21, 22], 3: [31] };
|
||||
});
|
||||
|
||||
it("选中行为空时应清空按钮状态", () => {
|
||||
simulateHandleSelectionChange([]);
|
||||
expect(Object.keys(checkedBtns).length).toBe(0);
|
||||
});
|
||||
|
||||
it("选中行非空时不应清空按钮", () => {
|
||||
simulateHandleSelectionChange([2]);
|
||||
expect(checkedBtns[2]).toEqual([21, 22]);
|
||||
expect(checkedBtns[3]).toEqual([31]);
|
||||
});
|
||||
|
||||
it("部分选中不应影响按钮状态", () => {
|
||||
simulateHandleSelectionChange([2, 3]);
|
||||
expect(checkedBtns[2]).toEqual([21, 22]);
|
||||
expect(checkedBtns[3]).toEqual([31]);
|
||||
});
|
||||
});
|
||||
|
||||
/* ============================================================
|
||||
Test Suite: collectBtnState + cascadeDeselect
|
||||
============================================================ */
|
||||
describe("collectBtnState / cascadeDeselect", () => {
|
||||
it("collectBtnState 应为有按钮的行收集按钮 ID", () => {
|
||||
const row = findTableNode(2, mockTableData)!;
|
||||
const btns = getMenuBtns(row, mockRawTree);
|
||||
expect(btns.length).toBeGreaterThan(0);
|
||||
const checkedBtns: Record<number, number[]> = {};
|
||||
checkedBtns[row.id!] = btns.map((b) => b.id!);
|
||||
expect(checkedBtns[2]).toEqual([21, 22, 23]);
|
||||
});
|
||||
|
||||
it("cascadeDeselect 应清空指定行及其子行的按钮", () => {
|
||||
const checkedBtns: Record<number, number[]> = {
|
||||
2: [21, 22, 23],
|
||||
3: [31, 32],
|
||||
};
|
||||
// 模拟级联取消: 清空节点 1 下所有子菜单的按钮
|
||||
function cascadeDeselect(id: number) {
|
||||
delete checkedBtns[id];
|
||||
const node = findTableNode(id, mockTableData);
|
||||
if (node?.children) {
|
||||
for (const child of node.children) {
|
||||
if (child.type === MENU_TYPE || child.type === DIR_TYPE) {
|
||||
cascadeDeselect(child.id!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cascadeDeselect(1);
|
||||
expect(Object.keys(checkedBtns).length).toBe(0);
|
||||
});
|
||||
|
||||
it("级联取消不应影响无关行的按钮", () => {
|
||||
const checkedBtns: Record<number, number[]> = {
|
||||
2: [21, 22],
|
||||
6: [61],
|
||||
};
|
||||
function cascadeDeselect(id: number) {
|
||||
delete checkedBtns[id];
|
||||
const node = findTableNode(id, mockTableData);
|
||||
if (node?.children) {
|
||||
for (const child of node.children) {
|
||||
if (child.type === MENU_TYPE || child.type === DIR_TYPE) {
|
||||
cascadeDeselect(child.id!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cascadeDeselect(2);
|
||||
// 节点 2 的按钮被清空
|
||||
expect(checkedBtns[2]).toBeUndefined();
|
||||
// 节点 6 不受影响
|
||||
expect(checkedBtns[6]).toEqual([61]);
|
||||
});
|
||||
});
|
||||
|
||||
/* ============================================================
|
||||
辅助函数
|
||||
============================================================ */
|
||||
function flattenIds(nodes: MenuTable[]): number[] {
|
||||
const ids: number[] = [];
|
||||
const walk = (list: MenuTable[]) => {
|
||||
list.forEach((n) => {
|
||||
ids.push(n.id!);
|
||||
if (n.children) walk(n.children);
|
||||
});
|
||||
};
|
||||
walk(nodes);
|
||||
return ids;
|
||||
}
|
||||
@@ -75,16 +75,25 @@
|
||||
max-height="75vh"
|
||||
>
|
||||
<template #data_scope="{ row }">
|
||||
<ElTag v-if="row?.data_scope === 1" type="primary">仅本人数据权限</ElTag>
|
||||
<ElTag v-else-if="row?.data_scope === 2" type="info">本部门数据权限</ElTag>
|
||||
<ElTag v-else-if="row?.data_scope === 3" type="warning">本部门及以下数据权限</ElTag>
|
||||
<ElTag v-else-if="row?.data_scope === 4" type="success">全部数据权限</ElTag>
|
||||
<ElTag v-else type="danger">自定义数据权限</ElTag>
|
||||
<FaStatusTag v-if="row?.data_scope === 1" type="primary" label="仅本人数据权限" />
|
||||
<FaStatusTag v-else-if="row?.data_scope === 2" type="info" label="本部门数据权限" />
|
||||
<FaStatusTag
|
||||
v-else-if="row?.data_scope === 3"
|
||||
type="warning"
|
||||
label="本部门及以下数据权限"
|
||||
/>
|
||||
<FaStatusTag v-else-if="row?.data_scope === 4" type="success" label="全部数据权限" />
|
||||
<FaStatusTag v-else type="danger" label="自定义数据权限" />
|
||||
</template>
|
||||
<template #depts="{ row }">
|
||||
<template v-if="row?.depts && (row.depts as any[]).length > 0">
|
||||
<template
|
||||
v-if="
|
||||
(row as unknown as RoleTable)?.depts &&
|
||||
(row as unknown as RoleTable).depts!.length > 0
|
||||
"
|
||||
>
|
||||
<ElTag
|
||||
v-for="dept in row.depts as any[]"
|
||||
v-for="dept in (row as unknown as RoleTable).depts!"
|
||||
:key="dept.id"
|
||||
type="info"
|
||||
:style="'margin-right: 4px; margin-bottom: 4px'"
|
||||
@@ -143,6 +152,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from "vue";
|
||||
import { useTable } from "@/hooks/core/useTable";
|
||||
import { useImportExport } from "@/hooks/core/useImportExport";
|
||||
import { useCrudDialog } from "@/hooks/core/useCrudDialog";
|
||||
@@ -150,7 +160,7 @@ import { useTableSelection } from "@/hooks/core/useTableSelection";
|
||||
import { useCrudForm } from "@/hooks/core/useCrudForm";
|
||||
import { confirmDelete, confirmBatchDelete, confirmToggleStatus } from "@/hooks/core/useConfirm";
|
||||
import { cleanEmptyArrayParams, stripPaginationParams } from "@/utils/query";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@utils";
|
||||
import { renderTableOperationCell, type TableOperationAction, resolveStatusColumns } from "@utils";
|
||||
import type { ColumnOption } from "@/types/component";
|
||||
import RoleAPI, {
|
||||
type RoleForm,
|
||||
@@ -161,10 +171,11 @@ import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { useUserStore } from "@stores";
|
||||
import type { IObject } from "@/components/modal/types";
|
||||
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
||||
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import FaForm from "@/components/forms/fa-form/index.vue";
|
||||
import { ElTag, ElMessage } from "element-plus";
|
||||
import type FaForm from "@/components/forms/fa-form/index.vue";
|
||||
import StatusTag from "@/components/others/fa-status-tag/index.vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import FaPermissonDrawer from "./components/FaPermissonDrawer.vue";
|
||||
|
||||
defineOptions({
|
||||
@@ -176,7 +187,7 @@ const { hasAuth } = useAuth();
|
||||
|
||||
type RoleSearchForm = {
|
||||
name?: string;
|
||||
status?: string;
|
||||
status?: number;
|
||||
created_time?: string[];
|
||||
};
|
||||
|
||||
@@ -194,32 +205,22 @@ function buildRoleReplaceParams(p: RoleSearchForm): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function dataScopeTag(row: RoleTable) {
|
||||
const ds = row.data_scope;
|
||||
if (ds === 1) return h(ElTag, { type: "primary" }, () => "仅本人数据权限");
|
||||
if (ds === 2) return h(ElTag, { type: "info" }, () => "本部门数据权限");
|
||||
if (ds === 3) return h(ElTag, { type: "warning" }, () => "本部门及以下数据权限");
|
||||
if (ds === 4) return h(ElTag, { type: "success" }, () => "全部数据权限");
|
||||
return h(ElTag, { type: "danger" }, () => "自定义数据权限");
|
||||
}
|
||||
|
||||
function deptsCell(row: RoleTable) {
|
||||
const list = row.depts;
|
||||
if (!list?.length) {
|
||||
return h("span", { style: { color: "var(--el-text-color-placeholder)" } }, "-");
|
||||
}
|
||||
const tags = list
|
||||
.slice(0, 3)
|
||||
.map((dept) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ key: dept.id, type: "info", style: { marginRight: "4px", marginBottom: "4px" } },
|
||||
() => dept.name ?? ""
|
||||
)
|
||||
);
|
||||
const tags = list.slice(0, 3).map((dept) =>
|
||||
h(StatusTag, {
|
||||
key: dept.id,
|
||||
type: "info",
|
||||
label: dept.name ?? "",
|
||||
style: { marginRight: "4px", marginBottom: "4px" },
|
||||
})
|
||||
);
|
||||
if (list.length > 3) {
|
||||
tags.push(
|
||||
h(ElTag, { type: "info", style: { marginBottom: "4px" } }, () => `+${list.length - 3}`)
|
||||
h(StatusTag, { type: "info", label: `+${list.length - 3}`, style: { marginBottom: "4px" } })
|
||||
);
|
||||
}
|
||||
return h("span", { class: "inline-flex flex-wrap items-center" }, tags);
|
||||
@@ -547,7 +548,7 @@ const {
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
},
|
||||
columnsFactory: (): ColumnOption<RoleTable>[] => [
|
||||
columnsFactory: resolveStatusColumns<RoleTable>(() => [
|
||||
{ type: "selection", width: 48, fixed: "left" },
|
||||
{ type: "globalIndex", width: 56, label: "序号" },
|
||||
{ prop: "name", label: "角色名称", minWidth: 100, showOverflowTooltip: true },
|
||||
@@ -556,7 +557,13 @@ const {
|
||||
prop: "data_scope",
|
||||
label: "数据权限",
|
||||
minWidth: 200,
|
||||
formatter: (row: RoleTable) => dataScopeTag(row),
|
||||
status: {
|
||||
1: { type: "primary", text: "仅本人数据权限" },
|
||||
2: { type: "info", text: "本部门数据权限" },
|
||||
3: { type: "warning", text: "本部门及以下数据权限" },
|
||||
4: { type: "success", text: "全部数据权限" },
|
||||
5: { type: "danger", text: "自定义数据权限" },
|
||||
},
|
||||
},
|
||||
{
|
||||
prop: "depts",
|
||||
@@ -569,10 +576,10 @@ const {
|
||||
prop: "status",
|
||||
label: "状态",
|
||||
width: 88,
|
||||
formatter: (row: RoleTable) =>
|
||||
h(ElTag, { type: row.status === 0 ? "success" : "danger" }, () =>
|
||||
row.status === 0 ? "启用" : "停用"
|
||||
),
|
||||
status: {
|
||||
0: { type: "success", text: "启用" },
|
||||
1: { type: "danger", text: "停用" },
|
||||
},
|
||||
},
|
||||
{ prop: "description", label: "描述", minWidth: 100, showOverflowTooltip: true },
|
||||
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
|
||||
@@ -585,7 +592,7 @@ const {
|
||||
align: "right",
|
||||
formatter: (row: RoleTable) => formatRoleOperationCell(row, opCtx),
|
||||
},
|
||||
],
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -93,27 +93,29 @@
|
||||
max-height="75vh"
|
||||
>
|
||||
<template #ticket_type="{ row }">
|
||||
<ElTag :type="typeTag(row?.ticket_type as string)">
|
||||
{{ typeLabel(row?.ticket_type as string) }}
|
||||
</ElTag>
|
||||
<FaStatusTag
|
||||
:type="typeTag(row?.ticket_type as string)"
|
||||
:label="typeLabel(row?.ticket_type as string)"
|
||||
/>
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<ElTag :type="statusTag(row?.status as string)">
|
||||
{{ statusLabel(row?.status as string) }}
|
||||
</ElTag>
|
||||
<FaStatusTag
|
||||
:type="statusTag(row?.status as string)"
|
||||
:label="statusLabel(row?.status as string)"
|
||||
/>
|
||||
</template>
|
||||
<template #ticket_content>
|
||||
<div class="ticket-html-preview">
|
||||
<ElScrollbar class="ticket-html-preview" view-class="p-3">
|
||||
<template v-if="detailHasRenderableContent">
|
||||
<div v-html="detailContentHtml" />
|
||||
</template>
|
||||
<p v-else class="ticket-html-empty">暂无内容</p>
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
</template>
|
||||
<template #reply_content>
|
||||
<div v-if="detailFormData.reply" class="ticket-html-preview">
|
||||
<ElScrollbar v-if="detailFormData.reply" class="ticket-html-preview" view-class="p-3">
|
||||
<div v-html="sanitizedReply" />
|
||||
</div>
|
||||
</ElScrollbar>
|
||||
<p v-else class="ticket-html-empty">暂无回复</p>
|
||||
</template>
|
||||
</FaDescriptions>
|
||||
@@ -207,16 +209,13 @@ import TicketAPI, {
|
||||
type TicketTable,
|
||||
} from "@/api/module_system/ticket";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@utils";
|
||||
import { renderTableOperationCell, type TableOperationAction, resolveStatusColumns } from "@utils";
|
||||
import type { IObject } from "@/components/modal/types";
|
||||
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
||||
import FaUserTableSelect from "@/components/forms/fa-search-bar/FaUserTableSelect.vue";
|
||||
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import FaForm from "@/components/forms/fa-form/index.vue";
|
||||
import FaWangEditor from "@/components/forms/fa-wang-editor/index.vue";
|
||||
import { ElTag, ElMessage, ElSelect, ElOption, ElRadioGroup, ElRadio } from "element-plus";
|
||||
import { h } from "vue";
|
||||
import type FaForm from "@/components/forms/fa-form/index.vue";
|
||||
import { ElMessage, ElSelect, ElOption, ElRadioGroup, ElRadio, ElScrollbar } from "element-plus";
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
defineOptions({
|
||||
@@ -229,7 +228,7 @@ const { hasAuth } = useAuth();
|
||||
type TicketSearchForm = {
|
||||
title?: string;
|
||||
ticket_type?: string;
|
||||
status?: string;
|
||||
status?: number;
|
||||
created_time?: string[];
|
||||
created_id?: number;
|
||||
assigned_id?: number;
|
||||
@@ -541,7 +540,7 @@ const {
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
},
|
||||
columnsFactory: (): ColumnOption<TicketTable>[] => [
|
||||
columnsFactory: resolveStatusColumns<TicketTable>(() => [
|
||||
{ type: "selection", width: 48, fixed: "left" },
|
||||
{ type: "globalIndex", width: 56, label: "序号" },
|
||||
{ prop: "title", label: "工单标题", minWidth: 180, showOverflowTooltip: true },
|
||||
@@ -549,15 +548,23 @@ const {
|
||||
prop: "ticket_type",
|
||||
label: "工单类型",
|
||||
width: 100,
|
||||
formatter: (row: TicketTable) =>
|
||||
h(ElTag, { type: typeTag(row.ticket_type) }, () => typeLabel(row.ticket_type)),
|
||||
status: {
|
||||
suggestion: { type: "success", text: "建议" },
|
||||
bug: { type: "danger", text: "缺陷" },
|
||||
optimize: { type: "warning", text: "优化" },
|
||||
other: { type: "info", text: "其他" },
|
||||
},
|
||||
},
|
||||
{
|
||||
prop: "status",
|
||||
label: "状态",
|
||||
width: 100,
|
||||
formatter: (row: TicketTable) =>
|
||||
h(ElTag, { type: statusTag(String(row.status)) }, () => statusLabel(String(row.status))),
|
||||
status: {
|
||||
"0": { type: "warning", text: "待处理" },
|
||||
"1": { type: "info", text: "处理中" },
|
||||
"2": { type: "success", text: "已完成" },
|
||||
"3": { type: "info", text: "已关闭" },
|
||||
},
|
||||
},
|
||||
{
|
||||
prop: "assigned_by",
|
||||
@@ -581,7 +588,7 @@ const {
|
||||
align: "right",
|
||||
formatter: (row: TicketTable) => formatTicketOperationCell(row),
|
||||
},
|
||||
],
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -768,8 +775,6 @@ async function handleMoreClick(status: string) {
|
||||
box-sizing: border-box;
|
||||
min-height: 120px;
|
||||
max-height: min(360px, 45vh);
|
||||
padding: 12px 16px;
|
||||
overflow-y: auto;
|
||||
background-color: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: calc(var(--custom-radius) / 3 + 2px);
|
||||
|
||||
@@ -114,19 +114,23 @@
|
||||
</template>
|
||||
<!-- 性别 → 三种状态 Tag -->
|
||||
<template #gender="{ row }">
|
||||
<ElTag v-if="row?.gender === '0'" type="success">男</ElTag>
|
||||
<ElTag v-else-if="row?.gender === '1'" type="warning">女</ElTag>
|
||||
<ElTag v-else type="info">未知</ElTag>
|
||||
<FaStatusTag v-if="row?.gender === '0'" type="success" label="男" />
|
||||
<FaStatusTag v-else-if="row?.gender === '1'" type="warning" label="女" />
|
||||
<FaStatusTag v-else type="info" label="未知" />
|
||||
</template>
|
||||
<!-- 角色 → 数组 join 渲染 -->
|
||||
<template #roles="{ row }">
|
||||
{{ row?.roles ? (row.roles as any[]).map((item: any) => item.name).join("、") : "" }}
|
||||
{{
|
||||
(row as unknown as UserInfo)?.roles
|
||||
? (row as unknown as UserInfo).roles!.map((item) => item.name).join("、")
|
||||
: ""
|
||||
}}
|
||||
</template>
|
||||
<!-- 岗位 → 数组 join 渲染 -->
|
||||
<template #positions="{ row }">
|
||||
{{
|
||||
row?.positions
|
||||
? (row.positions as any[]).map((item: any) => item.name).join("、")
|
||||
(row as unknown as UserInfo)?.positions
|
||||
? (row as unknown as UserInfo).positions!.map((item) => item.name).join("、")
|
||||
: ""
|
||||
}}
|
||||
</template>
|
||||
@@ -209,6 +213,7 @@ defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import { h } from "vue";
|
||||
import { UserFilled } from "@element-plus/icons-vue";
|
||||
import { ElAvatar } from "element-plus";
|
||||
import { DeviceEnum } from "@/enums/settings/device.enum";
|
||||
@@ -224,22 +229,26 @@ import UserAPI, {
|
||||
type UserInfo,
|
||||
type UserPageQuery,
|
||||
} from "@/api/module_system/user";
|
||||
import { formatTree, renderTableOperationCell, type TableOperationAction } from "@utils";
|
||||
import {
|
||||
formatTree,
|
||||
renderTableOperationCell,
|
||||
type TableOperationAction,
|
||||
resolveStatusColumns,
|
||||
} from "@utils";
|
||||
import PositionAPI from "@/api/module_system/position";
|
||||
import DeptAPI from "@/api/module_system/dept";
|
||||
import RoleAPI from "@/api/module_system/role";
|
||||
import { useAppStore, useUserStore } from "@stores";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import type { ColumnOption } from "@/types/component";
|
||||
import FaUserTableSelect from "@/components/forms/fa-search-bar/FaUserTableSelect.vue";
|
||||
import type { DescriptionsItem } from "@/components/others/fa-descriptions/index.vue";
|
||||
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
||||
import type FaForm from "@/components/forms/fa-form/index.vue";
|
||||
import type { IContentConfig, IObject } from "@/components/modal/types";
|
||||
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||
import FaForm from "@/components/forms/fa-form/index.vue";
|
||||
import FaDeptTree from "./components/FaDeptTree.vue";
|
||||
import { ElTag, ElMessage, ElMessageBox } from "element-plus";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
|
||||
const { hasAuth } = useAuth();
|
||||
const appStore = useAppStore();
|
||||
@@ -248,7 +257,7 @@ const userStore = useUserStore();
|
||||
type UserSearchForm = {
|
||||
username?: string;
|
||||
name?: string;
|
||||
status?: string;
|
||||
status?: number;
|
||||
created_id?: number;
|
||||
created_time?: string[];
|
||||
};
|
||||
@@ -592,7 +601,7 @@ const {
|
||||
page_no: 1,
|
||||
page_size: 20,
|
||||
},
|
||||
columnsFactory: (): import("@/types/component").ColumnOption<UserInfo>[] => [
|
||||
columnsFactory: resolveStatusColumns<UserInfo>(() => [
|
||||
{ type: "selection", width: 48, fixed: "left" },
|
||||
{ type: "globalIndex", width: 56, label: "序号" },
|
||||
{
|
||||
@@ -616,10 +625,10 @@ const {
|
||||
prop: "status",
|
||||
label: "状态",
|
||||
width: 88,
|
||||
formatter: (row: UserInfo) =>
|
||||
h(ElTag, { type: row.status === 0 ? "success" : "danger" }, () =>
|
||||
row.status === 0 ? "启用" : "停用"
|
||||
),
|
||||
status: {
|
||||
0: { type: "success", text: "启用" },
|
||||
1: { type: "danger", text: "停用" },
|
||||
},
|
||||
},
|
||||
{
|
||||
prop: "dept",
|
||||
@@ -631,10 +640,9 @@ const {
|
||||
prop: "gender",
|
||||
label: "性别",
|
||||
width: 88,
|
||||
formatter: (row: UserInfo) => {
|
||||
if (row.gender === "0") return h(ElTag, { type: "success" }, () => "男");
|
||||
if (row.gender === "1") return h(ElTag, { type: "warning" }, () => "女");
|
||||
return h(ElTag, { type: "info" }, () => "未知");
|
||||
status: {
|
||||
"0": { type: "success", text: "男" },
|
||||
"1": { type: "warning", text: "女" },
|
||||
},
|
||||
},
|
||||
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
|
||||
@@ -647,7 +655,7 @@ const {
|
||||
align: "right",
|
||||
formatter: (row: UserInfo) => formatUserOperationCell(row, opCtx),
|
||||
},
|
||||
],
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user