mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-03 08:49:12 +00:00
refactor: 大规模代码整理与功能优化
1. 重构后端API路由、CRUD与模块结构,整合日志管理,移除废弃demo代码 2. 优化前端组件类型定义、样式与路由配置,修复权限判断逻辑 3. 调整默认排序规则、滚动条样式与工具类函数,更新依赖与配置文件 4. 修复多处类型不匹配与默认值问题,完善表单与菜单验证逻辑
This commit is contained in:
@@ -25,6 +25,8 @@ import { CanvasRenderer } from "echarts/renderers";
|
||||
|
||||
import { useResizeObserver } from "@vueuse/core";
|
||||
|
||||
defineOptions({ name: "FaECharts" });
|
||||
|
||||
// 按需注册组件
|
||||
echarts.use([
|
||||
RadarChart,
|
||||
@@ -37,11 +39,16 @@ echarts.use([
|
||||
LegendComponent,
|
||||
]);
|
||||
|
||||
const props = defineProps<{
|
||||
interface Props {
|
||||
options: echarts.EChartsCoreOption;
|
||||
width?: string;
|
||||
height?: string;
|
||||
}>();
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
});
|
||||
|
||||
const chartRef = ref<HTMLDivElement | null>(null);
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
|
||||
@@ -160,11 +160,13 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
imageConfig: () => ({ src: "", width: "12rem", bottom: "-3rem", right: "0" }),
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
click: []; // 整体点击事件
|
||||
buttonClick: []; // 按钮点击事件
|
||||
}
|
||||
|
||||
// 定义组件事件
|
||||
const emit = defineEmits<{
|
||||
(e: "click"): void; // 整体点击事件
|
||||
(e: "buttonClick"): void; // 按钮点击事件
|
||||
}>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 计算按钮样式属性
|
||||
const buttonColor = computed(() => props.buttonConfig?.color ?? "#fff");
|
||||
|
||||
@@ -41,15 +41,15 @@ import defaultIcon from "@imgs/3d/icon1.webp";
|
||||
defineOptions({ name: "FaCardBanner" });
|
||||
|
||||
// 定义卡片横幅组件的属性接口
|
||||
interface CardBannerProps {
|
||||
interface Props {
|
||||
/** 高度 */
|
||||
height?: string;
|
||||
/** 图片路径 */
|
||||
image?: string;
|
||||
/** 标题文本 */
|
||||
title: string;
|
||||
title?: string;
|
||||
/** 描述文本 */
|
||||
description: string;
|
||||
description?: string;
|
||||
/** 主按钮配置 */
|
||||
button?: {
|
||||
/** 是否显示 */
|
||||
@@ -75,7 +75,7 @@ interface CardBannerProps {
|
||||
}
|
||||
|
||||
// 定义组件属性默认值
|
||||
withDefaults(defineProps<CardBannerProps>(), {
|
||||
withDefaults(defineProps<Props>(), {
|
||||
height: "100%",
|
||||
image: defaultIcon,
|
||||
title: "",
|
||||
@@ -96,11 +96,13 @@ withDefaults(defineProps<CardBannerProps>(), {
|
||||
}),
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
click: []; // 主按钮点击事件
|
||||
cancel: []; // 取消按钮点击事件
|
||||
}
|
||||
|
||||
// 定义组件事件
|
||||
const emit = defineEmits<{
|
||||
(e: "click"): void; // 主按钮点击事件
|
||||
(e: "cancel"): void; // 取消按钮点击事件
|
||||
}>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 主按钮点击处理函数
|
||||
const handleClick = () => {
|
||||
|
||||
@@ -13,7 +13,9 @@ interface Props {
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
withDefaults(defineProps<Props>(), {
|
||||
icon: "",
|
||||
});
|
||||
|
||||
const attrs = useAttrs();
|
||||
|
||||
|
||||
@@ -42,10 +42,25 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { commentList, Comment } from "@/mock/temp/commentDetail";
|
||||
import { type Comment } from "@/mock/temp/commentDetail";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
const comments = commentList;
|
||||
defineOptions({ name: "FaCommentWidget" });
|
||||
|
||||
interface Props {
|
||||
comments?: Comment[];
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
comments: () => [],
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
"add-comment": [comment: Comment];
|
||||
"add-reply": [commentId: number, author: string, content: string];
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const newComment = ref<Partial<Comment>>({
|
||||
author: "",
|
||||
@@ -60,7 +75,7 @@ const addComment = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
comments.value.push({
|
||||
emit("add-comment", {
|
||||
id: Date.now(),
|
||||
author: newComment.value.author.trim(),
|
||||
content: newComment.value.content.trim(),
|
||||
@@ -79,34 +94,12 @@ const addReply = (commentId: number, replyAuthor: string, replyContent: string)
|
||||
return;
|
||||
}
|
||||
|
||||
const comment = findComment(comments.value, commentId);
|
||||
if (comment) {
|
||||
comment.replies.push({
|
||||
id: Date.now(),
|
||||
author: replyAuthor.trim(),
|
||||
content: replyContent.trim(),
|
||||
timestamp: new Date().toISOString(),
|
||||
replies: [],
|
||||
});
|
||||
showReplyForm.value = null;
|
||||
ElMessage.success("回复发布成功");
|
||||
}
|
||||
emit("add-reply", commentId, replyAuthor.trim(), replyContent.trim());
|
||||
showReplyForm.value = null;
|
||||
ElMessage.success("回复发布成功");
|
||||
};
|
||||
|
||||
const toggleReply = (commentId: number) => {
|
||||
showReplyForm.value = showReplyForm.value === commentId ? null : commentId;
|
||||
};
|
||||
|
||||
const findComment = (comments: Comment[], commentId: number): Comment | undefined => {
|
||||
for (const comment of comments) {
|
||||
if (comment.id === commentId) {
|
||||
return comment;
|
||||
}
|
||||
const found = findComment(comment.replies, commentId);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
import AppConfig from "@/config";
|
||||
import { ref } from "vue";
|
||||
|
||||
defineOptions({ name: "FaCommentItem" });
|
||||
|
||||
interface Comment {
|
||||
id: number;
|
||||
author: string;
|
||||
@@ -69,15 +71,20 @@ interface Comment {
|
||||
replies: Comment[];
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
interface Props {
|
||||
/** 评论内容 */
|
||||
comment: Comment;
|
||||
showReplyForm: number | null;
|
||||
}>();
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "toggle-reply", commentId: number): void;
|
||||
(event: "add-reply", commentId: number, replyAuthor: string, replyContent: string): void;
|
||||
}>();
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
"toggle-reply": [commentId: number];
|
||||
"add-reply": [commentId: number, replyAuthor: string, replyContent: string];
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const replyAuthor = ref("");
|
||||
const replyContent = ref("");
|
||||
|
||||
@@ -30,32 +30,33 @@ import { computed } from "vue";
|
||||
|
||||
defineOptions({ name: "FaCardList" });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
items: any[];
|
||||
total?: number;
|
||||
pageSize?: number;
|
||||
currentPage?: number;
|
||||
loading?: boolean;
|
||||
columns?: number;
|
||||
keyField?: string;
|
||||
emptyText?: string;
|
||||
}>(),
|
||||
{
|
||||
total: 0,
|
||||
pageSize: 12,
|
||||
currentPage: 1,
|
||||
loading: false,
|
||||
columns: 4,
|
||||
keyField: "id",
|
||||
emptyText: "暂无数据",
|
||||
}
|
||||
);
|
||||
interface Props {
|
||||
items: any[];
|
||||
total?: number;
|
||||
pageSize?: number;
|
||||
currentPage?: number;
|
||||
loading?: boolean;
|
||||
columns?: number;
|
||||
keyField?: string;
|
||||
emptyText?: string;
|
||||
}
|
||||
|
||||
defineEmits<{
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
total: 0,
|
||||
pageSize: 12,
|
||||
currentPage: 1,
|
||||
loading: false,
|
||||
columns: 4,
|
||||
keyField: "id",
|
||||
emptyText: "暂无数据",
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
pageChange: [page: number];
|
||||
itemClick: [item: any];
|
||||
}>();
|
||||
}
|
||||
|
||||
defineEmits<Emits>();
|
||||
|
||||
const gridClass = computed(() => {
|
||||
const cols = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- 数据列表卡片 -->
|
||||
<template>
|
||||
<div class="fa-card p-5 h-[28.2rem] overflow-hidden">
|
||||
<div class="fa-card p-5 h-110 overflow-hidden">
|
||||
<div class="pb-3.5">
|
||||
<p class="text-lg font-medium">{{ title }}</p>
|
||||
<p class="text-sm text-g-600">{{ subtitle }}</p>
|
||||
@@ -66,10 +66,12 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
const maxHeight = computed(() => `${ITEM_HEIGHT * props.maxCount}px`);
|
||||
|
||||
const emit = defineEmits<{
|
||||
interface Emits {
|
||||
/** 点击更多按钮事件 */
|
||||
(e: "more"): void;
|
||||
}>();
|
||||
more: [];
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const handleMore = () => emit("more");
|
||||
</script>
|
||||
|
||||
@@ -53,9 +53,9 @@ defineOptions({ name: "FaImageCard" });
|
||||
|
||||
interface Props {
|
||||
/** 图片地址 */
|
||||
imageUrl: string;
|
||||
imageUrl?: string;
|
||||
/** 标题 */
|
||||
title: string;
|
||||
title?: string;
|
||||
/** 分类 */
|
||||
category?: string;
|
||||
/** 阅读时间 */
|
||||
@@ -78,9 +78,11 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
date: "",
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "click", card: Props): void;
|
||||
}>();
|
||||
interface Emits {
|
||||
click: [card: Props];
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const handleClick = () => {
|
||||
emit("click", props);
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: "FaStatsCard" });
|
||||
|
||||
interface StatsCardProps {
|
||||
interface Props {
|
||||
/** 盒子样式 */
|
||||
boxStyle?: string;
|
||||
/** 图标 */
|
||||
@@ -57,9 +57,13 @@ interface StatsCardProps {
|
||||
textColor?: string;
|
||||
/** 是否显示箭头 */
|
||||
showArrow?: boolean;
|
||||
/** 图标大小 */
|
||||
iconSize?: number;
|
||||
/** 图标背景圆角 */
|
||||
iconBgRadius?: number;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<StatsCardProps>(), {
|
||||
withDefaults(defineProps<Props>(), {
|
||||
iconSize: 30,
|
||||
iconBgRadius: 50,
|
||||
decimals: 0,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- 时间轴列表卡片 -->
|
||||
<template>
|
||||
<div class="fa-card p-5 h-[28.2rem] overflow-hidden">
|
||||
<div class="fa-card p-5 h-110 overflow-hidden">
|
||||
<div class="pb-3.5">
|
||||
<p class="text-lg font-medium">{{ title }}</p>
|
||||
<p class="text-sm text-g-600">{{ subtitle }}</p>
|
||||
@@ -50,7 +50,7 @@ interface Props {
|
||||
/** 时间轴列表数据 */
|
||||
list: TimelineItem[];
|
||||
/** 标题 */
|
||||
title: string;
|
||||
title?: string;
|
||||
/** 副标题 */
|
||||
subtitle?: string;
|
||||
/** 最大显示数量 */
|
||||
|
||||
@@ -34,11 +34,13 @@ const props = withDefaults(defineProps<MapChartProps & { dynamic?: boolean }>(),
|
||||
dynamic: false,
|
||||
});
|
||||
|
||||
// 定义 emit
|
||||
const emit = defineEmits<{
|
||||
interface Emits {
|
||||
renderComplete: [];
|
||||
regionClick: [region: { name: string; adcode: string; level: string }];
|
||||
}>();
|
||||
}
|
||||
|
||||
// 定义 emit
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 检查是否为空数据
|
||||
const isEmpty = computed(() => {
|
||||
|
||||
@@ -45,9 +45,11 @@ const hasAnyAuthItem = computed(() => {
|
||||
return props.list.some((item) => !item.auth || hasAuth(item.auth));
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "click", item: ButtonMoreItem): void;
|
||||
}>();
|
||||
interface Emits {
|
||||
click: [item: ButtonMoreItem];
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const handleClick = (item: ButtonMoreItem) => {
|
||||
emit("click", item);
|
||||
|
||||
@@ -30,9 +30,11 @@ interface Props {
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "click"): void;
|
||||
}>();
|
||||
interface Emits {
|
||||
click: [];
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 默认按钮配置
|
||||
const defaultButtons = {
|
||||
|
||||
@@ -46,7 +46,7 @@ defineOptions({ name: "FaDragVerify" });
|
||||
const emit = defineEmits(["handlerMove", "update:value", "passCallback"]);
|
||||
|
||||
// 组件属性接口定义
|
||||
interface PropsType {
|
||||
interface Props {
|
||||
/** 是否通过验证 */
|
||||
value: boolean;
|
||||
/** 组件宽度 */
|
||||
@@ -80,7 +80,7 @@ interface PropsType {
|
||||
}
|
||||
|
||||
// 属性默认值设置
|
||||
const props = withDefaults(defineProps<PropsType>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
value: false,
|
||||
width: "100%",
|
||||
height: 40,
|
||||
|
||||
@@ -46,7 +46,7 @@ interface ColumnConfig {
|
||||
}
|
||||
|
||||
/** 导出配置选项 */
|
||||
interface ExportOptions {
|
||||
interface Props {
|
||||
/** 数据源 */
|
||||
data: ExportData[];
|
||||
/** 文件名(不含扩展名) */
|
||||
@@ -90,7 +90,7 @@ interface ExportOptions {
|
||||
};
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ExportOptions>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
filename: () => `export_${new Date().toISOString().slice(0, 10)}`,
|
||||
sheetName: "Sheet1",
|
||||
type: "primary",
|
||||
@@ -108,12 +108,14 @@ const props = withDefaults(defineProps<ExportOptions>(), {
|
||||
workbookOptions: () => ({}),
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
interface Emits {
|
||||
"before-export": [data: ExportData[]];
|
||||
"export-success": [filename: string, rowCount: number];
|
||||
"export-error": [error: ExportError];
|
||||
"export-progress": [progress: number];
|
||||
}>();
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
/** 导出错误类型 */
|
||||
class ExportError extends Error {
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
<div class="inline-block">
|
||||
<ElUpload
|
||||
:auto-upload="false"
|
||||
accept=".xlsx, .xls"
|
||||
:accept="accept"
|
||||
:show-file-list="false"
|
||||
:disabled="disabled"
|
||||
@change="handleFileChange"
|
||||
>
|
||||
<ElButton type="primary" v-ripple>
|
||||
<slot>导入 Excel</slot>
|
||||
<ElButton type="primary" v-ripple :loading="loading">
|
||||
<slot>{{ buttonText }}</slot>
|
||||
</ElButton>
|
||||
</ElUpload>
|
||||
</div>
|
||||
@@ -20,6 +21,24 @@ import type { UploadFile } from "element-plus";
|
||||
|
||||
defineOptions({ name: "FaExcelImport" });
|
||||
|
||||
interface Props {
|
||||
/** 接受的文件类型 */
|
||||
accept?: string;
|
||||
/** 按钮文本 */
|
||||
buttonText?: string;
|
||||
/** 加载状态 */
|
||||
loading?: boolean;
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
accept: ".xlsx, .xls",
|
||||
buttonText: "导入 Excel",
|
||||
loading: false,
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
// Excel 导入工具函数
|
||||
async function importExcel(file: File): Promise<Array<Record<string, unknown>>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -44,10 +63,12 @@ async function importExcel(file: File): Promise<Array<Record<string, unknown>>>
|
||||
}
|
||||
|
||||
// 定义 emits
|
||||
const emit = defineEmits<{
|
||||
interface Emits {
|
||||
"import-success": [data: Array<Record<string, unknown>>];
|
||||
"import-error": [error: Error];
|
||||
}>();
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 处理文件导入
|
||||
const handleFileChange = async (uploadFile: UploadFile) => {
|
||||
|
||||
@@ -277,7 +277,7 @@ export interface FormItem {
|
||||
}
|
||||
|
||||
// 表单配置
|
||||
interface FormProps {
|
||||
interface Props {
|
||||
/** 表单数据 */
|
||||
items: FormItem[];
|
||||
/** 每列的宽度(基于 24 格布局) */
|
||||
@@ -319,7 +319,7 @@ interface SanitizeOutputOptions {
|
||||
keepFalse: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<FormProps>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
items: () => [],
|
||||
span: 6,
|
||||
gutter: 12,
|
||||
@@ -334,12 +334,12 @@ const props = withDefaults(defineProps<FormProps>(), {
|
||||
maxHeight: "75vh",
|
||||
});
|
||||
|
||||
interface FormEmits {
|
||||
interface Emits {
|
||||
reset: [];
|
||||
submit: [Record<string, any>];
|
||||
}
|
||||
|
||||
const emit = defineEmits<FormEmits>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const modelValue = defineModel<Record<string, any>>({ default: {} });
|
||||
const initialModelValue = ref<Record<string, any>>({});
|
||||
@@ -520,9 +520,20 @@ const getSanitizedOutput = () => {
|
||||
};
|
||||
|
||||
const getProps = (item: FormItem) => {
|
||||
if (item.props) return item.props;
|
||||
const props = { ...item };
|
||||
rootProps.forEach((key) => delete (props as Record<string, any>)[key]);
|
||||
let props: Record<string, any>;
|
||||
if (item.props) {
|
||||
props = { ...item.props };
|
||||
} else {
|
||||
props = { ...item };
|
||||
rootProps.forEach((key) => delete props[key]);
|
||||
}
|
||||
|
||||
// 对于日期选择器组件,确保 type 被传递给 FaDatePicker
|
||||
const datePickerTypes = ["date", "daterange", "datetime", "datetimerange", "monthrange"];
|
||||
if (item.type && datePickerTypes.includes(item.type) && !props.type) {
|
||||
props.type = item.type;
|
||||
}
|
||||
|
||||
return props;
|
||||
};
|
||||
|
||||
|
||||
@@ -31,24 +31,29 @@ type DatePickerType =
|
||||
| "daterange"
|
||||
| "monthrange";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: any;
|
||||
type?: DatePickerType;
|
||||
format?: string;
|
||||
rangeSeparator?: string;
|
||||
startPlaceholder?: string;
|
||||
endPlaceholder?: string;
|
||||
}>(),
|
||||
{
|
||||
type: "datetimerange",
|
||||
rangeSeparator: "至",
|
||||
startPlaceholder: "开始日期",
|
||||
endPlaceholder: "结束日期",
|
||||
}
|
||||
);
|
||||
defineOptions({ name: "FaDatePicker" });
|
||||
|
||||
const emit = defineEmits(["update:model-value"]);
|
||||
interface Props {
|
||||
modelValue?: any;
|
||||
type?: DatePickerType;
|
||||
format?: string;
|
||||
rangeSeparator?: string;
|
||||
startPlaceholder?: string;
|
||||
endPlaceholder?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
type: "datetimerange",
|
||||
rangeSeparator: "至",
|
||||
startPlaceholder: "开始日期",
|
||||
endPlaceholder: "结束日期",
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: "update:model-value", value: any): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
/** 是否为范围选择模式 */
|
||||
const isRangeType = computed(() => {
|
||||
|
||||
@@ -47,22 +47,23 @@ import {
|
||||
|
||||
defineOptions({ name: "FaSearchBarWithAudit", inheritAttrs: false });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 仅业务条件表单项,不含审计四字段 */
|
||||
items: SearchFormItem[];
|
||||
/** 为 false 时与原生 FaSearchBar 一致,仅使用 `items` */
|
||||
includeAudit?: boolean;
|
||||
/** 传给 getAuditSearchFormItems 的选项 */
|
||||
auditItemOptions?: GetAuditSearchFormItemsOptions;
|
||||
}>(),
|
||||
{ includeAudit: true }
|
||||
);
|
||||
interface Props {
|
||||
/** 仅业务条件表单项,不含审计四字段 */
|
||||
items: SearchFormItem[];
|
||||
/** 为 false 时与原生 FaSearchBar 一致,仅使用 `items` */
|
||||
includeAudit?: boolean;
|
||||
/** 传给 getAuditSearchFormItems 的选项 */
|
||||
auditItemOptions?: GetAuditSearchFormItemsOptions;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
const props = withDefaults(defineProps<Props>(), { includeAudit: true });
|
||||
|
||||
interface Emits {
|
||||
search: [Record<string, any>];
|
||||
reset: [];
|
||||
}>();
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const modelValue = defineModel<Record<string, any>>({ default: () => ({}) });
|
||||
const attrs = useAttrs();
|
||||
|
||||
@@ -18,14 +18,20 @@
|
||||
import type { ISelectConfig } from "@/components/others/fa-table-select/index.vue";
|
||||
import UserAPI, { UserPageQuery } from "@/api/module_system/user";
|
||||
|
||||
// 父组件双向绑定的值(选中用户ID)
|
||||
const props = defineProps<{ modelValue?: number }>();
|
||||
defineOptions({ name: "FaUserTableSelect" });
|
||||
|
||||
// 事件向上派发,支持父组件监听和 v-model(仅回传选中用户ID)
|
||||
const emit = defineEmits<{
|
||||
interface Props {
|
||||
modelValue?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
interface Emits {
|
||||
confirmClick: [data: IUser[]];
|
||||
"update:modelValue": [val: number | undefined];
|
||||
}>();
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const selectConfig: ISelectConfig = {
|
||||
pk: "id",
|
||||
|
||||
@@ -190,7 +190,7 @@ export interface SearchFormItem {
|
||||
}
|
||||
|
||||
// 表单配置
|
||||
interface SearchBarProps {
|
||||
interface Props {
|
||||
/** 表单数据 */
|
||||
items: SearchFormItem[];
|
||||
/** 每列的宽度(基于 24 格布局) */
|
||||
@@ -234,7 +234,7 @@ interface SanitizeOutputOptions {
|
||||
keepFalse: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<SearchBarProps>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
items: () => [],
|
||||
span: 6,
|
||||
gutter: 12,
|
||||
@@ -250,12 +250,12 @@ const props = withDefaults(defineProps<SearchBarProps>(), {
|
||||
sanitizeOutput: () => ({}),
|
||||
});
|
||||
|
||||
interface SearchBarEmits {
|
||||
interface Emits {
|
||||
reset: [];
|
||||
search: [Record<string, any>];
|
||||
}
|
||||
|
||||
const emit = defineEmits<SearchBarEmits>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const modelValue = defineModel<Record<string, any>>({ default: {} });
|
||||
const initialModelValue = ref<Record<string, any>>({});
|
||||
|
||||
@@ -75,16 +75,28 @@
|
||||
<template #footer>
|
||||
<div class="dialog-footer box-border flex-c border-t-d pt-4.5 pb-1">
|
||||
<div class="flex-cc">
|
||||
<FaSvgIcon icon="fluent:arrow-enter-left-20-filled" class="keyboard" />
|
||||
<FaSvgIcon
|
||||
icon="fluent:arrow-enter-left-20-filled"
|
||||
class="mr-2 box-border h-5 w-5.5 rounded border border-g-400 px-1 text-g-500 shadow-[0_2px_0_var(--default-border-dashed)]"
|
||||
/>
|
||||
<span class="mr-3.5 text-xs text-g-700">{{ $t("search.selectKeydown") }}</span>
|
||||
</div>
|
||||
<div class="flex-c">
|
||||
<FaSvgIcon icon="ri:arrow-up-wide-fill" class="keyboard" />
|
||||
<FaSvgIcon icon="ri:arrow-down-wide-fill" class="keyboard" />
|
||||
<FaSvgIcon
|
||||
icon="ri:arrow-up-wide-fill"
|
||||
class="mr-2 box-border h-5 w-5.5 rounded border border-g-400 px-1 text-g-500 shadow-[0_2px_0_var(--default-border-dashed)]"
|
||||
/>
|
||||
<FaSvgIcon
|
||||
icon="ri:arrow-down-wide-fill"
|
||||
class="mr-2 box-border h-5 w-5.5 rounded border border-g-400 px-1 text-g-500 shadow-[0_2px_0_var(--default-border-dashed)]"
|
||||
/>
|
||||
<span class="mr-3.5 text-xs text-g-700">{{ $t("search.switchKeydown") }}</span>
|
||||
</div>
|
||||
<div class="flex-c">
|
||||
<i class="keyboard w-8! flex-cc"><p class="text-[10px] font-medium">ESC</p></i>
|
||||
<i
|
||||
class="mr-2 box-border h-5 rounded border border-g-400 px-1 text-g-500 shadow-[0_2px_0_var(--default-border-dashed)] w-8! flex-cc"
|
||||
><p class="text-[10px] font-medium">ESC</p></i
|
||||
>
|
||||
<span class="mr-3.5 text-xs text-g-700">{{ $t("search.exitKeydown") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -409,21 +421,3 @@ const highlightOnHoverHistory = (index: number) => {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
@reference '@styles/core/tailwind.css';
|
||||
|
||||
.keyboard {
|
||||
@apply mr-2
|
||||
box-border
|
||||
h-5
|
||||
w-5.5
|
||||
rounded
|
||||
border
|
||||
border-g-400
|
||||
px-1
|
||||
text-g-500
|
||||
shadow-[0_2px_0_var(--default-border-dashed)]
|
||||
last-of-type:mr-1.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -249,8 +249,17 @@ const configStore = useConfigStore();
|
||||
const activeTabRef = ref("apiWhitelist");
|
||||
|
||||
// 与父组件的 v-model 同步
|
||||
const props = defineProps<{ modelValue: boolean }>();
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
interface Emits {
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
const drawerVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val: boolean) => emit("update:modelValue", val),
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<!-- 租户切换器(顶栏版):头像左侧,始终可见 -->
|
||||
<template>
|
||||
<ElDropdown
|
||||
v-if="tenantList.length > 1"
|
||||
trigger="click"
|
||||
@command="handleSwitch"
|
||||
placement="bottom"
|
||||
>
|
||||
<span class="tenant-btn">
|
||||
<span class="tenant-icon">🏢</span>
|
||||
<span class="tenant-name">{{ currentTenantName }}</span>
|
||||
<FaSvgIcon icon="ri:arrow-down-s-line" class="arrow" />
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem
|
||||
v-for="t in tenantList"
|
||||
:key="t.id"
|
||||
:command="t.id"
|
||||
:class="{ 'is-active': t.id === currentTenant?.id }"
|
||||
>
|
||||
<span class="dropdown-row">
|
||||
<span>{{ t.name }}</span>
|
||||
<FaSvgIcon v-if="t.id === currentTenant?.id" icon="ri:check-line" class="check" />
|
||||
</span>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useUserStore } from "@stores";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
defineOptions({ name: "FaTenantSwitcher" });
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { tenantList, currentTenant } = storeToRefs(userStore);
|
||||
|
||||
const currentTenantName = computed(
|
||||
() => currentTenant.value?.name || tenantList.value[0]?.name || "—"
|
||||
);
|
||||
|
||||
async function handleSwitch(tenantId: number) {
|
||||
if (tenantId === currentTenant.value?.id) return;
|
||||
try {
|
||||
await userStore.selectTenant(tenantId);
|
||||
setTimeout(() => window.location.reload(), 200);
|
||||
} catch {
|
||||
// 静默失败
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tenant-btn {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
max-width: 160px;
|
||||
height: 30px;
|
||||
padding: 0 10px 0 6px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-primary);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: var(--fa-gray-100);
|
||||
border: 1px solid var(--fa-gray-300);
|
||||
border-radius: 6px;
|
||||
transition:
|
||||
background 0.15s,
|
||||
border-color 0.15s;
|
||||
}
|
||||
|
||||
.tenant-btn:hover {
|
||||
background: var(--fa-gray-200);
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
}
|
||||
|
||||
.tenant-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tenant-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.dropdown-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.check {
|
||||
flex-shrink: 0;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -58,7 +58,7 @@
|
||||
<ul class="py-4 mt-3 border-t border-g-300/80">
|
||||
<li
|
||||
v-if="tenantList.length > 1"
|
||||
class="btn-item flex-col! items-start! mb-4 p-2! bg-(--fa-gray-100) rounded-lg"
|
||||
class="flex select-none cursor-pointer last:mb-0 hover:bg-(--fa-gray-200) flex-col! items-start! mb-4 p-2! rounded-lg bg-(--fa-gray-100)"
|
||||
>
|
||||
<span class="text-xs text-g-500 mb-2 block w-full">当前租户</span>
|
||||
<ElDropdown trigger="click" @command="handleTenantSwitch">
|
||||
@@ -87,28 +87,46 @@
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</li>
|
||||
<li class="btn-item" @click="goPage('/fastlink/profile')">
|
||||
<FaSvgIcon icon="ri:user-3-line" />
|
||||
<span>{{ $t("topBar.user.userCenter") }}</span>
|
||||
<li
|
||||
class="flex items-center p-2 mb-3 select-none rounded-md cursor-pointer last:mb-0 hover:bg-(--fa-gray-200)"
|
||||
@click="goPage('/fastlink/profile')"
|
||||
>
|
||||
<FaSvgIcon icon="ri:user-3-line" class="mr-2 text-base" />
|
||||
<span class="text-sm">{{ $t("topBar.user.userCenter") }}</span>
|
||||
</li>
|
||||
<li class="btn-item" @click="openParamConfig">
|
||||
<FaSvgIcon icon="ri:settings-3-line" />
|
||||
<span>{{ $t("topBar.user.paramConfig") }}</span>
|
||||
<li
|
||||
class="flex items-center p-2 mb-3 select-none rounded-md cursor-pointer last:mb-0 hover:bg-(--fa-gray-200)"
|
||||
@click="openParamConfig"
|
||||
>
|
||||
<FaSvgIcon icon="ri:settings-3-line" class="mr-2 text-base" />
|
||||
<span class="text-sm">{{ $t("topBar.user.paramConfig") }}</span>
|
||||
</li>
|
||||
<li class="btn-item" @click="toGithub()">
|
||||
<FaSvgIcon icon="ri:github-line" />
|
||||
<span>{{ $t("topBar.user.github") }}</span>
|
||||
<li
|
||||
class="flex items-center p-2 mb-3 select-none rounded-md cursor-pointer last:mb-0 hover:bg-(--fa-gray-200)"
|
||||
@click="toGithub()"
|
||||
>
|
||||
<FaSvgIcon icon="ri:github-line" class="mr-2 text-base" />
|
||||
<span class="text-sm">{{ $t("topBar.user.github") }}</span>
|
||||
</li>
|
||||
<li class="btn-item" @click="toGitee">
|
||||
<FaSvgIcon icon="ri:git-branch-line" />
|
||||
<span>{{ $t("topBar.user.gitee") }}</span>
|
||||
<li
|
||||
class="flex items-center p-2 mb-3 select-none rounded-md cursor-pointer last:mb-0 hover:bg-(--fa-gray-200)"
|
||||
@click="toGitee"
|
||||
>
|
||||
<FaSvgIcon icon="ri:git-branch-line" class="mr-2 text-base" />
|
||||
<span class="text-sm">{{ $t("topBar.user.gitee") }}</span>
|
||||
</li>
|
||||
<li class="btn-item" @click="lockScreen()">
|
||||
<FaSvgIcon icon="ri:lock-line" />
|
||||
<span>{{ $t("topBar.user.lockScreen") }}</span>
|
||||
<li
|
||||
class="flex items-center p-2 mb-3 select-none rounded-md cursor-pointer last:mb-0 hover:bg-(--fa-gray-200)"
|
||||
@click="lockScreen()"
|
||||
>
|
||||
<FaSvgIcon icon="ri:lock-line" class="mr-2 text-base" />
|
||||
<span class="text-sm">{{ $t("topBar.user.lockScreen") }}</span>
|
||||
</li>
|
||||
<div class="w-full h-px my-2 bg-g-300/80"></div>
|
||||
<li class="btn-item btn-item--logout" @click="handleLogout">
|
||||
<li
|
||||
class="flex p-2 select-none rounded-md cursor-pointer last:mb-0 hover:bg-(--fa-gray-200) justify-center mt-5 mb-0 py-1.5 text-xs border border-g-400 hover:text-(--el-color-danger) hover:border-(--el-color-danger-light-3)"
|
||||
@click="handleLogout"
|
||||
>
|
||||
{{ $t("topBar.user.logout") }}
|
||||
</li>
|
||||
</ul>
|
||||
@@ -213,37 +231,6 @@ function closeUserMenu(): void {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '@styles/core/tailwind.css';
|
||||
|
||||
@layer components {
|
||||
.btn-item {
|
||||
@apply flex items-center p-2 mb-3 select-none rounded-md cursor-pointer last:mb-0;
|
||||
|
||||
span {
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
.fa-svg-icon {
|
||||
@apply mr-2 text-base;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--fa-gray-200);
|
||||
}
|
||||
}
|
||||
|
||||
/** 退出:沿用菜单项 hover 底纹,并加重边框/文字,避免仅 shadow 几乎无反馈 */
|
||||
.btn-item.btn-item--logout {
|
||||
@apply justify-center mt-5 mb-0 py-1.5 text-xs border border-g-400;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-danger);
|
||||
background-color: var(--fa-gray-200);
|
||||
border-color: var(--el-color-danger-light-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ElPopover 基于 Tooltip:触发层默认 inline-block,与顶栏 flex 图标中线对齐 */
|
||||
.fa-user-menu .el-tooltip__trigger {
|
||||
display: inline-flex !important;
|
||||
|
||||
+16
-16
@@ -45,29 +45,29 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, type PropType } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { AppRouteRecord } from "@/types/router";
|
||||
import { handleMenuJump, formatMenuTitle } from "@utils";
|
||||
|
||||
defineOptions({ name: "FaHorizontalSubmenu" });
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object as PropType<AppRouteRecord>,
|
||||
required: true,
|
||||
},
|
||||
theme: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
isMobile: Boolean,
|
||||
level: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
interface Props {
|
||||
item: AppRouteRecord;
|
||||
theme?: Record<string, any>;
|
||||
isMobile?: boolean;
|
||||
level?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
theme: () => ({}),
|
||||
level: 0,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close"]);
|
||||
interface Emits {
|
||||
close: [];
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 过滤后的子菜单项(不包含隐藏的)
|
||||
const filteredChildren = computed(() => {
|
||||
|
||||
@@ -223,28 +223,6 @@ const initScrollState = (): void => {
|
||||
onMounted(initScrollState);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '@styles/core/tailwind.css';
|
||||
|
||||
.button-arrow {
|
||||
@apply absolute
|
||||
top-1/2
|
||||
z-2
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
size-7.5
|
||||
text-g-600
|
||||
cursor-pointer
|
||||
rounded
|
||||
transition-all
|
||||
duration-300
|
||||
-translate-y-1/2
|
||||
hover:text-g-900
|
||||
hover:bg-g-200;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-scrollbar__bar.is-horizontal) {
|
||||
bottom: 5px;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!-- 通知组件 -->
|
||||
<template>
|
||||
<div
|
||||
class="fa-notification-panel fa-card-sm shadow-xl!"
|
||||
class="absolute top-14.5 right-5 w-90 h-125 overflow-hidden transition-all duration-300 origin-top will-change-[top,left] max-[640px]:top-[65px] max-[640px]:right-0 max-[640px]:w-full max-[640px]:h-[80vh] fa-card-sm shadow-xl!"
|
||||
:style="{
|
||||
transform: show ? 'scaleY(1)' : 'scaleY(0.9)',
|
||||
opacity: show ? 1 : 0,
|
||||
@@ -156,13 +156,17 @@ type NoticeType = "email" | "message" | "collection" | "user" | "notice";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
interface Props {
|
||||
value: boolean;
|
||||
}>();
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
interface Emits {
|
||||
"update:value": [value: boolean];
|
||||
}>();
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const show = ref(false);
|
||||
const visible = ref(false);
|
||||
@@ -390,25 +394,6 @@ watch(
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '@styles/core/tailwind.css';
|
||||
|
||||
.fa-notification-panel {
|
||||
@apply absolute
|
||||
top-14.5
|
||||
right-5
|
||||
w-90
|
||||
h-125
|
||||
overflow-hidden
|
||||
transition-all
|
||||
duration-300
|
||||
origin-top
|
||||
will-change-[top,left]
|
||||
max-[640px]:top-[65px]
|
||||
max-[640px]:right-0
|
||||
max-[640px]:w-full
|
||||
max-[640px]:h-[80vh];
|
||||
}
|
||||
|
||||
.bar-active {
|
||||
color: var(--theme-color) !important;
|
||||
border-bottom: 2px solid var(--theme-color);
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
:bottom="28"
|
||||
class="z-90"
|
||||
>
|
||||
<FaSvgIcon icon="ri:arrow-up-circle-line" class="text-2xl text-g-600" />
|
||||
<FaSvgIcon icon="ri:arrow-up-circle-line" class="text-2xl text-theme" />
|
||||
</ElBacktop>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -147,6 +147,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Lock } from "@element-plus/icons-vue";
|
||||
|
||||
defineOptions({ name: "FaScreenLock" });
|
||||
import type { FormInstance, FormRules } from "element-plus";
|
||||
import { ElInput } from "element-plus";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
@@ -34,7 +34,9 @@ interface Props {
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
open: false,
|
||||
});
|
||||
|
||||
// 使用设置面板逻辑
|
||||
const settingsPanel = useSettingsPanel();
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@stores";
|
||||
|
||||
defineOptions({ name: "FaBasicSettings" });
|
||||
import { useSettingsConfig } from "../composables/useSettingsConfig";
|
||||
import { useSettingsHandlers } from "../composables/useSettingsHandlers";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@stores";
|
||||
|
||||
defineOptions({ name: "FaBoxStyleSettings" });
|
||||
import { useSettingsConfig } from "../composables/useSettingsConfig";
|
||||
import { useSettingsHandlers } from "../composables/useSettingsHandlers";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@stores";
|
||||
|
||||
defineOptions({ name: "FaColorSettings" });
|
||||
import { useSettingsConfig } from "../composables/useSettingsConfig";
|
||||
import { useSettingsHandlers } from "../composables/useSettingsHandlers";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@stores";
|
||||
|
||||
defineOptions({ name: "FaContainerSettings" });
|
||||
import { useSettingsConfig } from "../composables/useSettingsConfig";
|
||||
import { useSettingsHandlers } from "../composables/useSettingsHandlers";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@stores";
|
||||
|
||||
defineOptions({ name: "FaMenuLayoutSettings" });
|
||||
import { useSettingsConfig } from "../composables/useSettingsConfig";
|
||||
import { useSettingsState } from "../composables/useSettingsState";
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppConfig from "@/config";
|
||||
|
||||
defineOptions({ name: "FaMenuStyleSettings" });
|
||||
import { MenuTypeEnum, type MenuThemeEnum } from "@/enums/appEnum";
|
||||
import { useSettingsStore } from "@stores";
|
||||
|
||||
|
||||
@@ -8,10 +8,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: "FaSectionTitle" });
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
style?: Record<string, any>;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
withDefaults(defineProps<Props>(), {
|
||||
style: () => ({}),
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: "FaSettingDrawer" });
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
}
|
||||
@@ -29,7 +31,7 @@ interface Emits {
|
||||
(e: "close"): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = computed({
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineEmits<{
|
||||
defineOptions({ name: "FaSettingHeader" });
|
||||
|
||||
interface Emits {
|
||||
close: [];
|
||||
}>();
|
||||
}
|
||||
|
||||
defineEmits<Emits>();
|
||||
</script>
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { ComputedRef } from "vue";
|
||||
|
||||
defineOptions({ name: "FaSettingItem" });
|
||||
|
||||
interface SettingItemConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
@@ -62,7 +64,7 @@ interface Emits {
|
||||
(e: "change", value: any): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 标准化选项,处理computed和普通数组
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from "@stores";
|
||||
|
||||
defineOptions({ name: "FaThemeSettings" });
|
||||
import { useSettingsConfig } from "../composables/useSettingsConfig";
|
||||
import { useTheme } from "@/hooks/core/useTheme";
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ import ImgCutter from "vue-img-cutter";
|
||||
|
||||
defineOptions({ name: "FaCutterImg" });
|
||||
|
||||
interface CutterProps {
|
||||
interface Props {
|
||||
// 基础配置
|
||||
/** 是否模态框 */
|
||||
isModal?: boolean;
|
||||
@@ -119,7 +119,7 @@ interface CutterResult {
|
||||
dataURL: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<CutterProps>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
// 基础配置默认值
|
||||
isModal: false,
|
||||
tool: true,
|
||||
@@ -156,7 +156,14 @@ const props = withDefaults(defineProps<CutterProps>(), {
|
||||
previewMode: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:imgUrl", "error", "imageLoadComplete", "imageLoadError"]);
|
||||
interface Emits {
|
||||
"update:imgUrl": [value: string];
|
||||
error: [error: any];
|
||||
imageLoadComplete: [result: CutterResult];
|
||||
imageLoadError: [error: any];
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const temImgPath = ref("");
|
||||
const imgCutterModal = ref();
|
||||
|
||||
@@ -11,11 +11,11 @@ defineOptions({ name: "FaVideoPlayer" });
|
||||
|
||||
interface Props {
|
||||
/** 播放器容器 ID */
|
||||
playerId: string;
|
||||
playerId?: string;
|
||||
/** 视频源URL */
|
||||
videoUrl: string;
|
||||
videoUrl?: string;
|
||||
/** 视频封面图URL */
|
||||
posterUrl: string;
|
||||
posterUrl?: string;
|
||||
/** 是否自动播放 */
|
||||
autoplay?: boolean;
|
||||
/** 音量大小(0-1) */
|
||||
|
||||
@@ -54,34 +54,33 @@ import FaIconButton from "@/components/widget/fa-icon-button/index.vue";
|
||||
|
||||
defineOptions({ name: "FaDialog", inheritAttrs: false });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: boolean;
|
||||
title?: string;
|
||||
width?: string | number;
|
||||
/** 默认可拖拽;全屏时 Element Plus 会限制拖拽 */
|
||||
draggable?: boolean;
|
||||
/** 透传到 el-dialog 的 class */
|
||||
dialogClass?: string;
|
||||
/** 遮罩层自定义 class */
|
||||
modalClass?: string;
|
||||
/** 表单模式:detail 仅显示确定;create/update 显示取消+确定 */
|
||||
formMode?: "detail" | "create" | "update";
|
||||
/** 确定按钮 loading 状态 */
|
||||
confirmLoading?: boolean;
|
||||
/** 确定按钮文本 */
|
||||
confirmText?: string;
|
||||
/** 取消按钮文本 */
|
||||
cancelText?: string;
|
||||
}>(),
|
||||
{
|
||||
draggable: true,
|
||||
confirmText: "确定",
|
||||
cancelText: "取消",
|
||||
}
|
||||
);
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
title?: string;
|
||||
width?: string | number;
|
||||
/** 默认可拖拽;全屏时 Element Plus 会限制拖拽 */
|
||||
draggable?: boolean;
|
||||
/** 透传到 el-dialog 的 class */
|
||||
dialogClass?: string;
|
||||
/** 遮罩层自定义 class */
|
||||
modalClass?: string;
|
||||
/** 表单模式:detail 仅显示确定;create/update 显示取消+确定 */
|
||||
formMode?: "detail" | "create" | "update";
|
||||
/** 确定按钮 loading 状态 */
|
||||
confirmLoading?: boolean;
|
||||
/** 确定按钮文本 */
|
||||
confirmText?: string;
|
||||
/** 取消按钮文本 */
|
||||
cancelText?: string;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
draggable: true,
|
||||
confirmText: "确定",
|
||||
cancelText: "取消",
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
"update:modelValue": [v: boolean];
|
||||
close: [];
|
||||
opened: [];
|
||||
@@ -90,7 +89,9 @@ const emit = defineEmits<{
|
||||
cancel: [];
|
||||
/** 点击确定按钮 */
|
||||
confirm: [];
|
||||
}>();
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const attrs = useAttrs();
|
||||
const fullscreen = ref(false);
|
||||
|
||||
@@ -48,31 +48,30 @@ import FaIconButton from "@/components/widget/fa-icon-button/index.vue";
|
||||
|
||||
defineOptions({ name: "FaDrawer", inheritAttrs: false });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: boolean;
|
||||
title?: string;
|
||||
size?: string | number;
|
||||
direction?: "rtl" | "ltr" | "ttb" | "btt";
|
||||
/** 透传到 el-drawer 的 class */
|
||||
drawerClass?: string;
|
||||
/** 表单模式:detail 仅显示确定;create/update 显示取消+确定 */
|
||||
formMode?: "detail" | "create" | "update";
|
||||
/** 确定按钮 loading 状态 */
|
||||
confirmLoading?: boolean;
|
||||
/** 确定按钮文本 */
|
||||
confirmText?: string;
|
||||
/** 取消按钮文本 */
|
||||
cancelText?: string;
|
||||
}>(),
|
||||
{
|
||||
direction: "rtl",
|
||||
confirmText: "确定",
|
||||
cancelText: "取消",
|
||||
}
|
||||
);
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
title?: string;
|
||||
size?: string | number;
|
||||
direction?: "rtl" | "ltr" | "ttb" | "btt";
|
||||
/** 透传到 el-drawer 的 class */
|
||||
drawerClass?: string;
|
||||
/** 表单模式:detail 仅显示确定;create/update 显示取消+确定 */
|
||||
formMode?: "detail" | "create" | "update";
|
||||
/** 确定按钮 loading 状态 */
|
||||
confirmLoading?: boolean;
|
||||
/** 确定按钮文本 */
|
||||
confirmText?: string;
|
||||
/** 取消按钮文本 */
|
||||
cancelText?: string;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
direction: "rtl",
|
||||
confirmText: "确定",
|
||||
cancelText: "取消",
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
"update:modelValue": [v: boolean];
|
||||
close: [];
|
||||
opened: [];
|
||||
@@ -80,7 +79,9 @@ const emit = defineEmits<{
|
||||
cancel: [];
|
||||
/** 点击确定按钮 */
|
||||
confirm: [];
|
||||
}>();
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const attrs = useAttrs();
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ function saveBlobDownload(blob: Blob, rawName: string) {
|
||||
/**
|
||||
* 导出模态框组件属性定义
|
||||
*/
|
||||
interface FaExportDialogProps {
|
||||
interface Props {
|
||||
/** 内容配置 */
|
||||
contentConfig: Pick<
|
||||
IContentConfig,
|
||||
@@ -100,7 +100,7 @@ interface FaExportDialogProps {
|
||||
}
|
||||
|
||||
// 定义接收的属性
|
||||
const props = defineProps<FaExportDialogProps>();
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
const remoteExportEnabled = computed(
|
||||
() => !!(props.contentConfig.exportsAction || props.contentConfig.exportsBlobAction)
|
||||
|
||||
@@ -85,7 +85,7 @@ defineOptions({ name: "FaImportDialog", inheritAttrs: false });
|
||||
/**
|
||||
* 导入模态框组件属性定义
|
||||
*/
|
||||
interface FaImportDialogProps {
|
||||
interface Props {
|
||||
/**
|
||||
* 弹窗标题
|
||||
*/
|
||||
@@ -178,7 +178,7 @@ interface FaImportDialogProps {
|
||||
}
|
||||
|
||||
// 定义props
|
||||
const props = withDefaults(defineProps<FaImportDialogProps>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: "导入数据",
|
||||
width: "600px",
|
||||
maxHeight: "60vh",
|
||||
@@ -198,8 +198,7 @@ const importModalVisible = defineModel<boolean>("modelValue", {
|
||||
default: false,
|
||||
});
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
interface Emits {
|
||||
/** 导入成功事件 */
|
||||
"import-success": [data: any];
|
||||
/** 导入失败事件 */
|
||||
@@ -210,7 +209,10 @@ const emit = defineEmits<{
|
||||
"download-template": [];
|
||||
/** 上传事件 */
|
||||
upload: [formData: FormData, file: File];
|
||||
}>();
|
||||
}
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 引用
|
||||
const importFormRef = ref(null);
|
||||
|
||||
@@ -17,15 +17,14 @@ defineOptions({
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
style: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
interface Props {
|
||||
text?: string;
|
||||
style?: Record<string, any>;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
text: "",
|
||||
style: () => ({}),
|
||||
});
|
||||
|
||||
async function handleClipboard() {
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
:label-class-name="item.labelClassName"
|
||||
:class-name="item.className"
|
||||
>
|
||||
<!-- 带 slot 名称的项:通过具名插槽自定义 -->
|
||||
<slot
|
||||
v-if="item.slot"
|
||||
:name="item.slot"
|
||||
@@ -21,22 +20,12 @@
|
||||
:value="data ? getNestedValue(data, item.prop) : undefined"
|
||||
:row="data"
|
||||
/>
|
||||
<!-- Tag 模式 -->
|
||||
<template v-else-if="item.tag">
|
||||
<ElTag
|
||||
v-if="item.tag === true && data && data[item.prop] !== undefined"
|
||||
:type="resolveTagType(getNestedValue(data, item.prop))"
|
||||
>
|
||||
{{ getNestedValue(data, item.prop) }}
|
||||
</ElTag>
|
||||
<ElTag
|
||||
v-else-if="typeof item.tag === 'object' && data"
|
||||
:type="resolveTagType(getNestedValue(data, item.prop), item.tag.type)"
|
||||
>
|
||||
{{ resolveTagText(getNestedValue(data, item.prop), item.tag) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
<!-- 纯文本模式 -->
|
||||
<ElTag
|
||||
v-else-if="item.tag != null"
|
||||
:type="resolveTagType(getNestedValue(data, item.prop), item.tag)"
|
||||
>
|
||||
{{ resolveTagText(getNestedValue(data, item.prop), item.tag) }}
|
||||
</ElTag>
|
||||
<template v-else>
|
||||
<slot
|
||||
:name="item.prop"
|
||||
@@ -49,13 +38,11 @@
|
||||
</template>
|
||||
</ElDescriptionsItem>
|
||||
</template>
|
||||
<!-- 完全自定义模式 -->
|
||||
<template v-else>
|
||||
<slot />
|
||||
</template>
|
||||
</ElDescriptions>
|
||||
</ElScrollbar>
|
||||
<!-- 不使用滚动条时直接渲染 -->
|
||||
<ElDescriptions v-else v-bind="bindings" :class="ns.b()">
|
||||
<template v-if="$slots.title" #title>
|
||||
<slot name="title" />
|
||||
@@ -76,20 +63,12 @@
|
||||
:value="data ? getNestedValue(data, item.prop) : undefined"
|
||||
:row="data"
|
||||
/>
|
||||
<template v-else-if="item.tag">
|
||||
<ElTag
|
||||
v-if="item.tag === true && data && data[item.prop] !== undefined"
|
||||
:type="resolveTagType(getNestedValue(data, item.prop))"
|
||||
>
|
||||
{{ getNestedValue(data, item.prop) }}
|
||||
</ElTag>
|
||||
<ElTag
|
||||
v-else-if="typeof item.tag === 'object' && data"
|
||||
:type="resolveTagType(getNestedValue(data, item.prop), item.tag.type)"
|
||||
>
|
||||
{{ resolveTagText(getNestedValue(data, item.prop), item.tag) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
<ElTag
|
||||
v-else-if="item.tag != null"
|
||||
:type="resolveTagType(getNestedValue(data, item.prop), item.tag)"
|
||||
>
|
||||
{{ resolveTagText(getNestedValue(data, item.prop), item.tag) }}
|
||||
</ElTag>
|
||||
<template v-else>
|
||||
<slot
|
||||
:name="item.prop"
|
||||
@@ -113,94 +92,62 @@ defineOptions({ name: "FaDescriptions" });
|
||||
|
||||
import { computed, useAttrs } from "vue";
|
||||
import { useNamespace } from "element-plus";
|
||||
// ── 组件类型定义(内联,避免跨文件引用导致 TS Server 延迟)──
|
||||
/** ElTag 可接受的 type 值 */
|
||||
|
||||
export type TagType = "primary" | "success" | "warning" | "danger" | "info";
|
||||
|
||||
/** Tag 渲染配置 */
|
||||
export interface TagConfig {
|
||||
/**
|
||||
* Tag 映射表:key 为数据值,value 为 { type: ElTag类型, text: 显示文本 }
|
||||
* 例:{ '0': { type: 'success', text: '启用' }, '1': { type: 'danger', text: '停用' } }
|
||||
*/
|
||||
map?: Record<string, { type?: TagType; text?: string }>;
|
||||
/** 默认 type,当 map 未匹配到时使用 */
|
||||
type?: TagType;
|
||||
}
|
||||
|
||||
/** 单个描述项配置 */
|
||||
export interface DescriptionsItem {
|
||||
/** 标签文本 */
|
||||
label: string;
|
||||
/** 对应 data 中的属性名,支持 'a.b.c' 嵌套路径 */
|
||||
prop: string;
|
||||
/** 列跨度,不传则使用组件级 span 默认值 */
|
||||
span?: number;
|
||||
/**
|
||||
* 渲染模式:
|
||||
* - 不设置:纯文本
|
||||
* - true:作为 ElTag 渲染,文本为 data[prop]
|
||||
* - TagConfig 对象:通过 map 映射类型和文本
|
||||
*/
|
||||
tag?: boolean | TagConfig;
|
||||
/** 自定义插槽名称,设置后该 item 将通过具名插槽渲染 */
|
||||
slot?: string;
|
||||
/** 传递给 el-descriptions-item 的 label-class-name */
|
||||
labelClassName?: string;
|
||||
/** 传递给 el-descriptions-item 的 class-name */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const attrs = useAttrs();
|
||||
const ns = useNamespace("descriptions");
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 每行显示的描述项数量,默认 4 */
|
||||
column?: number;
|
||||
/** 是否展示边框,默认 true */
|
||||
border?: boolean;
|
||||
/** 列表尺寸,默认 'default' */
|
||||
size?: "default" | "small";
|
||||
/** 标签宽度 */
|
||||
labelWidth?: string;
|
||||
/** 描述项配置列表 */
|
||||
items?: DescriptionsItem[];
|
||||
/** 数据对象,items 模式下自动取 data[item.prop] 渲染 */
|
||||
data?: Record<string, unknown> | null;
|
||||
/** 默认 span,当 item 未指定 span 时生效,默认 2 */
|
||||
span?: number;
|
||||
/** 是否需要内置 ElScrollbar 包裹,默认 true */
|
||||
scrollbar?: boolean;
|
||||
/** ElScrollbar 最大高度,默认 '70vh' */
|
||||
maxHeight?: string;
|
||||
}>(),
|
||||
{
|
||||
column: 4,
|
||||
border: true,
|
||||
size: "default",
|
||||
labelWidth: undefined,
|
||||
items: () => [],
|
||||
data: null,
|
||||
span: 2,
|
||||
scrollbar: true,
|
||||
maxHeight: "70vh",
|
||||
}
|
||||
);
|
||||
interface Props {
|
||||
column?: number;
|
||||
border?: boolean;
|
||||
size?: "default" | "small";
|
||||
labelWidth?: string;
|
||||
items?: DescriptionsItem[];
|
||||
data?: Record<string, unknown> | null;
|
||||
span?: number;
|
||||
scrollbar?: boolean;
|
||||
maxHeight?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
column: 4,
|
||||
border: true,
|
||||
size: "default",
|
||||
labelWidth: undefined,
|
||||
items: () => [],
|
||||
data: null,
|
||||
span: 2,
|
||||
scrollbar: true,
|
||||
maxHeight: "70vh",
|
||||
});
|
||||
|
||||
// 传递给 ElDescriptions 的绑定属性
|
||||
const bindings = computed(() => {
|
||||
const bind: Record<string, unknown> = {
|
||||
column: props.column,
|
||||
border: props.border,
|
||||
...attrs, // 透传 class、style 等属性
|
||||
...attrs,
|
||||
};
|
||||
if (props.size !== "default") bind.size = props.size;
|
||||
if (props.labelWidth !== undefined) bind.labelWidth = props.labelWidth;
|
||||
return bind;
|
||||
});
|
||||
|
||||
/** 获取嵌套属性值,支持 'a.b.c' 路径 */
|
||||
function getNestedValue(obj: Record<string, unknown> | null, path: string): unknown {
|
||||
if (!obj) return undefined;
|
||||
return path.split(".").reduce((acc, key) => {
|
||||
@@ -211,22 +158,28 @@ function getNestedValue(obj: Record<string, unknown> | null, path: string): unkn
|
||||
}, obj as unknown);
|
||||
}
|
||||
|
||||
/** 解析 ElTag 的 type 属性,确保返回 ElTag 可接受的有效类型值 */
|
||||
function resolveTagType(_value: unknown, fallback?: string): TagType {
|
||||
// 优先级 1: fallback 本身就是合法 TagType → 直接使用
|
||||
if (fallback && isValidTagType(fallback)) return fallback;
|
||||
// 优先级 2: 无有效 fallback 时,用 'info' 作为兜底
|
||||
function resolveTagType(value: unknown, tag: boolean | TagConfig): TagType {
|
||||
if (typeof tag === "boolean") {
|
||||
return tag ? "success" : "danger";
|
||||
}
|
||||
if (tag.map) {
|
||||
const raw = value == null ? "" : String(value);
|
||||
if (raw in tag.map) {
|
||||
const t = tag.map[raw].type;
|
||||
if (t && TAG_TYPES.has(t)) return t;
|
||||
}
|
||||
}
|
||||
if (tag.type && TAG_TYPES.has(tag.type)) return tag.type;
|
||||
return "info";
|
||||
}
|
||||
|
||||
const TAG_TYPES: Set<string> = new Set(["primary", "success", "warning", "danger", "info"]);
|
||||
function isValidTagType(v: string): v is TagType {
|
||||
return TAG_TYPES.has(v);
|
||||
}
|
||||
|
||||
/** 解析 ElTag 显示的文本,支持 tagMap 映射 */
|
||||
function resolveTagText(value: unknown, tag: TagConfig): string {
|
||||
function resolveTagText(value: unknown, tag: boolean | TagConfig): string {
|
||||
const raw = value == null ? "" : String(value);
|
||||
if (typeof tag === "boolean") {
|
||||
return raw;
|
||||
}
|
||||
if (tag.map && raw in tag.map) {
|
||||
return tag.map[raw].text ?? raw;
|
||||
}
|
||||
@@ -235,10 +188,6 @@ function resolveTagText(value: unknown, tag: TagConfig): string {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/*
|
||||
* fa-descriptions 在 ElScrollbar 内部包裹时,
|
||||
* 需要占满宽度,避免 ElDescriptions 边框被截断。
|
||||
*/
|
||||
:deep(.fa-descriptions) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -60,25 +60,32 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: "FaGuide" });
|
||||
import { computed, type PropType } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useSettingsStore } from "@stores";
|
||||
import { MenuTypeEnum } from "@/enums/appEnum";
|
||||
|
||||
const settingStore = useSettingsStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
teleport: {
|
||||
type: [String, Object] as PropType<string | HTMLElement | null>,
|
||||
default: "body",
|
||||
},
|
||||
interface Props {
|
||||
modelValue?: boolean;
|
||||
teleport?: string | HTMLElement | null;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
teleport: "body" as const,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "change", "prev", "next", "skip"]);
|
||||
interface Emits {
|
||||
"update:modelValue": [value: boolean];
|
||||
change: [index: number];
|
||||
prev: [];
|
||||
next: [];
|
||||
skip: [];
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const open = computed({
|
||||
get: () => props.modelValue,
|
||||
|
||||
@@ -89,18 +89,21 @@ import {
|
||||
resolveIconForFaSvgIcon,
|
||||
} from "@utils";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: "500px",
|
||||
},
|
||||
interface Props {
|
||||
modelValue?: string;
|
||||
width?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: "",
|
||||
width: "500px",
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
interface Emits {
|
||||
"update:modelValue": [value: string];
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const iconSelectRef = ref();
|
||||
const popoverContentRef = ref();
|
||||
@@ -109,9 +112,7 @@ const activeTab = ref("svg");
|
||||
|
||||
const svgIcons = ref<string[]>([]);
|
||||
const elementIcons = ref<string[]>(Object.keys(ElementPlusIconsVue));
|
||||
const selectedIcon = defineModel("modelValue", {
|
||||
type: String,
|
||||
required: true,
|
||||
const selectedIcon = defineModel<string | undefined>("modelValue", {
|
||||
default: "",
|
||||
});
|
||||
|
||||
|
||||
@@ -81,15 +81,19 @@ interface WeekOption {
|
||||
}
|
||||
|
||||
// 定义props
|
||||
const props = defineProps<{
|
||||
interface Props {
|
||||
cronValue?: string;
|
||||
}>();
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
// 定义emits
|
||||
const emit = defineEmits<{
|
||||
interface Emits {
|
||||
(e: "confirm", value: string): void;
|
||||
(e: "cancel"): void;
|
||||
}>();
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const formRef = ref();
|
||||
|
||||
|
||||
@@ -20,9 +20,14 @@ import { computed } from "vue";
|
||||
import VueJsonPretty from "vue-json-pretty";
|
||||
import "vue-json-pretty/lib/styles.css";
|
||||
|
||||
const props = defineProps({
|
||||
value: { type: [String, Object, Array, Number, Boolean], default: "" },
|
||||
height: { type: String, default: "240px" },
|
||||
interface Props {
|
||||
value?: string | Record<string, any> | any[] | number | boolean;
|
||||
height?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
value: "",
|
||||
height: "240px",
|
||||
});
|
||||
|
||||
const parsed = computed(() => {
|
||||
|
||||
@@ -9,6 +9,8 @@ import markdownItHighlightjs from "markdown-it-highlightjs";
|
||||
import hljs from "highlight.js";
|
||||
import "highlight.js/styles/atom-one-light.css";
|
||||
|
||||
defineOptions({ name: "FaMarkdownRenderer" });
|
||||
|
||||
interface Props {
|
||||
content: string;
|
||||
maxLength?: number;
|
||||
|
||||
@@ -136,11 +136,13 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
animationDuration: 100,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
interface Emits {
|
||||
(e: "select", item: MenuItemType): void;
|
||||
(e: "show"): void;
|
||||
(e: "hide"): void;
|
||||
}>();
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = ref(false);
|
||||
const position = ref({ x: 0, y: 0 });
|
||||
|
||||
@@ -33,13 +33,15 @@ import {
|
||||
|
||||
defineOptions({ name: "FaMenuRouteIcon", inheritAttrs: false });
|
||||
|
||||
const props = defineProps<{
|
||||
interface Props {
|
||||
icon?: string;
|
||||
color?: string;
|
||||
/** 与 Vue `class` 一致(含条件 `false`) */
|
||||
class?: string | string[] | Record<string, boolean> | null | undefined | false;
|
||||
style?: Record<string, unknown> | string;
|
||||
}>();
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
const trimmedIcon = computed(() => props.icon?.trim() ?? "");
|
||||
|
||||
|
||||
@@ -0,0 +1,415 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full overflow-hidden">
|
||||
<div class="mb-3 flex items-center gap-3 shrink-0">
|
||||
<ElInput
|
||||
v-model="filterText"
|
||||
placeholder="搜索菜单名称"
|
||||
clearable
|
||||
style="width: 320px"
|
||||
:prefix-icon="Search"
|
||||
size="small"
|
||||
/>
|
||||
<ElButton type="primary" size="small" plain @click="toggleExpand">
|
||||
<template #icon><SwitchIcon /></template>
|
||||
{{ isExpanded ? "收起" : "展开" }}
|
||||
</ElButton>
|
||||
<ElCheckbox v-model="parentChildLinked"> 父子联动 </ElCheckbox>
|
||||
</div>
|
||||
|
||||
<ElTable
|
||||
ref="permTableRef"
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
row-key="id"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
:default-expand-all="true"
|
||||
class="flex-1 min-h-0"
|
||||
@selection-change="onSelectionChange"
|
||||
@select="onSelect"
|
||||
@select-all="onSelectAll"
|
||||
>
|
||||
<ElTableColumn type="selection" width="48" />
|
||||
<ElTableColumn
|
||||
prop="name"
|
||||
label="菜单名称"
|
||||
width="220"
|
||||
show-overflow-tooltip
|
||||
:formatter="formatMenuName"
|
||||
/>
|
||||
<ElTableColumn label="功能权限">
|
||||
<template #default="{ row }">
|
||||
<ElCheckboxGroup
|
||||
v-model="checkedBtns[row.id]"
|
||||
class="flex flex-wrap gap-x-3"
|
||||
@change="(val: CheckboxValueType[]) => onBtnChange(row, val)"
|
||||
>
|
||||
<ElCheckbox
|
||||
v-for="btn in getMenuBtns(row)"
|
||||
:key="btn.id"
|
||||
:value="btn.id"
|
||||
:label="btn.name"
|
||||
/>
|
||||
</ElCheckboxGroup>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick, computed, h } from "vue";
|
||||
import type { TableInstance, CheckboxValueType } from "element-plus";
|
||||
import { Search, Switch as SwitchIcon } from "@element-plus/icons-vue";
|
||||
import FaMenuRouteIcon from "@/components/others/fa-menu-routeIcon/index.vue";
|
||||
|
||||
defineOptions({ name: "FaMenuTreeTable" });
|
||||
|
||||
// ---- 类型常量 ----
|
||||
const DIR_TYPE = 1;
|
||||
const MENU_TYPE = 2;
|
||||
const BTN_TYPE = 3;
|
||||
|
||||
function isDirOrMenu(type: number | undefined): boolean {
|
||||
return type === DIR_TYPE || type === MENU_TYPE;
|
||||
}
|
||||
|
||||
function isPermission(type: number | undefined): boolean {
|
||||
return type != null && type >= BTN_TYPE;
|
||||
}
|
||||
|
||||
// ---- Props ----
|
||||
interface Props {
|
||||
menuTree: any[];
|
||||
checkedIds?: number[];
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
checkedIds: () => [],
|
||||
});
|
||||
|
||||
// ---- 状态 ----
|
||||
const filterText = ref("");
|
||||
const isExpanded = ref(true);
|
||||
const parentChildLinked = ref(true);
|
||||
const isHandlingCascade = ref(false);
|
||||
const permTableRef = ref<TableInstance>();
|
||||
const tableData = ref<any[]>([]);
|
||||
const checkedBtns = ref<Record<number, number[]>>({});
|
||||
const selectedMenuIds = ref<Set<number>>(new Set());
|
||||
|
||||
// ---- 搜索过滤后的菜单树 ----
|
||||
const filteredMenuTree = computed(() =>
|
||||
filterText.value ? filterMenuTree(props.menuTree, filterText.value) : props.menuTree
|
||||
);
|
||||
|
||||
function filterMenuTree(nodes: any[], text: string): any[] {
|
||||
if (!text) return nodes;
|
||||
const lower = text.toLowerCase();
|
||||
|
||||
function walk(n: any): any | null {
|
||||
const nameMatch = (n.name || "").toLowerCase().includes(lower);
|
||||
const filteredChildren = n.children?.map(walk).filter(Boolean) ?? [];
|
||||
|
||||
if (nameMatch || filteredChildren.length > 0) {
|
||||
return { ...n, children: filteredChildren.length > 0 ? filteredChildren : n.children };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return nodes.map(walk).filter(Boolean) as any[];
|
||||
}
|
||||
|
||||
// ---- 工具函数 ----
|
||||
function walkTree(nodes: any[], fn: (node: any) => void) {
|
||||
for (const node of nodes) {
|
||||
fn(node);
|
||||
if (node.children) walkTree(node.children, fn);
|
||||
}
|
||||
}
|
||||
|
||||
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 findTableNode(id: number, nodes: any[]): any | 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 findParentInRawTree(menuId: number, nodes: any[]): any | null {
|
||||
for (const n of nodes) {
|
||||
if (n.children?.some((c: any) => c.id === menuId)) return n;
|
||||
if (n.children) {
|
||||
const found = findParentInRawTree(menuId, n.children);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatMenuName(row: any) {
|
||||
const name = row.name || "";
|
||||
if (row.icon) {
|
||||
return h("span", { class: "inline-flex items-center gap-1.5" }, [
|
||||
h(FaMenuRouteIcon, { icon: row.icon, style: { verticalAlign: "-0.15em" } }),
|
||||
name,
|
||||
]);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function getMenuBtns(row: any): any[] {
|
||||
const fullNode = findRawNode(row.id, props.menuTree);
|
||||
if (!fullNode?.children) return [];
|
||||
return fullNode.children.filter((c: any) => isPermission(c.type));
|
||||
}
|
||||
|
||||
function hasAnySelectedDescendant(node: any): boolean {
|
||||
if (checkedBtns.value[node.id]?.length > 0) return true;
|
||||
const tableNode = findTableNode(node.id, tableData.value);
|
||||
if (tableNode?.children) {
|
||||
for (const child of tableNode.children) {
|
||||
if (isDirOrMenu(child.type) && hasAnySelectedDescendant(child)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function findAncestors(nodeId: number): any[] {
|
||||
function collectAncestorIds(nodes: any[], target: number): number[] {
|
||||
for (const n of nodes) {
|
||||
if (n.children?.some((c: any) => c.id === target)) {
|
||||
return [n.id!, ...collectAncestorIds(props.menuTree, n.id!)];
|
||||
}
|
||||
if (n.children) {
|
||||
const found = collectAncestorIds(n.children, target);
|
||||
if (found.length > 0) return found;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
return collectAncestorIds(props.menuTree, nodeId)
|
||||
.map((id) => findTableNode(id, tableData.value))
|
||||
.filter(Boolean) as any[];
|
||||
}
|
||||
|
||||
function filterTableData(nodes: any[]): any[] {
|
||||
return nodes
|
||||
.filter((n) => isDirOrMenu(n.type))
|
||||
.map((n) => ({
|
||||
...n,
|
||||
children: n.children ? filterTableData(n.children) : undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
// ---- 级联 ----
|
||||
function collectBtnState(node: any) {
|
||||
const ids = getMenuBtns(node).map((b: any) => b.id!);
|
||||
if (ids.length) checkedBtns.value[node.id!] = ids;
|
||||
}
|
||||
|
||||
function cascadeSelect(node: any) {
|
||||
permTableRef.value?.toggleRowSelection(node, true);
|
||||
collectBtnState(node);
|
||||
const tableNode = findTableNode(node.id, tableData.value);
|
||||
if (tableNode?.children) {
|
||||
for (const child of tableNode.children) {
|
||||
if (isDirOrMenu(child.type)) cascadeSelect(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cascadeDeselect(node: any) {
|
||||
permTableRef.value?.toggleRowSelection(node, false);
|
||||
checkedBtns.value[node.id!] = [];
|
||||
const tableNode = findTableNode(node.id, tableData.value);
|
||||
if (tableNode?.children) {
|
||||
for (const child of tableNode.children) {
|
||||
if (isDirOrMenu(child.type)) cascadeDeselect(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectAncestors(node: any) {
|
||||
findAncestors(node.id!).forEach((a) => permTableRef.value?.toggleRowSelection(a, true));
|
||||
}
|
||||
|
||||
function deselectAncestorsIfNeeded(node: any) {
|
||||
findAncestors(node.id!).forEach((a) => {
|
||||
if (!hasAnySelectedDescendant(a)) permTableRef.value?.toggleRowSelection(a, false);
|
||||
});
|
||||
}
|
||||
|
||||
function withCascadeGuard(fn: () => void) {
|
||||
isHandlingCascade.value = true;
|
||||
try {
|
||||
fn();
|
||||
} finally {
|
||||
isHandlingCascade.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Table 事件 ----
|
||||
function onSelectionChange() {
|
||||
const store = (permTableRef.value as any)?.store;
|
||||
const selection = store?.states?.selection?.value || [];
|
||||
const newSelected = new Set<number>();
|
||||
selection.forEach((row: any) => {
|
||||
if (isDirOrMenu(row.type)) newSelected.add(row.id!);
|
||||
});
|
||||
selectedMenuIds.value = newSelected;
|
||||
if (newSelected.size === 0) checkedBtns.value = {};
|
||||
}
|
||||
|
||||
function onSelectAll() {
|
||||
const store = (permTableRef.value as any)?.store;
|
||||
const selection = store?.states?.selection?.value || [];
|
||||
withCascadeGuard(() => {
|
||||
if (!selection.length) {
|
||||
checkedBtns.value = {};
|
||||
selectedMenuIds.value.clear();
|
||||
return;
|
||||
}
|
||||
for (const row of selection) {
|
||||
if (isDirOrMenu(row.type)) {
|
||||
selectedMenuIds.value.add(row.id!);
|
||||
if (parentChildLinked.value) collectBtnState(row);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onSelect(selection: any[], row: any) {
|
||||
if (isHandlingCascade.value || !parentChildLinked.value) return;
|
||||
const tableRow = findTableNode(row.id, tableData.value);
|
||||
if (!tableRow) return;
|
||||
const isSelected = selection.some((r) => r.id === tableRow.id);
|
||||
withCascadeGuard(() => {
|
||||
if (isSelected) {
|
||||
selectAncestors(tableRow);
|
||||
cascadeSelect(tableRow);
|
||||
} else {
|
||||
cascadeDeselect(tableRow);
|
||||
deselectAncestorsIfNeeded(tableRow);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onBtnChange(row: any, val: CheckboxValueType[]) {
|
||||
checkedBtns.value[row.id!] = val.map((v) => Number(v));
|
||||
if (!parentChildLinked.value) return;
|
||||
withCascadeGuard(() => {
|
||||
if (val.length > 0) {
|
||||
permTableRef.value?.toggleRowSelection(row, true);
|
||||
selectAncestors(row);
|
||||
} else if (!hasAnySelectedDescendant(row)) {
|
||||
permTableRef.value?.toggleRowSelection(row, false);
|
||||
deselectAncestorsIfNeeded(row);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---- 展开/收起 ----
|
||||
function setAllRowsExpanded(expanded: boolean) {
|
||||
nextTick(() => {
|
||||
const el = permTableRef.value;
|
||||
if (!el || !tableData.value.length) return;
|
||||
walkTree(tableData.value, (row) => {
|
||||
if (row.children?.length) el.toggleRowExpansion(row, expanded);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toggleExpand() {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
setAllRowsExpanded(isExpanded.value);
|
||||
}
|
||||
|
||||
// ---- 初始化 ----
|
||||
function checkParentChildLinked(ids: number[], nodes: any[]): boolean {
|
||||
if (!ids.length || !nodes.length) return true;
|
||||
const idMap = new Map<number, any>();
|
||||
walkTree(nodes, (n) => idMap.set(n.id!, n));
|
||||
for (const id of ids) {
|
||||
const node = idMap.get(id);
|
||||
if (!node) continue;
|
||||
if (node.children?.length && node.children.some((c: any) => !ids.includes(c.id!))) return false;
|
||||
const parent = findParentInRawTree(id, nodes);
|
||||
if (parent && !ids.includes(parent.id!)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function initSelection(ids: number[]) {
|
||||
await nextTick();
|
||||
const btnsState: Record<number, number[]> = {};
|
||||
const rowsToSelect: any[] = [];
|
||||
|
||||
walkTree(tableData.value, (node) => {
|
||||
if (isDirOrMenu(node.type) && ids.includes(node.id!)) {
|
||||
rowsToSelect.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
walkTree(props.menuTree, (node) => {
|
||||
if (!isPermission(node.type) || !ids.includes(node.id!)) return;
|
||||
const parent = findParentInRawTree(node.id!, props.menuTree);
|
||||
if (parent) (btnsState[parent.id!] ??= []).push(node.id!);
|
||||
});
|
||||
|
||||
checkedBtns.value = btnsState;
|
||||
|
||||
withCascadeGuard(() => {
|
||||
const store = (permTableRef.value as any)?.store;
|
||||
if (!store) return;
|
||||
store.clearSelection();
|
||||
rowsToSelect.forEach((row) => store.toggleRowSelection(row, true, false));
|
||||
});
|
||||
}
|
||||
|
||||
// ---- 对外方法 ----
|
||||
function getCheckedIds(): number[] {
|
||||
const allIds = new Set<number>();
|
||||
selectedMenuIds.value.forEach((id) => allIds.add(id));
|
||||
Object.values(checkedBtns.value).forEach((btnIds) => btnIds.forEach((id) => allIds.add(id)));
|
||||
return [...allIds];
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
filterText.value = "";
|
||||
const tree = props.menuTree;
|
||||
parentChildLinked.value = checkParentChildLinked(props.checkedIds, tree);
|
||||
tableData.value = filterTableData(tree);
|
||||
nextTick(() => initSelection(props.checkedIds));
|
||||
}
|
||||
|
||||
defineExpose({ getCheckedIds, refresh });
|
||||
|
||||
// ---- 数据变化监听 ----
|
||||
watch(
|
||||
() => [props.menuTree, props.checkedIds] as const,
|
||||
() => refresh(),
|
||||
{ immediate: false }
|
||||
);
|
||||
|
||||
watch(filterText, () => {
|
||||
const tree = filteredMenuTree.value;
|
||||
tableData.value = filterTableData(tree);
|
||||
nextTick(() => initSelection(props.checkedIds));
|
||||
if (filterText.value) setAllRowsExpanded(true);
|
||||
});
|
||||
</script>
|
||||
@@ -20,52 +20,41 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, type PropType } from "vue";
|
||||
import { watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
total: {
|
||||
type: Number as PropType<number>,
|
||||
default: 0,
|
||||
},
|
||||
pageSizes: {
|
||||
type: Array as PropType<number[]>,
|
||||
default() {
|
||||
return [10, 20, 30, 50, 100];
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
type: String,
|
||||
default: "total, sizes, prev, pager, next, jumper",
|
||||
},
|
||||
background: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
defineOptions({ name: "FaPagination" });
|
||||
|
||||
interface Props {
|
||||
total?: number;
|
||||
pageSizes?: number[];
|
||||
layout?: string;
|
||||
background?: boolean;
|
||||
disabled?: boolean;
|
||||
/** 页码按钮数量(透传 ElPagination) */
|
||||
pagerCount: {
|
||||
type: Number as PropType<number>,
|
||||
default: undefined,
|
||||
},
|
||||
pagerCount?: number;
|
||||
/** 分页器尺寸(透传 ElPagination) */
|
||||
size: {
|
||||
type: String as PropType<"" | "default" | "small" | "large">,
|
||||
default: undefined,
|
||||
},
|
||||
autoScroll: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
hidden: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
size?: "" | "default" | "small" | "large";
|
||||
autoScroll?: boolean;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
total: 0,
|
||||
pageSizes: () => [10, 20, 30, 50, 100],
|
||||
layout: "total, sizes, prev, pager, next, jumper",
|
||||
background: true,
|
||||
disabled: false,
|
||||
pagerCount: undefined,
|
||||
size: undefined,
|
||||
autoScroll: true,
|
||||
hidden: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["pagination"]);
|
||||
interface Emits {
|
||||
pagination: [params: { page: number; limit: number }];
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const currentPage = defineModel("page", {
|
||||
type: Number,
|
||||
|
||||
@@ -9,19 +9,12 @@
|
||||
* Widget 演示页章节标题(与历史 `page-title` Tailwind 一致)。
|
||||
* 业务页如需同款排版可直接复用;CRUD 列表页优先用 FaSearchBar + 表格区标题即可。
|
||||
*/
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
/** 页面唯一主标题用 h1,分区用 h2 */
|
||||
tag?: "h1" | "h2";
|
||||
}>(),
|
||||
{ tag: "h1" }
|
||||
);
|
||||
</script>
|
||||
defineOptions({ name: "FaShowcase" });
|
||||
|
||||
<style scoped>
|
||||
@reference "@styles/core/tailwind.css";
|
||||
|
||||
.fa-widget-demo-title {
|
||||
@apply my-5 text-xl font-medium first:mt-0;
|
||||
interface Props {
|
||||
/** 页面唯一主标题用 h1,分区用 h2 */
|
||||
tag?: "h1" | "h2";
|
||||
}
|
||||
</style>
|
||||
|
||||
withDefaults(defineProps<Props>(), { tag: "h1" });
|
||||
</script>
|
||||
|
||||
@@ -198,21 +198,22 @@ export interface ISelectConfig<T = any> {
|
||||
[key: string]: any;
|
||||
}>;
|
||||
}
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
selectConfig: ISelectConfig;
|
||||
text?: string;
|
||||
}>(),
|
||||
{
|
||||
text: "",
|
||||
}
|
||||
);
|
||||
interface Props {
|
||||
selectConfig: ISelectConfig;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
text: "",
|
||||
});
|
||||
|
||||
// 自定义事件
|
||||
const emit = defineEmits<{
|
||||
interface Emits {
|
||||
confirmClick: [selection: any[]];
|
||||
clearClick: [];
|
||||
}>();
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 主键
|
||||
const pk = props.selectConfig.pk ?? "id";
|
||||
|
||||
@@ -83,134 +83,76 @@ import { UploadRawFile, UploadRequestOptions, ElMessage, type UploadUserFile } f
|
||||
import ParamsAPI from "@/api/module_system/params";
|
||||
import { dataURLToFile } from "@utils";
|
||||
|
||||
const props = defineProps({
|
||||
interface Props {
|
||||
/**
|
||||
* 请求携带的额外参数
|
||||
*/
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
data?: Record<string, any>;
|
||||
/**
|
||||
* 上传文件的参数名
|
||||
*/
|
||||
name: {
|
||||
type: String,
|
||||
default: "file",
|
||||
},
|
||||
name?: string;
|
||||
/**
|
||||
* 最大文件大小(单位:M)
|
||||
*/
|
||||
maxFileSize: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
|
||||
maxFileSize?: number;
|
||||
/**
|
||||
* 上传图片格式,默认支持所有图片(image/*),指定格式示例:'.png,.jpg,.jpeg,.gif,.bmp'
|
||||
*/
|
||||
accept: {
|
||||
type: String,
|
||||
default: "image/*",
|
||||
},
|
||||
|
||||
accept?: string;
|
||||
/**
|
||||
* 自定义样式,用于设置组件的宽度和高度等其他样式
|
||||
*/
|
||||
style: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
width: "150px",
|
||||
height: "150px",
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
style?: Record<string, any>;
|
||||
/**
|
||||
* 是否禁用
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* 是否显示提示信息
|
||||
*/
|
||||
showTip: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
showTip?: boolean;
|
||||
/**
|
||||
* 提示文本
|
||||
*/
|
||||
tipText: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
|
||||
tipText?: string;
|
||||
/**
|
||||
* 是否启用图片预览功能
|
||||
*/
|
||||
enablePreview: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
|
||||
enablePreview?: boolean;
|
||||
/** 选图后先裁剪再上传(用于站点 Logo / 背景等) */
|
||||
enableCrop: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
enableCrop?: boolean;
|
||||
cropBoxWidth?: number;
|
||||
cropBoxHeight?: number;
|
||||
cropCutWidth?: number;
|
||||
cropCutHeight?: number;
|
||||
cropQuality?: number;
|
||||
cropFileType?: "png" | "jpeg" | "webp";
|
||||
cropDialogTitle?: string;
|
||||
cropInnerTitle?: string;
|
||||
cropPreviewTitle?: string;
|
||||
}
|
||||
|
||||
cropBoxWidth: {
|
||||
type: Number,
|
||||
default: 520,
|
||||
},
|
||||
|
||||
cropBoxHeight: {
|
||||
type: Number,
|
||||
default: 360,
|
||||
},
|
||||
|
||||
cropCutWidth: {
|
||||
type: Number,
|
||||
default: 400,
|
||||
},
|
||||
|
||||
cropCutHeight: {
|
||||
type: Number,
|
||||
default: 300,
|
||||
},
|
||||
|
||||
cropQuality: {
|
||||
type: Number,
|
||||
default: 0.92,
|
||||
},
|
||||
|
||||
cropFileType: {
|
||||
type: String as () => "png" | "jpeg" | "webp",
|
||||
default: "jpeg",
|
||||
},
|
||||
|
||||
cropDialogTitle: {
|
||||
type: String,
|
||||
default: "裁剪图片",
|
||||
},
|
||||
|
||||
cropInnerTitle: {
|
||||
type: String,
|
||||
default: "调整图片",
|
||||
},
|
||||
|
||||
cropPreviewTitle: {
|
||||
type: String,
|
||||
default: "预览",
|
||||
},
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
data: () => ({}),
|
||||
name: "file",
|
||||
maxFileSize: 10,
|
||||
accept: "image/*",
|
||||
style: () => ({ width: "150px", height: "150px" }),
|
||||
disabled: false,
|
||||
showTip: false,
|
||||
tipText: "",
|
||||
enablePreview: true,
|
||||
enableCrop: false,
|
||||
cropBoxWidth: 520,
|
||||
cropBoxHeight: 360,
|
||||
cropCutWidth: 400,
|
||||
cropCutHeight: 300,
|
||||
cropQuality: 0.92,
|
||||
cropFileType: "jpeg",
|
||||
cropDialogTitle: "裁剪图片",
|
||||
cropInnerTitle: "调整图片",
|
||||
cropPreviewTitle: "预览",
|
||||
});
|
||||
|
||||
// 接收字符串类型的modelValue,保持与现有代码的兼容性
|
||||
@@ -307,12 +249,14 @@ watch(
|
||||
/**
|
||||
* 定义组件触发的事件
|
||||
*/
|
||||
const emit = defineEmits<{
|
||||
interface Emits {
|
||||
(e: "success", fileInfo: UploadFilePath): void;
|
||||
(e: "error", error: any): void;
|
||||
(e: "input", value: string): void;
|
||||
(e: "update:modelValue", value: string): void;
|
||||
}>();
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
/**
|
||||
* 限制用户上传文件的格式和大小
|
||||
|
||||
@@ -28,7 +28,7 @@ import { useSettingsStore } from "@stores";
|
||||
|
||||
defineOptions({ name: "FaWatermark" });
|
||||
|
||||
interface WatermarkProps {
|
||||
interface Props {
|
||||
/** 水印内容 */
|
||||
content?: string;
|
||||
/** 水印是否可见 */
|
||||
@@ -51,7 +51,7 @@ interface WatermarkProps {
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<WatermarkProps>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
content: AppConfig.systemInfo.name,
|
||||
visible: false,
|
||||
fontSize: 16,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<ElButton
|
||||
v-if="permCreate"
|
||||
v-hasPerm="permCreate"
|
||||
type="success"
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="$emit('add')"
|
||||
plain
|
||||
@@ -30,7 +30,7 @@
|
||||
v-if="permImport"
|
||||
v-hasPerm="permImport"
|
||||
v-ripple
|
||||
type="warning"
|
||||
type="success"
|
||||
:loading="importLoading"
|
||||
:icon="Upload"
|
||||
@click="$emit('import')"
|
||||
@@ -42,7 +42,7 @@
|
||||
v-if="permExport"
|
||||
v-hasPerm="permExport"
|
||||
v-ripple
|
||||
type="primary"
|
||||
type="warning"
|
||||
:loading="exportLoading"
|
||||
:icon="Download"
|
||||
@click="$emit('export')"
|
||||
@@ -89,38 +89,39 @@ import { ArrowDown, Delete, Download, Plus, Upload } from "@element-plus/icons-v
|
||||
import { computed } from "vue";
|
||||
import type { FaTableHeaderLeftConfigButton } from "@/components/modal/types";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 与 CrudContent `toolbarLeftBtn` 一致时走配置驱动(与 handleToolbar 对齐) */
|
||||
configButtons?: FaTableHeaderLeftConfigButton[];
|
||||
/** 勾选行主键,用于禁用批删 / 更多(插槽完全自定义时可不传) */
|
||||
removeIds?: Array<string | number>;
|
||||
/** 新增按钮权限,不传则不显示(configButtons 未传时) */
|
||||
permCreate?: string | string[];
|
||||
/** 导入按钮权限,不传则不显示;顺序在新增之后、批量删除之前 */
|
||||
permImport?: string | string[];
|
||||
/** 导出按钮权限,不传则不显示 */
|
||||
permExport?: string | string[];
|
||||
/** 导入按钮 loading */
|
||||
importLoading?: boolean;
|
||||
/** 导出按钮 loading */
|
||||
exportLoading?: boolean;
|
||||
/** 批量删除权限,不传则不显示 */
|
||||
permDelete?: string | string[];
|
||||
/** 「更多」下拉权限,不传则不显示 */
|
||||
permPatch?: string | string[];
|
||||
/** 批量删除中(按钮 loading,并禁用「更多」) */
|
||||
deleteLoading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
removeIds: () => [],
|
||||
deleteLoading: false,
|
||||
importLoading: false,
|
||||
exportLoading: false,
|
||||
}
|
||||
);
|
||||
defineOptions({ name: "FaTableHeaderLeft" });
|
||||
|
||||
defineEmits<{
|
||||
interface Props {
|
||||
/** 与 CrudContent `toolbarLeftBtn` 一致时走配置驱动(与 handleToolbar 对齐) */
|
||||
configButtons?: FaTableHeaderLeftConfigButton[];
|
||||
/** 勾选行主键,用于禁用批删 / 更多(插槽完全自定义时可不传) */
|
||||
removeIds?: Array<string | number>;
|
||||
/** 新增按钮权限,不传则不显示(configButtons 未传时) */
|
||||
permCreate?: string | string[];
|
||||
/** 导入按钮权限,不传则不显示;顺序在新增之后、批量删除之前 */
|
||||
permImport?: string | string[];
|
||||
/** 导出按钮权限,不传则不显示 */
|
||||
permExport?: string | string[];
|
||||
/** 导入按钮 loading */
|
||||
importLoading?: boolean;
|
||||
/** 导出按钮 loading */
|
||||
exportLoading?: boolean;
|
||||
/** 批量删除权限,不传则不显示 */
|
||||
permDelete?: string | string[];
|
||||
/** 「更多」下拉权限,不传则不显示 */
|
||||
permPatch?: string | string[];
|
||||
/** 批量删除中(按钮 loading,并禁用「更多」) */
|
||||
deleteLoading?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
removeIds: () => [],
|
||||
deleteLoading: false,
|
||||
importLoading: false,
|
||||
exportLoading: false,
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
/** 配置模式:与 CrudContent handleToolbar 一致 */
|
||||
toolbar: [name: string];
|
||||
add: [];
|
||||
@@ -128,7 +129,9 @@ defineEmits<{
|
||||
export: [];
|
||||
delete: [];
|
||||
more: [value: string];
|
||||
}>();
|
||||
}
|
||||
|
||||
defineEmits<Emits>();
|
||||
|
||||
const moreDisabled = computed(() => props.removeIds.length === 0 || props.deleteLoading);
|
||||
</script>
|
||||
|
||||
@@ -192,11 +192,13 @@ const columns = defineModel<ColumnOption[]>("columns", {
|
||||
default: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "refresh"): void;
|
||||
(e: "search"): void;
|
||||
(e: "update:showSearchBar", value: boolean): void;
|
||||
}>();
|
||||
interface Emits {
|
||||
refresh: [];
|
||||
search: [];
|
||||
"update:showSearchBar": [value: boolean];
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
/**
|
||||
* 获取列的显示状态
|
||||
|
||||
@@ -149,7 +149,7 @@ interface PaginationOptions {
|
||||
}
|
||||
|
||||
/** FaTable 组件的 Props 接口 */
|
||||
interface FaTableProps extends TableProps<Record<string, any>> {
|
||||
interface Props extends TableProps<Record<string, any>> {
|
||||
/** 加载状态 */
|
||||
loading?: boolean;
|
||||
/** 列渲染配置 */
|
||||
@@ -168,7 +168,7 @@ interface FaTableProps extends TableProps<Record<string, any>> {
|
||||
disableRowDrag?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<FaTableProps>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
columns: () => [],
|
||||
fit: true,
|
||||
showHeader: true,
|
||||
@@ -314,12 +314,14 @@ const mergedTableProps = computed(() => ({
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
const emit = defineEmits<{
|
||||
interface Emits {
|
||||
(e: "pagination:size-change", val: number): void;
|
||||
(e: "pagination:current-change", val: number): void;
|
||||
(e: "update:data", val: Record<string, unknown>[]): void;
|
||||
(e: "row-order-change", val: Record<string, unknown>[]): void;
|
||||
}>();
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
/** 无 data 时用固定空数组,避免 v-model 每次拿到新 [] */
|
||||
const emptyDataStub = ref<Record<string, unknown>[]>([]);
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
import { computed, watch, nextTick, onUnmounted, shallowRef } from "vue";
|
||||
import { useTransition, TransitionPresets } from "@vueuse/core";
|
||||
|
||||
defineOptions({ name: "FaCountTo" });
|
||||
|
||||
// 类型定义
|
||||
interface CountToProps {
|
||||
interface Props {
|
||||
/** 目标值 */
|
||||
target: number;
|
||||
/** 动画持续时间(毫秒) */
|
||||
@@ -36,7 +38,7 @@ interface CountToProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface CountToEmits {
|
||||
interface Emits {
|
||||
started: [value: number];
|
||||
finished: [value: number];
|
||||
paused: [value: number];
|
||||
@@ -64,7 +66,7 @@ const MAX_DECIMALS = 10;
|
||||
const DEFAULT_EASING = "easeOutExpo";
|
||||
const DEFAULT_DURATION = 2000;
|
||||
|
||||
const props = withDefaults(defineProps<CountToProps>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
target: 0,
|
||||
duration: DEFAULT_DURATION,
|
||||
autoStart: true,
|
||||
@@ -77,7 +79,7 @@ const props = withDefaults(defineProps<CountToProps>(), {
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<CountToEmits>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 工具函数
|
||||
const validateNumber = (value: number, name: string, defaultValue: number): number => {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<FaTextScroll
|
||||
v-if="showFestivalStrip"
|
||||
class="mb-3!"
|
||||
type="primary"
|
||||
type="theme"
|
||||
:text="festivalScrollDisplayHtml"
|
||||
height="40px"
|
||||
:speed="55"
|
||||
|
||||
@@ -67,7 +67,7 @@ type ThemeType =
|
||||
/**
|
||||
* 文本滚动组件属性接口
|
||||
*/
|
||||
export interface TextScrollProps {
|
||||
export interface Props {
|
||||
/** 滚动文本内容 */
|
||||
text?: string;
|
||||
/** 主题类型 */
|
||||
@@ -88,7 +88,7 @@ export interface TextScrollProps {
|
||||
alwaysScroll?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<TextScrollProps>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
text: "",
|
||||
direction: "left",
|
||||
speed: 80,
|
||||
@@ -100,9 +100,11 @@ const props = withDefaults(defineProps<TextScrollProps>(), {
|
||||
alwaysScroll: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
interface Emits {
|
||||
close: [];
|
||||
}>();
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const handleClose = () => {
|
||||
emit("close");
|
||||
|
||||
@@ -121,14 +121,18 @@ defineOptions({ name: "AuthTopBar" });
|
||||
|
||||
const DEFAULT_APP_VERSION = "3.0.0";
|
||||
|
||||
const props = defineProps<{
|
||||
interface Props {
|
||||
/** 登录区表单水平对齐;未传入时不展示布局切换 */
|
||||
panelAlign?: LoginPanelAlign | null;
|
||||
}>();
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
|
||||
interface Emits {
|
||||
"update:panelAlign": [value: LoginPanelAlign];
|
||||
}>();
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const layoutAlignOptions: {
|
||||
value: LoginPanelAlign;
|
||||
|
||||
@@ -51,8 +51,7 @@
|
||||
show-password
|
||||
clearable
|
||||
:placeholder="$t('login.placeholder.password')"
|
||||
@keyup="checkCapsLock"
|
||||
@keyup.enter="$emit('submit')"
|
||||
@keyup="onPasswordKeyup"
|
||||
>
|
||||
<template #prefix>
|
||||
<ElIcon><Lock /></ElIcon>
|
||||
@@ -179,7 +178,9 @@ import type { Account, AccountKey } from "@views/module_system/auth/login/types"
|
||||
|
||||
const loginForm = defineModel<LoginFormData>("loginForm", { required: true });
|
||||
|
||||
defineProps<{
|
||||
defineOptions({ name: "FaLoginAccountForm" });
|
||||
|
||||
interface Props {
|
||||
rules: FormRules;
|
||||
captchaState: CaptchaInfo;
|
||||
codeLoading: boolean;
|
||||
@@ -189,12 +190,14 @@ defineProps<{
|
||||
isDark: boolean;
|
||||
dragVerifyTextColor: string;
|
||||
loading: boolean;
|
||||
}>();
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {});
|
||||
|
||||
const isPassing = defineModel<boolean>("isPassing", { required: true });
|
||||
const isClickPass = defineModel<boolean>("isClickPass", { required: true });
|
||||
|
||||
defineEmits<{
|
||||
interface Emits {
|
||||
submit: [];
|
||||
setupAccount: [key: AccountKey];
|
||||
getCaptcha: [];
|
||||
@@ -203,15 +206,20 @@ defineEmits<{
|
||||
forget: [];
|
||||
register: [];
|
||||
oauth: [provider: "wechat" | "qq" | "github" | "gitee"];
|
||||
}>();
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const formRef = ref();
|
||||
const dragVerifyRef = ref<{ reset?: () => void } | null>(null);
|
||||
const isCapsLock = ref(false);
|
||||
|
||||
function checkCapsLock(event: KeyboardEvent) {
|
||||
function onPasswordKeyup(event: KeyboardEvent) {
|
||||
if (event instanceof KeyboardEvent) {
|
||||
isCapsLock.value = event.getModifierState("CapsLock");
|
||||
if (event.key === "Enter") {
|
||||
emit("submit");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,20 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{ hint: string; linkText: string }>();
|
||||
defineEmits<{ link: [] }>();
|
||||
defineOptions({ name: "FaLoginAuthLinkRow" });
|
||||
|
||||
interface Props {
|
||||
hint: string;
|
||||
linkText: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {});
|
||||
|
||||
interface Emits {
|
||||
link: [];
|
||||
}
|
||||
|
||||
defineEmits<Emits>();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -17,13 +17,12 @@ import loginIcon from "@fa_imgs/background.svg";
|
||||
|
||||
defineOptions({ name: "LoginCenterBackdrop" });
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
/** 铺满视口并置于底层(与固定顶栏配合) */
|
||||
viewportFixed?: boolean;
|
||||
}>(),
|
||||
{ viewportFixed: false }
|
||||
);
|
||||
interface Props {
|
||||
/** 铺满视口并置于底层(与固定顶栏配合) */
|
||||
viewportFixed?: boolean;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), { viewportFixed: false });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
clearable
|
||||
:placeholder="$t('login.placeholder.password')"
|
||||
@keyup="checkCapsLock"
|
||||
@keyup.enter="$emit('submit')"
|
||||
>
|
||||
<template #prefix>
|
||||
<ElIcon><Lock /></ElIcon>
|
||||
@@ -52,7 +51,6 @@
|
||||
clearable
|
||||
:placeholder="$t('login.message.password.confirm')"
|
||||
@keyup="checkCapsLock"
|
||||
@keyup.enter="$emit('submit')"
|
||||
>
|
||||
<template #prefix>
|
||||
<ElIcon><Lock /></ElIcon>
|
||||
@@ -88,13 +86,22 @@ import type { FormRules } from "element-plus";
|
||||
|
||||
const forgetForm = defineModel<ForgetPasswordForm>("forgetForm", { required: true });
|
||||
|
||||
defineProps<{
|
||||
defineOptions({ name: "FaLoginForgetPanel" });
|
||||
|
||||
interface Props {
|
||||
forgetRules: FormRules<ForgetPasswordForm>;
|
||||
formKey: number | string;
|
||||
forgetLoading: boolean;
|
||||
}>();
|
||||
}
|
||||
|
||||
defineEmits<{ submit: []; toLogin: [] }>();
|
||||
withDefaults(defineProps<Props>(), {});
|
||||
|
||||
interface Emits {
|
||||
submit: [];
|
||||
toLogin: [];
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const formRef = ref();
|
||||
const isCapsLock = ref(false);
|
||||
@@ -102,6 +109,9 @@ const isCapsLock = ref(false);
|
||||
function checkCapsLock(event: KeyboardEvent) {
|
||||
if (event instanceof KeyboardEvent) {
|
||||
isCapsLock.value = event.getModifierState("CapsLock");
|
||||
if (event.key === "Enter") {
|
||||
emit("submit");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,11 +93,18 @@ import loginIcon from "@fa_imgs/background.svg";
|
||||
import { useConfigStore } from "@stores";
|
||||
import { themeAnimation } from "@utils";
|
||||
|
||||
defineProps<{
|
||||
defineOptions({ name: "FaLoginLeftView" });
|
||||
|
||||
interface Props {
|
||||
hideContent?: boolean;
|
||||
/** 顶栏已展示 Logo/标题/版本时隐藏左侧重复顶栏 */
|
||||
hideTopBranding?: boolean;
|
||||
}>();
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
hideContent: false,
|
||||
hideTopBranding: false,
|
||||
});
|
||||
|
||||
const configStore = useConfigStore();
|
||||
|
||||
|
||||
@@ -78,7 +78,14 @@
|
||||
import { Iphone } from "@element-plus/icons-vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
defineEmits<{ back: []; register: [] }>();
|
||||
defineOptions({ name: "FaLoginMobilePanel" });
|
||||
|
||||
interface Emits {
|
||||
back: [];
|
||||
register: [];
|
||||
}
|
||||
|
||||
defineEmits<Emits>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
@@ -37,7 +37,14 @@
|
||||
import QrcodeVue from "qrcode.vue";
|
||||
import type { Level, RenderAs } from "qrcode.vue";
|
||||
|
||||
defineEmits<{ back: []; register: [] }>();
|
||||
defineOptions({ name: "FaLoginQrPanel" });
|
||||
|
||||
interface Emits {
|
||||
back: [];
|
||||
register: [];
|
||||
}
|
||||
|
||||
defineEmits<Emits>();
|
||||
|
||||
/** 集中配置 + H 级纠错 + SVG(矢量清晰、易对齐容器) */
|
||||
const qrCodeRenderOptions: {
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
clearable
|
||||
:placeholder="$t('login.placeholder.password')"
|
||||
@keyup="checkCapsLock"
|
||||
@keyup.enter="$emit('submit')"
|
||||
>
|
||||
<template #prefix>
|
||||
<ElIcon><Lock /></ElIcon>
|
||||
@@ -51,7 +50,6 @@
|
||||
clearable
|
||||
:placeholder="$t('login.message.password.confirm')"
|
||||
@keyup="checkCapsLock"
|
||||
@keyup.enter="$emit('submit')"
|
||||
>
|
||||
<template #prefix>
|
||||
<ElIcon><Lock /></ElIcon>
|
||||
@@ -104,18 +102,27 @@ import type { FormRules } from "element-plus";
|
||||
|
||||
const registerForm = defineModel<RegisterForm>("registerForm", { required: true });
|
||||
|
||||
defineProps<{
|
||||
defineOptions({ name: "FaLoginRegisterPanel" });
|
||||
|
||||
interface Props {
|
||||
registerRules: FormRules<RegisterForm>;
|
||||
formKey: number | string;
|
||||
registerLoading: boolean;
|
||||
userAgreementHref: string;
|
||||
}>();
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {});
|
||||
|
||||
const registerAgreementReadModel = defineModel<boolean>("registerAgreementRead", {
|
||||
required: true,
|
||||
});
|
||||
|
||||
defineEmits<{ submit: []; toLogin: [] }>();
|
||||
interface Emits {
|
||||
submit: [];
|
||||
toLogin: [];
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const formRef = ref();
|
||||
const isCapsLock = ref(false);
|
||||
@@ -123,6 +130,9 @@ const isCapsLock = ref(false);
|
||||
function checkCapsLock(event: KeyboardEvent) {
|
||||
if (event instanceof KeyboardEvent) {
|
||||
isCapsLock.value = event.getModifierState("CapsLock");
|
||||
if (event.key === "Enter") {
|
||||
emit("submit");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { OAuthProvider } from "@/api/module_system/auth";
|
||||
|
||||
defineEmits<{ oauth: [provider: OAuthProvider] }>();
|
||||
defineOptions({ name: "FaLoginThirdPartySection" });
|
||||
|
||||
interface Emits {
|
||||
oauth: [provider: OAuthProvider];
|
||||
}
|
||||
|
||||
defineEmits<Emits>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ defineSlots<{
|
||||
buttons?: () => void;
|
||||
}>();
|
||||
|
||||
interface ResultPageProps {
|
||||
interface Props {
|
||||
/** 成功/失败 */
|
||||
type: "success" | "fail";
|
||||
/** 标题 */
|
||||
@@ -41,7 +41,7 @@ interface ResultPageProps {
|
||||
iconCode: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<ResultPageProps>(), {
|
||||
withDefaults(defineProps<Props>(), {
|
||||
type: "success",
|
||||
title: "",
|
||||
message: "",
|
||||
|
||||
Reference in New Issue
Block a user