mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 04:46:26 +00:00
+3
-1
@@ -79,4 +79,6 @@ frontend/app/*
|
|||||||
.plan
|
.plan
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
.codebuddy
|
.codebuddy
|
||||||
|
.agent
|
||||||
|
.workbuddy
|
||||||
@@ -160,3 +160,102 @@ src/
|
|||||||
|
|
||||||
- **格式化与校验**:`pnpm lint`
|
- **格式化与校验**:`pnpm lint`
|
||||||
- **提交**:husky、lint-staged、commitlint;可使用 **`pnpm commit`**(Commitizen / cz-git)
|
- **提交**:husky、lint-staged、commitlint;可使用 **`pnpm commit`**(Commitizen / cz-git)
|
||||||
|
|
||||||
|
element-plus 组件库
|
||||||
|
基础组件:
|
||||||
|
el-button 按钮
|
||||||
|
el-container 容器
|
||||||
|
el-header 头部
|
||||||
|
el-aside 侧边栏
|
||||||
|
el-main 主体
|
||||||
|
el-footer 底部
|
||||||
|
el-icon 图标
|
||||||
|
el-row 行布局
|
||||||
|
el-col 列布局单元
|
||||||
|
el-link 链接
|
||||||
|
el-text 文本内容
|
||||||
|
el-scrollbar 滚动条
|
||||||
|
el-space 空格
|
||||||
|
el-splitter、el-splitter-panel 分割面板
|
||||||
|
Typography 排版
|
||||||
|
配置组件:
|
||||||
|
el-config-provider 全局配置
|
||||||
|
Form表单组件:
|
||||||
|
el-autocomplete 自动补全
|
||||||
|
el-cascader 级联选择器
|
||||||
|
el-checkbox 多选框
|
||||||
|
el-color-picker-panel 颜色选择器面板
|
||||||
|
el-color-picker 颜色选择器
|
||||||
|
el-date-picker-panel 日期选择器面板
|
||||||
|
el-radio-group、el-radio-button 日期选择器
|
||||||
|
el-date-picker 日期时间选择器
|
||||||
|
el-time-picker 时间选择器
|
||||||
|
el-time-select 时间选择
|
||||||
|
el-form、el-form-item 表单
|
||||||
|
el-input 输入框
|
||||||
|
el-input-number 数字输入框
|
||||||
|
el-input-tag 标签输入框
|
||||||
|
el-input-otp 一次性密码输入框
|
||||||
|
el-mention 提及
|
||||||
|
el-radio-group、el-radio 单选框
|
||||||
|
el-rate 评分
|
||||||
|
el-select、el-option 选择器
|
||||||
|
el-select-v2 虚拟化选择器
|
||||||
|
el-slider 滑块
|
||||||
|
el-switch 开关按钮
|
||||||
|
el-transfer 穿梭框
|
||||||
|
el-tree-select 树形选择
|
||||||
|
el-upload 上传器
|
||||||
|
Data数据展示组件:
|
||||||
|
el-avatar 头像
|
||||||
|
el-badge 徽章
|
||||||
|
el-calendar 日历
|
||||||
|
el-card 卡片
|
||||||
|
el-carousel 走马灯
|
||||||
|
el-collapse、el-collapse-item 折叠面板
|
||||||
|
el-descriptions、el-descriptions-item 描述列表.
|
||||||
|
el-empty 空状态
|
||||||
|
el-image 图片
|
||||||
|
Infinite Scroll 无限滚动:在要实现滚动加载的列表上添加v-infinite-scroll,并赋值相应的加载方法,可实现滚动到底部时自动执行加载方法。
|
||||||
|
el-pagination 分页
|
||||||
|
el-progress 进度条
|
||||||
|
el-result 结果
|
||||||
|
el-skeleton 骨架屏
|
||||||
|
el-table、el-table-col 表格
|
||||||
|
el-table-v2 虚拟化表格
|
||||||
|
el-tag 标签
|
||||||
|
el-timeline、el-timeline-item 时间线
|
||||||
|
el-tour、el-tour-step 漫游式引导
|
||||||
|
el-tree 树形控件
|
||||||
|
el-tree-v2 虚拟化树形控件
|
||||||
|
el-statistic 统计组件
|
||||||
|
el-segmented 分段控制器
|
||||||
|
导航类组件:
|
||||||
|
el-affix 固钉
|
||||||
|
el-anchor、el-anchor-link 锚点
|
||||||
|
el-backtop 回到顶部按钮
|
||||||
|
el-breadcrumb、el-breadcrumb-item 面包屑
|
||||||
|
el-dropdown、el-dropdown-menu、el-dropdown-item 下拉菜单
|
||||||
|
el-menu、el-sub-menu、el-menu-item-group、el-menu-item 菜单
|
||||||
|
el-page-header 页头
|
||||||
|
el-steps、el-step 步骤条
|
||||||
|
el-tabs、el-tab-pane 标签页
|
||||||
|
Feedback 反馈组件:
|
||||||
|
el-alert 提示
|
||||||
|
el-dialog 对话框
|
||||||
|
el-drawer 抽屉
|
||||||
|
loading:Element Plus 提供了两种调用 Loading 的方法:指令和服务。
|
||||||
|
对于自定义指令 v-loading,只需要绑定 boolean 值即可。
|
||||||
|
默认状况下,Loading 遮罩会插入到绑定元素的子节点。
|
||||||
|
通过添加 body 修饰符,可以使遮罩插入至 Dom 中的 body 上。
|
||||||
|
ElMessage 消息:与 Notification 的区别是后者更多用于系统级通知的被动提醒,默认情况下在顶部显示并在 3 秒后消失。
|
||||||
|
ElMessageBox 消息弹框:
|
||||||
|
ElMessageBox.confirm 方法以打开 confirm 框。它模拟了系统的 confirm 方法。
|
||||||
|
ElMessageBox.prompt 方法以打开 prompt 框。它模拟了系统的 prompt 方法。
|
||||||
|
ElNotification 通知
|
||||||
|
el-popconfirm 气泡确认框
|
||||||
|
el-popover 弹出框
|
||||||
|
el-tooltip 文字提示
|
||||||
|
Others 其他组件:
|
||||||
|
el-divider 分割线
|
||||||
|
el-watermark 水印
|
||||||
|
|||||||
@@ -7,17 +7,7 @@
|
|||||||
shadow: 'never',
|
shadow: 'never',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<ElWatermark
|
<RouterView></RouterView>
|
||||||
:font="{ color: fontColor }"
|
|
||||||
:content="showWatermark ? watermarkContent : ''"
|
|
||||||
:z-index="9999"
|
|
||||||
class="wh-full"
|
|
||||||
>
|
|
||||||
<RouterView></RouterView>
|
|
||||||
|
|
||||||
<!-- AI 助手 -->
|
|
||||||
<FaAiAssistant v-if="enableAiAssistant" />
|
|
||||||
</ElWatermark>
|
|
||||||
</ElConfigProvider>
|
</ElConfigProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -25,15 +15,12 @@
|
|||||||
import { computed, onBeforeMount, onErrorCaptured, onMounted, onUnmounted } from "vue";
|
import { computed, onBeforeMount, onErrorCaptured, onMounted, onUnmounted } from "vue";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { useWindowSize } from "@vueuse/core";
|
import { useWindowSize } from "@vueuse/core";
|
||||||
import { useAppStore, useUserStore } from "./store";
|
import { useAppStore } from "./store";
|
||||||
import { useSettingsStore } from "./store/modules/setting.store";
|
|
||||||
import { defaultSettings } from "./config/setting";
|
|
||||||
import { ComponentSize } from "./enums/settings/layout.enum";
|
import { ComponentSize } from "./enums/settings/layout.enum";
|
||||||
import { MOBILE_BREAKPOINT } from "./utils/constants/definitions";
|
import { MOBILE_BREAKPOINT } from "./utils/constants/definitions";
|
||||||
import { hexToRgba, toggleTransition } from "./utils/ui";
|
import { toggleTransition } from "./utils/ui";
|
||||||
import { initializeTheme } from "./hooks/core/useTheme";
|
import { initializeTheme } from "./hooks/core/useTheme";
|
||||||
import { useAppBootstrap } from "@/hooks/core/useAppBootstrap";
|
import { useAppBootstrap } from "@/hooks/core/useAppBootstrap";
|
||||||
import { ThemeMode } from "./enums";
|
|
||||||
import en from "element-plus/es/locale/lang/en";
|
import en from "element-plus/es/locale/lang/en";
|
||||||
import zhCn from "element-plus/es/locale/lang/zh-cn";
|
import zhCn from "element-plus/es/locale/lang/zh-cn";
|
||||||
import { router } from "@/router";
|
import { router } from "@/router";
|
||||||
@@ -41,8 +28,6 @@ import { ElNotification } from "element-plus";
|
|||||||
import { initIconifyAsync } from "./plugins/iconify";
|
import { initIconifyAsync } from "./plugins/iconify";
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const settingsStore = useSettingsStore();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const { width } = useWindowSize();
|
const { width } = useWindowSize();
|
||||||
|
|
||||||
// H5 用小尺寸,桌面用用户设置的大小
|
// H5 用小尺寸,桌面用用户设置的大小
|
||||||
@@ -50,32 +35,12 @@ const size = computed(() => {
|
|||||||
if (width.value < MOBILE_BREAKPOINT) return "small" as ComponentSize;
|
if (width.value < MOBILE_BREAKPOINT) return "small" as ComponentSize;
|
||||||
return appStore.size as ComponentSize;
|
return appStore.size as ComponentSize;
|
||||||
});
|
});
|
||||||
const showWatermark = computed(() => settingsStore.showWatermark);
|
|
||||||
const watermarkContent = defaultSettings.watermarkContent;
|
|
||||||
|
|
||||||
// 根据语言设置返回对应的语言包
|
// 根据语言设置返回对应的语言包
|
||||||
const locale = computed(() => {
|
const locale = computed(() => {
|
||||||
return appStore.language === "en" ? en : zhCn;
|
return appStore.language === "en" ? en : zhCn;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 只有在启用 AI 助手且用户已登录时才显示
|
|
||||||
const enableAiAssistant = computed(() => {
|
|
||||||
const isEnabled = settingsStore.userEnableAi;
|
|
||||||
const isLoggedIn = userStore.basicInfo && Object.keys(userStore.basicInfo).length > 0;
|
|
||||||
return isEnabled && isLoggedIn;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 水印文字默认使用当前主题色(半透明),随主题色设置变化
|
|
||||||
const fontColor = computed(() => {
|
|
||||||
const hex = settingsStore.themeColor || defaultSettings.themeColor;
|
|
||||||
const alpha = settingsStore.theme === ThemeMode.DARK ? 0.22 : 0.16;
|
|
||||||
try {
|
|
||||||
return hexToRgba(hex, alpha).rgba;
|
|
||||||
} catch {
|
|
||||||
return hexToRgba(defaultSettings.themeColor, alpha).rgba;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用根组件生命周期:
|
* 应用根组件生命周期:
|
||||||
*
|
*
|
||||||
|
|||||||
+7
-6
@@ -20,7 +20,6 @@
|
|||||||
:tool="true"
|
:tool="true"
|
||||||
:show-preview="true"
|
:show-preview="true"
|
||||||
:original-graph="false"
|
:original-graph="false"
|
||||||
:file-type="cropFileType"
|
|
||||||
:title="cropInnerTitle"
|
:title="cropInnerTitle"
|
||||||
:preview-title="cropPreviewTitle"
|
:preview-title="cropPreviewTitle"
|
||||||
@update:img-url="onCropConfirm"
|
@update:img-url="onCropConfirm"
|
||||||
@@ -129,7 +128,6 @@ interface Props {
|
|||||||
cropCutWidth?: number;
|
cropCutWidth?: number;
|
||||||
cropCutHeight?: number;
|
cropCutHeight?: number;
|
||||||
cropQuality?: number;
|
cropQuality?: number;
|
||||||
cropFileType?: "png" | "jpeg" | "webp";
|
|
||||||
cropDialogTitle?: string;
|
cropDialogTitle?: string;
|
||||||
cropInnerTitle?: string;
|
cropInnerTitle?: string;
|
||||||
cropPreviewTitle?: string;
|
cropPreviewTitle?: string;
|
||||||
@@ -151,7 +149,6 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
cropCutWidth: 360,
|
cropCutWidth: 360,
|
||||||
cropCutHeight: 200,
|
cropCutHeight: 200,
|
||||||
cropQuality: 1,
|
cropQuality: 1,
|
||||||
cropFileType: "jpeg",
|
|
||||||
cropDialogTitle: "裁剪图片",
|
cropDialogTitle: "裁剪图片",
|
||||||
cropInnerTitle: "调整图片",
|
cropInnerTitle: "调整图片",
|
||||||
cropPreviewTitle: "预览",
|
cropPreviewTitle: "预览",
|
||||||
@@ -167,6 +164,7 @@ const internalFileList = ref<UploadUserFile[]>([]);
|
|||||||
|
|
||||||
const cropVisible = ref(false);
|
const cropVisible = ref(false);
|
||||||
const cropSourceUrl = ref("");
|
const cropSourceUrl = ref("");
|
||||||
|
const cropFileName = ref("");
|
||||||
|
|
||||||
function revokeCropUrl() {
|
function revokeCropUrl() {
|
||||||
if (cropSourceUrl.value.startsWith("blob:")) {
|
if (cropSourceUrl.value.startsWith("blob:")) {
|
||||||
@@ -185,9 +183,7 @@ function onCropError() {
|
|||||||
|
|
||||||
async function onCropConfirm(dataURL: string) {
|
async function onCropConfirm(dataURL: string) {
|
||||||
try {
|
try {
|
||||||
const ext =
|
const file = dataURLToFile(dataURL, cropFileName.value);
|
||||||
props.cropFileType === "png" ? "png" : props.cropFileType === "webp" ? "webp" : "jpg";
|
|
||||||
const file = dataURLToFile(dataURL, `upload.${ext}`);
|
|
||||||
await uploadFileInternal(file);
|
await uploadFileInternal(file);
|
||||||
cropVisible.value = false;
|
cropVisible.value = false;
|
||||||
revokeCropUrl();
|
revokeCropUrl();
|
||||||
@@ -293,6 +289,11 @@ function handleBeforeUpload(file: UploadRawFile) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (props.enableCrop) {
|
if (props.enableCrop) {
|
||||||
|
// 非 canvas 兼容格式(如 svg/gif/bmp 等),跳过裁剪直接上传
|
||||||
|
if (file.type !== "image/png" && file.type !== "image/jpeg" && file.type !== "image/webp") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
cropFileName.value = file.name;
|
||||||
revokeCropUrl();
|
revokeCropUrl();
|
||||||
cropSourceUrl.value = URL.createObjectURL(file);
|
cropSourceUrl.value = URL.createObjectURL(file);
|
||||||
cropVisible.value = true;
|
cropVisible.value = true;
|
||||||
@@ -95,7 +95,7 @@ import {
|
|||||||
ElSkeletonItem,
|
ElSkeletonItem,
|
||||||
ElScrollbar,
|
ElScrollbar,
|
||||||
} from "element-plus";
|
} from "element-plus";
|
||||||
import FaPagination from "@/components/others/fa-pagination/index.vue";
|
import FaPagination from "@/components/tables/fa-pagination/index.vue";
|
||||||
|
|
||||||
defineOptions({ name: "FaCardGrid" });
|
defineOptions({ name: "FaCardGrid" });
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fa-carousel">
|
||||||
|
<ElCarousel
|
||||||
|
v-bind="$attrs"
|
||||||
|
:height="height"
|
||||||
|
:indicator-position="indicatorPosition"
|
||||||
|
:autoplay="autoplay"
|
||||||
|
:interval="interval"
|
||||||
|
:type="type"
|
||||||
|
>
|
||||||
|
<ElCarouselItem v-for="item in items" :key="item.value">
|
||||||
|
<div
|
||||||
|
class="fa-carousel__item h-full flex items-center justify-center"
|
||||||
|
:style="{ backgroundColor: item.color || '#f0f0f0' }"
|
||||||
|
>
|
||||||
|
<template v-if="item.image">
|
||||||
|
<img
|
||||||
|
:src="item.image"
|
||||||
|
:alt="item.label"
|
||||||
|
class="max-h-full max-w-full object-contain"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="fa-carousel__content text-center px-4">
|
||||||
|
<h3 class="text-lg font-medium mb-2">{{ item.label }}</h3>
|
||||||
|
<p v-if="item.description" class="text-sm text-gray-500">
|
||||||
|
{{ item.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</ElCarouselItem>
|
||||||
|
<slot name="default" />
|
||||||
|
</ElCarousel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: "FaCarousel" });
|
||||||
|
|
||||||
|
interface CarouselItem {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
image?: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
items: CarouselItem[];
|
||||||
|
height?: string;
|
||||||
|
indicatorPosition?: "none" | "outside";
|
||||||
|
autoplay?: boolean;
|
||||||
|
interval?: number;
|
||||||
|
type?: "card";
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
height: "300px",
|
||||||
|
indicatorPosition: undefined,
|
||||||
|
autoplay: true,
|
||||||
|
interval: 4000,
|
||||||
|
type: undefined,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
+2
-3
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="json-pretty-wrapper" :style="{ maxHeight: height }">
|
<ElScrollbar class="json-pretty-wrapper" :max-height="height">
|
||||||
<VueJsonPretty
|
<VueJsonPretty
|
||||||
v-if="isJson"
|
v-if="isJson"
|
||||||
:data="parsed"
|
:data="parsed"
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
:deep="3"
|
:deep="3"
|
||||||
/>
|
/>
|
||||||
<pre v-else class="json-pretty-fallback">{{ displayText }}</pre>
|
<pre v-else class="json-pretty-fallback">{{ displayText }}</pre>
|
||||||
</div>
|
</ElScrollbar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -52,7 +52,6 @@ const displayText = computed(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.json-pretty-wrapper {
|
.json-pretty-wrapper {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
overflow: auto;
|
|
||||||
background-color: var(--el-fill-color-blank);
|
background-color: var(--el-fill-color-blank);
|
||||||
border: 1px solid var(--el-border-color-lighter);
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<ElPageHeader v-bind="$attrs" :icon="iconComponent" :content="content" @back="handleBack">
|
||||||
|
<template #default>
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<slot name="extra" />
|
||||||
|
</template>
|
||||||
|
<template #breadcrumb>
|
||||||
|
<slot name="breadcrumb" />
|
||||||
|
</template>
|
||||||
|
</ElPageHeader>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { ArrowLeft } from "@element-plus/icons-vue";
|
||||||
|
import type { Component } from "vue";
|
||||||
|
|
||||||
|
defineOptions({ name: "FaPageHeader" });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 返回图标(ElPageHeader 的 icon 属性) */
|
||||||
|
icon?: string;
|
||||||
|
/** 标题内容(ElPageHeader 的 content 属性) */
|
||||||
|
content?: string;
|
||||||
|
/** 返回回调 */
|
||||||
|
onBack?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
icon: "ArrowLeft",
|
||||||
|
content: "",
|
||||||
|
onBack: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconMap: Record<string, Component> = {
|
||||||
|
ArrowLeft,
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconComponent = computed(() => {
|
||||||
|
if (!props.icon) return undefined;
|
||||||
|
return iconMap[props.icon] ?? props.icon;
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleBack() {
|
||||||
|
props.onBack?.();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
+17
-15
@@ -1,29 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-content box-border px-20! py-3.5 text-center max-md:px-5!" :class="type">
|
<div class="page-content box-border px-20! py-3.5 text-center max-md:px-5!">
|
||||||
<FaSvgIcon
|
<ElResult :icon="resultIcon" :title="title" :sub-title="message">
|
||||||
class="icon size-22 p-2 mt-16 block rounded-full text-white!"
|
<template #icon>
|
||||||
:icon="iconCode"
|
<FaSvgIcon
|
||||||
:class="type === 'success' ? 'bg-[#19BE6B]' : 'bg-[#ED4014]'"
|
class="size-14!"
|
||||||
/>
|
:icon="iconCode"
|
||||||
<h1 class="title mt-8 text-3xl font-medium text-g-900! max-md:mt-2.5 max-md:text-2xl">
|
/>
|
||||||
{{ title }}
|
</template>
|
||||||
</h1>
|
<template #extra>
|
||||||
<p class="msg mt-5 text-base text-g-600">{{ message }}</p>
|
<slot name="buttons"></slot>
|
||||||
|
</template>
|
||||||
|
</ElResult>
|
||||||
<div
|
<div
|
||||||
class="res mt-7.5 rounded bg-g-200/80 dark:bg-g-300/40 px-7.5 py-5.5 text-left max-md:px-7.5 max-md:py-2.5 [&_p]:flex [&_p]:items-center [&_p]:py-2 [&_p]:text-sm [&_p]:text-[#808695] [&_p_i]:mr-1.5"
|
class="res mt-7.5 rounded bg-g-200/80 dark:bg-g-300/40 px-7.5 py-5.5 text-left max-md:px-7.5 max-md:py-2.5 [&_p]:flex [&_p]:items-center [&_p]:py-2 [&_p]:text-sm [&_p]:text-[#808695] [&_p_i]:mr-1.5"
|
||||||
>
|
>
|
||||||
<slot name="content"></slot>
|
<slot name="content"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group mt-12.5">
|
|
||||||
<slot name="buttons"></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
defineOptions({ name: "FaResultPage" });
|
defineOptions({ name: "FaResultPage" });
|
||||||
|
|
||||||
// 显式声明插槽类型
|
|
||||||
defineSlots<{
|
defineSlots<{
|
||||||
default?: () => void;
|
default?: () => void;
|
||||||
content?: () => void;
|
content?: () => void;
|
||||||
@@ -41,10 +41,12 @@ interface Props {
|
|||||||
iconCode: string;
|
iconCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "",
|
title: "",
|
||||||
message: "",
|
message: "",
|
||||||
iconCode: "",
|
iconCode: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const resultIcon = computed(() => (props.type === "success" ? "success" : "error"));
|
||||||
</script>
|
</script>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<template>
|
||||||
|
<ElStatistic
|
||||||
|
:value="displayValue"
|
||||||
|
:title="title"
|
||||||
|
:precision="precision"
|
||||||
|
:value-style="valueStyle"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<slot name="title">{{ title }}</slot>
|
||||||
|
</template>
|
||||||
|
<template #prefix>
|
||||||
|
<slot name="prefix">{{ prefix }}</slot>
|
||||||
|
</template>
|
||||||
|
<template #suffix>
|
||||||
|
<slot name="suffix">{{ suffix }}</slot>
|
||||||
|
</template>
|
||||||
|
<template v-if="$slots.default" #default>
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
|
</ElStatistic>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, computed } from "vue";
|
||||||
|
|
||||||
|
defineOptions({ name: "FaStatistic" });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 数值 */
|
||||||
|
value: number;
|
||||||
|
/** 标题 */
|
||||||
|
title?: string;
|
||||||
|
/** 数值前缀 */
|
||||||
|
prefix?: string;
|
||||||
|
/** 数值后缀 */
|
||||||
|
suffix?: string;
|
||||||
|
/** 小数位数 */
|
||||||
|
precision?: number;
|
||||||
|
/** 字体颜色 */
|
||||||
|
color?: string;
|
||||||
|
/** 是否启用数字滚动动画 */
|
||||||
|
animated?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: "",
|
||||||
|
prefix: "",
|
||||||
|
suffix: "",
|
||||||
|
precision: 0,
|
||||||
|
color: "",
|
||||||
|
animated: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayValue = ref(props.animated ? 0 : props.value);
|
||||||
|
|
||||||
|
const valueStyle = computed(() => {
|
||||||
|
const style: Record<string, any> = {};
|
||||||
|
if (props.color) {
|
||||||
|
style.color = props.color;
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.value,
|
||||||
|
(newVal) => {
|
||||||
|
if (!props.animated) {
|
||||||
|
displayValue.value = newVal;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const start = displayValue.value;
|
||||||
|
const duration = 800;
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
function step(currentTime: number) {
|
||||||
|
const elapsed = currentTime - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
displayValue.value = start + (newVal - start) * progress;
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(step);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<!-- 自动完成组件 -->
|
||||||
|
<template>
|
||||||
|
<div class="fa-autocomplete">
|
||||||
|
<ElAutocomplete
|
||||||
|
v-model="inputValue"
|
||||||
|
:fetch-suggestions="onSearch"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:debounce="debounce"
|
||||||
|
:clearable="clearable"
|
||||||
|
v-bind="$attrs"
|
||||||
|
@select="handleSelect"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
|
||||||
|
defineOptions({ name: "FaAutocomplete" });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
fetchSuggestions?: (queryString: string) => Promise<any[]>;
|
||||||
|
debounce?: number;
|
||||||
|
clearable?: boolean;
|
||||||
|
valueKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: "",
|
||||||
|
placeholder: "请输入",
|
||||||
|
debounce: 300,
|
||||||
|
clearable: true,
|
||||||
|
valueKey: "value",
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
"update:modelValue": [value: string];
|
||||||
|
select: [item: any];
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const inputValue = ref(props.modelValue);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(newVal) => {
|
||||||
|
inputValue.value = newVal;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(inputValue, (newVal) => {
|
||||||
|
emit("update:modelValue", newVal);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onSearch(queryString: string, cb: (results: any[]) => void) {
|
||||||
|
if (props.fetchSuggestions) {
|
||||||
|
const results = await props.fetchSuggestions(queryString);
|
||||||
|
cb(results);
|
||||||
|
} else {
|
||||||
|
cb([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSelect(item: any) {
|
||||||
|
emit("select", item);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fa-cascader">
|
||||||
|
<ElCascader
|
||||||
|
v-model="selectedValue"
|
||||||
|
:options="options"
|
||||||
|
:props="cascaderProps"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:clearable="clearable"
|
||||||
|
:filterable="filterable"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'FaCascader'
|
||||||
|
})
|
||||||
|
|
||||||
|
interface FaCascaderProps {
|
||||||
|
modelValue?: any
|
||||||
|
options?: any[]
|
||||||
|
placeholder?: string
|
||||||
|
clearable?: boolean
|
||||||
|
filterable?: boolean
|
||||||
|
showAllLevels?: boolean
|
||||||
|
separator?: string
|
||||||
|
checkStrictly?: boolean
|
||||||
|
multiple?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<FaCascaderProps>(), {
|
||||||
|
modelValue: () => [],
|
||||||
|
options: () => [],
|
||||||
|
placeholder: '请选择',
|
||||||
|
clearable: true,
|
||||||
|
filterable: true,
|
||||||
|
showAllLevels: true,
|
||||||
|
separator: '/',
|
||||||
|
checkStrictly: false,
|
||||||
|
multiple: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: any): void
|
||||||
|
(e: 'change', value: any): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const cascaderProps = computed(() => ({
|
||||||
|
checkStrictly: props.checkStrictly,
|
||||||
|
multiple: props.multiple,
|
||||||
|
emitPath: false
|
||||||
|
}))
|
||||||
|
|
||||||
|
const selectedValue = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value: any) => {
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
emit('change', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fa-input-tag">
|
||||||
|
<ElInputTag
|
||||||
|
v-model="tagValue"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:clearable="clearable"
|
||||||
|
:max="max"
|
||||||
|
:disabled="disabled"
|
||||||
|
:size="size"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<template v-if="$slots.prefix" #prefix><slot name="prefix" /></template>
|
||||||
|
</ElInputTag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: "FaInputTag" });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: string[];
|
||||||
|
placeholder?: string;
|
||||||
|
clearable?: boolean;
|
||||||
|
max?: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
size?: "large" | "default" | "small";
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: () => [],
|
||||||
|
placeholder: "请输入标签",
|
||||||
|
clearable: true,
|
||||||
|
disabled: false,
|
||||||
|
size: "default",
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: "update:modelValue", value: string[]): void;
|
||||||
|
(e: "change", value: string[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const tagValue = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value: string[]) => {
|
||||||
|
emit("update:modelValue", value);
|
||||||
|
emit("change", value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fa-mention">
|
||||||
|
<ElMention
|
||||||
|
v-model="content"
|
||||||
|
:options="mergedOptions"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:prefix="prefix"
|
||||||
|
v-bind="$attrs"
|
||||||
|
@select="handleSelect"
|
||||||
|
@search="handleSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
|
||||||
|
defineOptions({ name: "FaMention" });
|
||||||
|
|
||||||
|
interface MentionOption {
|
||||||
|
value?: string;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: string;
|
||||||
|
options?: MentionOption[];
|
||||||
|
placeholder?: string;
|
||||||
|
prefix?: string;
|
||||||
|
fetchOptions?: (query: string) => Promise<MentionOption[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: "",
|
||||||
|
options: () => [],
|
||||||
|
placeholder: "@提及",
|
||||||
|
prefix: "@",
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: "update:modelValue", val: string): void;
|
||||||
|
(e: "select", val: MentionOption): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const content = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val: string) => emit("update:modelValue", val),
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchedOptions = ref<MentionOption[]>([]);
|
||||||
|
|
||||||
|
const mergedOptions = computed<MentionOption[]>(() => {
|
||||||
|
if (fetchedOptions.value.length > 0) {
|
||||||
|
return fetchedOptions.value;
|
||||||
|
}
|
||||||
|
return props.options;
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSelect(val: MentionOption) {
|
||||||
|
fetchedOptions.value = [];
|
||||||
|
emit("select", val);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSearch(query: string) {
|
||||||
|
if (props.fetchOptions && query) {
|
||||||
|
try {
|
||||||
|
const result = await props.fetchOptions(query);
|
||||||
|
fetchedOptions.value = result;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[FaMention] fetchOptions error:", err);
|
||||||
|
fetchedOptions.value = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fetchedOptions.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fa-rate">
|
||||||
|
<ElRate
|
||||||
|
v-model="rateValue"
|
||||||
|
:max="max"
|
||||||
|
:disabled="disabled"
|
||||||
|
:allow-half="allowHalf"
|
||||||
|
:show-text="showText"
|
||||||
|
:show-score="showScore"
|
||||||
|
:texts="texts"
|
||||||
|
:colors="colors"
|
||||||
|
:low-threshold="lowThreshold"
|
||||||
|
:high-threshold="highThreshold"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'FaRate',
|
||||||
|
})
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: number
|
||||||
|
max?: number
|
||||||
|
disabled?: boolean
|
||||||
|
allowHalf?: boolean
|
||||||
|
showText?: boolean
|
||||||
|
showScore?: boolean
|
||||||
|
texts?: string[]
|
||||||
|
colors?: string[]
|
||||||
|
lowThreshold?: number
|
||||||
|
highThreshold?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: 0,
|
||||||
|
max: 5,
|
||||||
|
disabled: false,
|
||||||
|
allowHalf: false,
|
||||||
|
showText: false,
|
||||||
|
showScore: false,
|
||||||
|
texts: () => [],
|
||||||
|
colors: () => [],
|
||||||
|
lowThreshold: 2,
|
||||||
|
highThreshold: 4,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: number]
|
||||||
|
change: [value: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const rateValue = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val: number) => {
|
||||||
|
emit('update:modelValue', val)
|
||||||
|
emit('change', val)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ISelectConfig } from "@/components/others/fa-table-select/index.vue";
|
import type { ISelectConfig } from "@/components/tables/fa-table-select/index.vue";
|
||||||
import FaTableSelect from "@/components/others/fa-table-select/index.vue";
|
import FaTableSelect from "@/components/tables/fa-table-select/index.vue";
|
||||||
import UserAPI, { UserPageQuery } from "@/api/module_system/user";
|
import UserAPI, { UserPageQuery } from "@/api/module_system/user";
|
||||||
|
|
||||||
defineOptions({ name: "FaUserTableSelect" });
|
defineOptions({ name: "FaUserTableSelect" });
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fa-select-v2">
|
||||||
|
<ElSelectV2
|
||||||
|
v-model="selectedValue"
|
||||||
|
:options="options"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:clearable="clearable"
|
||||||
|
:filterable="filterable"
|
||||||
|
:multiple="multiple"
|
||||||
|
:collapse-tags="collapseTags"
|
||||||
|
v-bind="$attrs"
|
||||||
|
@change="handleChange"
|
||||||
|
>
|
||||||
|
<template v-if="$slots.prefix" #prefix><slot name="prefix" /></template>
|
||||||
|
<template v-if="$slots.empty" #empty><slot name="empty" /></template>
|
||||||
|
</ElSelectV2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
defineOptions({ name: "FaSelectV2" });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: any;
|
||||||
|
options?: any[];
|
||||||
|
placeholder?: string;
|
||||||
|
clearable?: boolean;
|
||||||
|
filterable?: boolean;
|
||||||
|
multiple?: boolean;
|
||||||
|
collapseTags?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: "",
|
||||||
|
options: () => [],
|
||||||
|
placeholder: "请选择",
|
||||||
|
clearable: true,
|
||||||
|
filterable: true,
|
||||||
|
multiple: false,
|
||||||
|
collapseTags: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
"update:modelValue": [value: any];
|
||||||
|
change: [value: any];
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const selectedValue = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit("update:modelValue", val),
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleChange(val: any) {
|
||||||
|
emit("change", val);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fa-slider">
|
||||||
|
<ElSlider
|
||||||
|
v-model="sliderValue"
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
|
:step="step"
|
||||||
|
:disabled="disabled"
|
||||||
|
:show-input="showInput"
|
||||||
|
:show-input-controls="showInputControls"
|
||||||
|
:show-stops="showStops"
|
||||||
|
:show-tooltip="showTooltip"
|
||||||
|
:range="range"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: "FaSlider" });
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", value: number | number[]): void;
|
||||||
|
(e: "change", value: number | number[]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: number | number[];
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
showInput?: boolean;
|
||||||
|
showInputControls?: boolean;
|
||||||
|
showStops?: boolean;
|
||||||
|
showTooltip?: boolean;
|
||||||
|
range?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: 0,
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
step: 1,
|
||||||
|
disabled: false,
|
||||||
|
showInput: false,
|
||||||
|
showInputControls: true,
|
||||||
|
showStops: false,
|
||||||
|
showTooltip: true,
|
||||||
|
range: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sliderValue = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val: number | number[]) => {
|
||||||
|
emit("update:modelValue", val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.fa-slider {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fa-time-select">
|
||||||
|
<ElTimeSelect
|
||||||
|
v-model="timeValue"
|
||||||
|
:start="start"
|
||||||
|
:end="end"
|
||||||
|
:step="step"
|
||||||
|
:min-time="minTime"
|
||||||
|
:max-time="maxTime"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:clearable="clearable"
|
||||||
|
:disabled="disabled"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'FaTimeSelect'
|
||||||
|
})
|
||||||
|
|
||||||
|
interface FaTimeSelectProps {
|
||||||
|
modelValue?: string
|
||||||
|
start?: string
|
||||||
|
end?: string
|
||||||
|
step?: string
|
||||||
|
minTime?: string
|
||||||
|
maxTime?: string
|
||||||
|
placeholder?: string
|
||||||
|
clearable?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<FaTimeSelectProps>(), {
|
||||||
|
modelValue: '',
|
||||||
|
start: '09:00',
|
||||||
|
end: '18:00',
|
||||||
|
step: '00:30',
|
||||||
|
placeholder: '请选择时间',
|
||||||
|
clearable: true,
|
||||||
|
disabled: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: string): void
|
||||||
|
(e: 'change', value: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const timeValue = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value: string) => {
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
emit('change', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fa-transfer">
|
||||||
|
<ElTransfer
|
||||||
|
v-model="selected"
|
||||||
|
:data="transferData"
|
||||||
|
:titles="titles"
|
||||||
|
:filterable="filterable"
|
||||||
|
:filter-placeholder="filterPlaceholder"
|
||||||
|
:props="transferProps"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: "FaTransfer" });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: any[];
|
||||||
|
data?: any[];
|
||||||
|
titles?: [string, string];
|
||||||
|
filterable?: boolean;
|
||||||
|
filterPlaceholder?: string;
|
||||||
|
keyProp?: string;
|
||||||
|
labelProp?: string;
|
||||||
|
disabledProp?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: () => [],
|
||||||
|
data: () => [],
|
||||||
|
titles: () => ["待选择", "已选择"],
|
||||||
|
filterable: true,
|
||||||
|
keyProp: "value",
|
||||||
|
labelProp: "label",
|
||||||
|
disabledProp: "disabled",
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:modelValue": [value: any[]];
|
||||||
|
change: [value: any[], direction: "left" | "right", movedKeys: any[]];
|
||||||
|
"left-check-change": [keys: any[], checkedKeys: any[]];
|
||||||
|
"right-check-change": [keys: any[], checkedKeys: any[]];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const transferProps = computed(() => ({
|
||||||
|
key: props.keyProp,
|
||||||
|
label: props.labelProp,
|
||||||
|
disabled: props.disabledProp,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const selected = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val: any[]) => {
|
||||||
|
emit("update:modelValue", val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const transferData = computed(() => {
|
||||||
|
return props.data.map((item: any) => ({
|
||||||
|
key: item[props.keyProp],
|
||||||
|
label: item[props.labelProp],
|
||||||
|
disabled: item[props.disabledProp],
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
import type { DialogProps } from "element-plus";
|
import type { DialogProps } from "element-plus";
|
||||||
import { ElDialog } from "element-plus";
|
import { ElDialog } from "element-plus";
|
||||||
import { computed, ref, useAttrs, watch, onMounted, onUnmounted } from "vue";
|
import { computed, ref, useAttrs, watch, onMounted, onUnmounted } from "vue";
|
||||||
import FaIconButton from "@/components/others/fa-icon-button/index.vue";
|
import FaIconButton from "@/components/actions/fa-icon-button/index.vue";
|
||||||
|
|
||||||
defineOptions({ name: "FaDialog", inheritAttrs: false });
|
defineOptions({ name: "FaDialog", inheritAttrs: false });
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { DrawerProps } from "element-plus";
|
import type { DrawerProps } from "element-plus";
|
||||||
import { computed, useAttrs, onMounted, onUnmounted } from "vue";
|
import { computed, useAttrs, onMounted, onUnmounted } from "vue";
|
||||||
import FaIconButton from "@/components/others/fa-icon-button/index.vue";
|
import FaIconButton from "@/components/actions/fa-icon-button/index.vue";
|
||||||
|
|
||||||
defineOptions({ name: "FaDrawer", inheritAttrs: false });
|
defineOptions({ name: "FaDrawer", inheritAttrs: false });
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fa-anchor">
|
||||||
|
<ElAnchor :container-id="containerId" :offset="offset" :affix="affix" v-bind="$attrs">
|
||||||
|
<slot />
|
||||||
|
</ElAnchor>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: "FaAnchor" });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 滚动容器的 CSS 选择器 */
|
||||||
|
containerId: string;
|
||||||
|
/** 锚点偏移距离 */
|
||||||
|
offset?: number;
|
||||||
|
/** 是否固定 */
|
||||||
|
affix?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
offset: 0,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<ElMenuItemGroup :title="title">
|
||||||
|
<slot />
|
||||||
|
</ElMenuItemGroup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: "FaMenuItemGroup" });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 分组标题 */
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
</script>
|
||||||
+1
-1
@@ -47,7 +47,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, nextTick, shallowRef } from "vue";
|
import { ref, watch, nextTick, shallowRef } from "vue";
|
||||||
import { Search, Switch as SwitchIcon } from "@element-plus/icons-vue";
|
import { Search, Switch as SwitchIcon } from "@element-plus/icons-vue";
|
||||||
import FaMenuRouteIcon from "@/components/others/fa-menu-route-icon/index.vue";
|
import FaMenuRouteIcon from "@/components/navigation/fa-menu-route-icon/index.vue";
|
||||||
|
|
||||||
defineOptions({ name: "FaMenuTreeTable" });
|
defineOptions({ name: "FaMenuTreeTable" });
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fa-affix">
|
||||||
|
<ElAffix :offset="offset" :target="target" :disabled="disabled">
|
||||||
|
<slot />
|
||||||
|
</ElAffix>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: "FaAffix" });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 固钉偏移距离 */
|
||||||
|
offset?: number;
|
||||||
|
/** 固钉定位的父级 CSS 选择器 */
|
||||||
|
target?: string;
|
||||||
|
/** 是否禁用固钉 */
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), { offset: 0 });
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-infinite-scroll="handleScroll"
|
||||||
|
:infinite-scroll-disabled="disabled"
|
||||||
|
:infinite-scroll-distance="distance"
|
||||||
|
:infinite-scroll-immediate="immediate"
|
||||||
|
class="fa-infinite-scroll"
|
||||||
|
:style="{ height: height, overflow: 'auto' }"
|
||||||
|
>
|
||||||
|
<template v-if="loading && !$slots.loading">
|
||||||
|
<div class="flex justify-center py-4">
|
||||||
|
<ElIcon class="is-loading"><Loading /></ElIcon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<slot />
|
||||||
|
<div v-if="noMore && showNoMore" class="fa-infinite-scroll__empty text-center py-4 text-sm text-gray-400">
|
||||||
|
{{ noMoreText }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Loading } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
defineOptions({ name: "FaInfiniteScroll" });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 容器高度 */
|
||||||
|
height?: string;
|
||||||
|
/** 是否禁用 */
|
||||||
|
disabled?: boolean;
|
||||||
|
/** 触发加载的距离阈值 */
|
||||||
|
distance?: number;
|
||||||
|
/** 是否立即触发加载 */
|
||||||
|
immediate?: boolean;
|
||||||
|
/** 是否正在加载 */
|
||||||
|
loading?: boolean;
|
||||||
|
/** 是否没有更多数据 */
|
||||||
|
noMore?: boolean;
|
||||||
|
/** 是否显示"没有更多了"提示 */
|
||||||
|
showNoMore?: boolean;
|
||||||
|
/** 没有更多数据时的提示文本 */
|
||||||
|
noMoreText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
height: "300px",
|
||||||
|
disabled: false,
|
||||||
|
distance: 100,
|
||||||
|
immediate: true,
|
||||||
|
loading: false,
|
||||||
|
noMore: false,
|
||||||
|
showNoMore: true,
|
||||||
|
noMoreText: "没有更多了",
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
load: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
emit("load");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fa-transfer">
|
||||||
|
<ElTransfer
|
||||||
|
v-model="selected"
|
||||||
|
:data="transferData"
|
||||||
|
:titles="titles"
|
||||||
|
:filterable="filterable"
|
||||||
|
:filter-placeholder="filterPlaceholder"
|
||||||
|
:props="transferProps"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
defineOptions({ name: "FaTransfer" });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: any[];
|
||||||
|
data?: any[];
|
||||||
|
titles?: [string, string];
|
||||||
|
filterable?: boolean;
|
||||||
|
filterPlaceholder?: string;
|
||||||
|
keyProp?: string;
|
||||||
|
labelProp?: string;
|
||||||
|
disabledProp?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: () => [],
|
||||||
|
data: () => [],
|
||||||
|
titles: () => ["待选择", "已选择"],
|
||||||
|
filterable: true,
|
||||||
|
keyProp: "value",
|
||||||
|
labelProp: "label",
|
||||||
|
disabledProp: "disabled",
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:modelValue": [value: any[]];
|
||||||
|
change: [value: any[], direction: "left" | "right", movedKeys: any[]];
|
||||||
|
"left-check-change": [keys: any[], checkedKeys: any[]];
|
||||||
|
"right-check-change": [keys: any[], checkedKeys: any[]];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const transferProps = computed(() => ({
|
||||||
|
key: props.keyProp,
|
||||||
|
label: props.labelProp,
|
||||||
|
disabled: props.disabledProp,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const selected = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val: any[]) => {
|
||||||
|
emit("update:modelValue", val);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const transferData = computed(() => {
|
||||||
|
return props.data.map((item: any) => ({
|
||||||
|
key: item[props.keyProp],
|
||||||
|
label: item[props.labelProp],
|
||||||
|
disabled: item[props.disabledProp],
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fa-tree-v2" :style="{ height: height }">
|
||||||
|
<ElTreeV2
|
||||||
|
:data="data"
|
||||||
|
:props="treeProps"
|
||||||
|
:height="computedHeight"
|
||||||
|
:highlight-current="highlightCurrent"
|
||||||
|
:default-expand-all="defaultExpandAll"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</ElTreeV2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
defineOptions({ name: "FaTreeV2" });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data?: Record<string, any>[];
|
||||||
|
height?: string;
|
||||||
|
emptyText?: string;
|
||||||
|
highlightCurrent?: boolean;
|
||||||
|
defaultExpandAll?: boolean;
|
||||||
|
nodeKey?: string;
|
||||||
|
/** 节点 label 字段名 */
|
||||||
|
label?: string;
|
||||||
|
/** 节点 children 字段名 */
|
||||||
|
children?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
data: () => [],
|
||||||
|
height: "400px",
|
||||||
|
highlightCurrent: true,
|
||||||
|
defaultExpandAll: false,
|
||||||
|
nodeKey: "id",
|
||||||
|
label: "label",
|
||||||
|
children: "children",
|
||||||
|
});
|
||||||
|
|
||||||
|
const treeProps = computed(() => ({
|
||||||
|
label: props.label,
|
||||||
|
children: props.children,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const computedHeight = computed(() => {
|
||||||
|
const parsed = parseInt(props.height, 10);
|
||||||
|
return isNaN(parsed) ? 400 : parsed;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -20,7 +20,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import AppConfig from "@/config";
|
|
||||||
import { defaultSettings } from "@/config/setting";
|
import { defaultSettings } from "@/config/setting";
|
||||||
import { ThemeMode } from "@/enums";
|
import { ThemeMode } from "@/enums";
|
||||||
import { hexToRgba } from "@utils";
|
import { hexToRgba } from "@utils";
|
||||||
@@ -52,7 +51,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
content: AppConfig.systemInfo.name,
|
content: "",
|
||||||
visible: false,
|
visible: false,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontColor: undefined,
|
fontColor: undefined,
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fa-table-v2" :style="{ height: height }">
|
||||||
|
<ElTableV2
|
||||||
|
:columns="columns"
|
||||||
|
:data="data"
|
||||||
|
:width="tableWidth"
|
||||||
|
:height="tableHeight"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</ElTableV2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
defineOptions({ name: "FaTableV2" });
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 列配置 */
|
||||||
|
columns?: any[];
|
||||||
|
/** 表格数据 */
|
||||||
|
data?: any[];
|
||||||
|
/** 表格宽度 */
|
||||||
|
width?: number | string;
|
||||||
|
/** 容器高度 */
|
||||||
|
height?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
columns: () => [],
|
||||||
|
data: () => [],
|
||||||
|
width: "100%",
|
||||||
|
height: "400px",
|
||||||
|
});
|
||||||
|
|
||||||
|
/** ElTableV2 需要数值类型的 width */
|
||||||
|
const tableWidth = computed(() => {
|
||||||
|
return Number(props.width) || 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** ElTableV2 需要数值类型的 height,从容器高度 prop 中提取 */
|
||||||
|
const tableHeight = computed(() => {
|
||||||
|
return parseInt(props.height, 10) || 400;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.fa-table-v2 {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -165,7 +165,7 @@ import { useI18n } from "vue-i18n";
|
|||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { useWorktabStore, useUserStore, useSettingsStore } from "@stores";
|
import { useWorktabStore, useUserStore, useSettingsStore } from "@stores";
|
||||||
import { MenuItemType } from "@/components/others/fa-menu-right/index.vue";
|
import { MenuItemType } from "@/components/navigation/fa-menu-right/index.vue";
|
||||||
import { useCommon } from "@/hooks/core/useCommon";
|
import { useCommon } from "@/hooks/core/useCommon";
|
||||||
import { formatMenuTitle, quickStartManager } from "@utils";
|
import { formatMenuTitle, quickStartManager } from "@utils";
|
||||||
import type { WorkTab } from "@/types/store";
|
import type { WorkTab } from "@/types/store";
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="app-layout">
|
<div class="app-layout">
|
||||||
|
<!-- 水印覆盖层(FaWatermark 组件,内含 ElWatermark,通过 store watermarkVisible 控制显隐) -->
|
||||||
|
<FaWatermark :content="userStore.basicInfo?.username || AppConfig.systemInfo.name" />
|
||||||
|
|
||||||
<!-- 左侧菜单导航 -->
|
<!-- 左侧菜单导航 -->
|
||||||
<aside id="app-sidebar" aria-label="主菜单导航">
|
<aside id="app-sidebar" aria-label="主菜单导航">
|
||||||
<FaSidebarMenu />
|
<FaSidebarMenu />
|
||||||
@@ -28,6 +31,9 @@
|
|||||||
<FaGlobalComponent />
|
<FaGlobalComponent />
|
||||||
<FaGuide v-if="guideVisible" v-model="guideVisible" @skip="onGuideFinished" />
|
<FaGuide v-if="guideVisible" v-model="guideVisible" @skip="onGuideFinished" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- AI 助手 -->
|
||||||
|
<FaAiAssistant v-if="enableAiAssistant" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -44,12 +50,21 @@
|
|||||||
* → settingStore.showGuide=false → 后续不再显示
|
* → settingStore.showGuide=false → 后续不再显示
|
||||||
*/
|
*/
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { useAppStore, useSettingsStore } from "@stores";
|
import { useAppStore, useSettingsStore, useUserStore } from "@stores";
|
||||||
|
import AppConfig from "@/config";
|
||||||
|
|
||||||
defineOptions({ name: "AppLayout" });
|
defineOptions({ name: "AppLayout" });
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const settingStore = useSettingsStore();
|
const settingStore = useSettingsStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// ── AI 助手 ──
|
||||||
|
const enableAiAssistant = computed(() => {
|
||||||
|
const isEnabled = settingStore.userEnableAi;
|
||||||
|
const isLoggedIn = userStore.basicInfo && Object.keys(userStore.basicInfo).length > 0;
|
||||||
|
return isEnabled && isLoggedIn;
|
||||||
|
});
|
||||||
|
|
||||||
/** 新手指引显隐 —— session 级状态,首次登录/注册后自动弹出 */
|
/** 新手指引显隐 —— session 级状态,首次登录/注册后自动弹出 */
|
||||||
const guideVisible = computed({
|
const guideVisible = computed({
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
* ])
|
* ])
|
||||||
*/
|
*/
|
||||||
import { h, type VNode } from "vue";
|
import { h, type VNode } from "vue";
|
||||||
import StatusTag from "@/components/others/fa-status-tag/index.vue";
|
import StatusTag from "@/components/display/fa-status-tag/index.vue";
|
||||||
import type { ColumnOption, StatusColumnConfig } from "@/types/component";
|
import type { ColumnOption, StatusColumnConfig } from "@/types/component";
|
||||||
/** ElTag 支持的 type */
|
/** ElTag 支持的 type */
|
||||||
export type StatusType = "primary" | "success" | "warning" | "danger" | "info";
|
export type StatusType = "primary" | "success" | "warning" | "danger" | "info";
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ElScrollbar class="h-[calc(100%-40px)]">
|
<ElScrollbar class="h-[calc(100%-40px)]" view-style="padding-right: 16px">
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between h-17.5 border-b border-g-300 text-sm last:border-b-0"
|
class="flex items-center justify-between h-17.5 border-b border-g-300 text-sm last:border-b-0"
|
||||||
v-for="item in list"
|
v-for="item in list"
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
<span class="deco-line" />
|
<span class="deco-line" />
|
||||||
</h1>
|
</h1>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<button class="fullscreen-btn" :title="isFs ? '退出全屏' : '进入全屏'" @click="handleToggle">
|
<ElTooltip :content="isFs ? '退出全屏' : '进入全屏'" placement="bottom">
|
||||||
|
<button class="fullscreen-btn" @click="handleToggle">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="fs-icon">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="fs-icon">
|
||||||
<path
|
<path
|
||||||
v-if="isFs"
|
v-if="isFs"
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
</ElTooltip>
|
||||||
<span class="header-time font-mono text-sm">{{ currentTime }}</span>
|
<span class="header-time font-mono text-sm">{{ currentTime }}</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -463,6 +463,7 @@ const uploadRef = ref<InstanceType<typeof ElUpload>>();
|
|||||||
|
|
||||||
const avatarCropVisible = ref(false);
|
const avatarCropVisible = ref(false);
|
||||||
const avatarCropSrc = ref("");
|
const avatarCropSrc = ref("");
|
||||||
|
const avatarCropName = ref("");
|
||||||
|
|
||||||
function revokeAvatarCropSrc() {
|
function revokeAvatarCropSrc() {
|
||||||
if (avatarCropSrc.value.startsWith("blob:")) {
|
if (avatarCropSrc.value.startsWith("blob:")) {
|
||||||
@@ -481,7 +482,7 @@ function onAvatarCropImgError() {
|
|||||||
|
|
||||||
async function onAvatarCropConfirm(dataURL: string) {
|
async function onAvatarCropConfirm(dataURL: string) {
|
||||||
try {
|
try {
|
||||||
const file = dataURLToFile(dataURL, "avatar.jpg");
|
const file = dataURLToFile(dataURL, avatarCropName.value);
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
const response = await UserAPI.uploadCurrentUserAvatar(formData);
|
const response = await UserAPI.uploadCurrentUserAvatar(formData);
|
||||||
@@ -664,8 +665,18 @@ const handleAvatarFileChange = (file: UploadFile) => {
|
|||||||
fileList.value = [];
|
fileList.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const raw = file.raw;
|
||||||
|
avatarCropName.value = raw.name;
|
||||||
|
|
||||||
|
// 非 canvas 兼容格式(如 svg),跳过裁剪直接上传原文件
|
||||||
|
if (raw.type !== "image/png" && raw.type !== "image/jpeg" && raw.type !== "image/webp") {
|
||||||
|
uploadRef.value?.submit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
revokeAvatarCropSrc();
|
revokeAvatarCropSrc();
|
||||||
avatarCropSrc.value = URL.createObjectURL(file.raw);
|
avatarCropSrc.value = URL.createObjectURL(raw);
|
||||||
avatarCropVisible.value = true;
|
avatarCropVisible.value = true;
|
||||||
uploadRef.value?.clearFiles();
|
uploadRef.value?.clearFiles();
|
||||||
fileList.value = [];
|
fileList.value = [];
|
||||||
|
|||||||
@@ -47,14 +47,12 @@
|
|||||||
:class="{ 'bg-active-color': selectedPerson?.id === item.id }"
|
:class="{ 'bg-active-color': selectedPerson?.id === item.id }"
|
||||||
@click="selectPerson(item)"
|
@click="selectPerson(item)"
|
||||||
>
|
>
|
||||||
<div class="relative mr-3">
|
<div class="mr-3">
|
||||||
<ElAvatar :size="40" :src="item.avatar">
|
<ElBadge is-dot :color="item.online ? 'var(--el-color-success)' : 'var(--el-color-error)'">
|
||||||
{{ item.name.charAt(0) }}
|
<ElAvatar :size="40" :src="item.avatar">
|
||||||
</ElAvatar>
|
{{ item.name.charAt(0) }}
|
||||||
<div
|
</ElAvatar>
|
||||||
class="absolute right-1 bottom-1 size-2 rounded-full"
|
</ElBadge>
|
||||||
:class="item.online ? 'bg-success' : 'bg-error'"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center justify-between mb-1">
|
<div class="flex items-center justify-between mb-1">
|
||||||
|
|||||||
@@ -39,20 +39,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 空状态 - 直接显示添加按钮作为唯一行动 -->
|
<!-- 空状态 - 直接显示添加按钮作为唯一行动 -->
|
||||||
<div v-if="items.length === 0" class="empty-state">
|
<ElEmpty v-if="items.length === 0">
|
||||||
<div class="empty-illust">
|
<template #image>
|
||||||
<ElIcon class="empty-icon" :size="56"><Cpu /></ElIcon>
|
<div class="empty-illust">
|
||||||
<ElIcon class="empty-icon-bg" :size="100"><ChatLineSquare /></ElIcon>
|
<ElIcon :size="56"><Cpu /></ElIcon>
|
||||||
</div>
|
<ElIcon class="empty-icon-bg" :size="100"><ChatLineSquare /></ElIcon>
|
||||||
<div class="empty-title">添加你的第一个 AI 模型</div>
|
</div>
|
||||||
<div class="empty-desc">
|
</template>
|
||||||
支持 OpenAI、DeepSeek、Ollama 等任何 OpenAI 兼容服务<br />
|
<template #description>
|
||||||
配置后即可在 AI 助手页一键切换
|
<div class="empty-title">添加你的第一个 AI 模型</div>
|
||||||
</div>
|
<div class="empty-desc">
|
||||||
|
支持 OpenAI、DeepSeek、Ollama 等任何 OpenAI 兼容服务<br />
|
||||||
|
配置后即可在 AI 助手页一键切换
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<ElButton type="primary" size="large" :icon="Plus" @click="openCreate">
|
<ElButton type="primary" size="large" :icon="Plus" @click="openCreate">
|
||||||
立即添加
|
立即添加
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</ElEmpty>
|
||||||
|
|
||||||
<!-- 列表 -->
|
<!-- 列表 -->
|
||||||
<div v-else class="config-list">
|
<div v-else class="config-list">
|
||||||
@@ -587,17 +591,6 @@ onMounted(loadList);
|
|||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 空状态 */
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 48px 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-illust {
|
.empty-illust {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -605,13 +598,6 @@ onMounted(loadList);
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-icon {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-icon-bg {
|
.empty-icon-bg {
|
||||||
|
|||||||
@@ -32,9 +32,11 @@
|
|||||||
>
|
>
|
||||||
<div class="model-switcher" :class="{ 'is-active': dropdownVisible }">
|
<div class="model-switcher" :class="{ 'is-active': dropdownVisible }">
|
||||||
<ElIcon class="model-icon"><Cpu /></ElIcon>
|
<ElIcon class="model-icon"><Cpu /></ElIcon>
|
||||||
<span class="model-name" :title="activeModelName">
|
<ElTooltip :content="activeModelName" placement="top">
|
||||||
{{ activeModelName }}
|
<span class="model-name">
|
||||||
</span>
|
{{ activeModelName }}
|
||||||
|
</span>
|
||||||
|
</ElTooltip>
|
||||||
<ElIcon class="model-arrow" :class="{ expanded: dropdownVisible }">
|
<ElIcon class="model-arrow" :class="{ expanded: dropdownVisible }">
|
||||||
<ArrowDown />
|
<ArrowDown />
|
||||||
</ElIcon>
|
</ElIcon>
|
||||||
|
|||||||
@@ -58,10 +58,12 @@
|
|||||||
@blur="handleSaveTitle(row)"
|
@blur="handleSaveTitle(row)"
|
||||||
@keyup.enter="handleSaveTitle(row)"
|
@keyup.enter="handleSaveTitle(row)"
|
||||||
/>
|
/>
|
||||||
<span v-else class="editable-cell" title="点击编辑" @click="handleEditTitle(row)">
|
<ElTooltip v-else content="点击编辑" placement="top">
|
||||||
{{ row.title || "未命名会话" }}
|
<span class="editable-cell" @click="handleEditTitle(row)">
|
||||||
<ElIcon class="edit-icon"><Edit /></ElIcon>
|
{{ row.title || "未命名会话" }}
|
||||||
</span>
|
<ElIcon class="edit-icon"><Edit /></ElIcon>
|
||||||
|
</span>
|
||||||
|
</ElTooltip>
|
||||||
</template>
|
</template>
|
||||||
</FaTable>
|
</FaTable>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
@@ -153,7 +155,7 @@ import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
|||||||
import type FaForm from "@/components/forms/fa-form/index.vue";
|
import type FaForm from "@/components/forms/fa-form/index.vue";
|
||||||
import { formatToDateTime, renderTableOperationCell, type TableOperationAction } from "@utils";
|
import { formatToDateTime, renderTableOperationCell, type TableOperationAction } from "@utils";
|
||||||
import type { ColumnOption } from "@/types/component";
|
import type { ColumnOption } from "@/types/component";
|
||||||
import FaDescriptions from "@/components/others/fa-descriptions/index.vue";
|
import FaDescriptions from "@/components/display/fa-descriptions/index.vue";
|
||||||
import FaTable from "@/components/tables/fa-table/index.vue";
|
import FaTable from "@/components/tables/fa-table/index.vue";
|
||||||
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
||||||
|
|
||||||
@@ -355,7 +357,7 @@ const { dialogVisible, closeDialog } = useCrudDialog();
|
|||||||
|
|
||||||
const detailFormData = ref<Partial<ChatSessionDetail>>({});
|
const detailFormData = ref<Partial<ChatSessionDetail>>({});
|
||||||
|
|
||||||
const memoryDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
|
const memoryDetailItems: import("@/components/display/fa-descriptions/index.vue").DescriptionsItem[] =
|
||||||
[
|
[
|
||||||
{ label: "会话ID", prop: "session_id" },
|
{ label: "会话ID", prop: "session_id" },
|
||||||
{ label: "标题", prop: "title" },
|
{ label: "标题", prop: "title" },
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ import DemoAPI, {
|
|||||||
type DemoTable,
|
type DemoTable,
|
||||||
} from "@/api/module_example/demo";
|
} from "@/api/module_example/demo";
|
||||||
import type { ColumnOption } from "@/types/component";
|
import type { ColumnOption } from "@/types/component";
|
||||||
import FaDescriptions from "@/components/others/fa-descriptions/index.vue";
|
import FaDescriptions from "@/components/display/fa-descriptions/index.vue";
|
||||||
import FaForm from "@/components/forms/fa-form/index.vue";
|
import FaForm from "@/components/forms/fa-form/index.vue";
|
||||||
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
||||||
|
|
||||||
@@ -368,7 +368,7 @@ const { dialogVisible } = useCrudDialog();
|
|||||||
|
|
||||||
const detailFormData = ref<DemoTable>({});
|
const detailFormData = ref<DemoTable>({});
|
||||||
|
|
||||||
const demoDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
|
const demoDetailItems: import("@/components/display/fa-descriptions/index.vue").DescriptionsItem[] =
|
||||||
[
|
[
|
||||||
{ label: "名称", prop: "name" },
|
{ label: "名称", prop: "name" },
|
||||||
{ label: "UUID", prop: "uuid" },
|
{ label: "UUID", prop: "uuid" },
|
||||||
|
|||||||
+6
-6
@@ -112,7 +112,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex-1 min-h-0 overflow-auto">
|
<ElScrollbar class="flex-1 min-h-0">
|
||||||
<ElTable :loading="loading" :data="cacheNames" row-key="cache_name">
|
<ElTable :loading="loading" :data="cacheNames" row-key="cache_name">
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<ElEmpty :image-size="80" description="暂无数据" />
|
<ElEmpty :image-size="80" description="暂无数据" />
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
</ElTable>
|
</ElTable>
|
||||||
</div>
|
</ElScrollbar>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
|
|
||||||
<!-- 键名列表 -->
|
<!-- 键名列表 -->
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex-1 min-h-0 overflow-auto">
|
<ElScrollbar class="flex-1 min-h-0">
|
||||||
<ElTable
|
<ElTable
|
||||||
:loading="subLoading"
|
:loading="subLoading"
|
||||||
:data="cacheKeys.map((key) => ({ cacheKey: key }))"
|
:data="cacheKeys.map((key) => ({ cacheKey: key }))"
|
||||||
@@ -216,7 +216,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
</ElTable>
|
</ElTable>
|
||||||
</div>
|
</ElScrollbar>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
|
|
||||||
<!-- 缓存内容 -->
|
<!-- 缓存内容 -->
|
||||||
@@ -243,7 +243,7 @@
|
|||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex-1 min-h-0 overflow-auto">
|
<ElScrollbar class="flex-1 min-h-0">
|
||||||
<ElForm :model="cacheForm" label-suffix=":" label-position="top">
|
<ElForm :model="cacheForm" label-suffix=":" label-position="top">
|
||||||
<ElFormItem label="缓存名称">
|
<ElFormItem label="缓存名称">
|
||||||
<ElInput v-model="cacheForm.cache_name" readonly placeholder="缓存名称" />
|
<ElInput v-model="cacheForm.cache_name" readonly placeholder="缓存名称" />
|
||||||
@@ -261,7 +261,7 @@
|
|||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
</div>
|
</ElScrollbar>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
</div>
|
</div>
|
||||||
</ElTabPane>
|
</ElTabPane>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ import type { TableOperationAction } from "@/utils/table";
|
|||||||
import { renderTableOperationCell } from "@utils";
|
import { renderTableOperationCell } from "@utils";
|
||||||
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||||
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
|
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
|
||||||
import FaCopyButton from "@/components/others/fa-copy-button/index.vue";
|
import FaCopyButton from "@/components/actions/fa-copy-button/index.vue";
|
||||||
import type { ColumnOption } from "@/types/component";
|
import type { ColumnOption } from "@/types/component";
|
||||||
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
||||||
|
|
||||||
|
|||||||
+5
-3
@@ -6,9 +6,11 @@
|
|||||||
<div class="logo-title-wrap">
|
<div class="logo-title-wrap">
|
||||||
<div class="logo-title-inline">
|
<div class="logo-title-inline">
|
||||||
<h1 class="title">{{ siteTitle }}</h1>
|
<h1 class="title">{{ siteTitle }}</h1>
|
||||||
<div class="logo-version-badge" :title="displayVersion">
|
<ElTooltip :content="displayVersion" placement="bottom">
|
||||||
<span class="logo-version-pill">{{ displayVersion }}</span>
|
<div class="logo-version-badge">
|
||||||
</div>
|
<span class="logo-version-pill">{{ displayVersion }}</span>
|
||||||
|
</div>
|
||||||
|
</ElTooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,9 +8,11 @@
|
|||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<h1 class="auth-top-bar__site-title">{{ siteTitle }}</h1>
|
<h1 class="auth-top-bar__site-title">{{ siteTitle }}</h1>
|
||||||
<div class="logo-version-badge shrink-0" :title="displayVersion">
|
<ElTooltip :content="displayVersion" placement="bottom">
|
||||||
<span class="logo-version-pill">{{ displayVersion }}</span>
|
<div class="logo-version-badge shrink-0">
|
||||||
</div>
|
<span class="logo-version-pill">{{ displayVersion }}</span>
|
||||||
|
</div>
|
||||||
|
</ElTooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ const { dialogVisible } = useCrudDialog();
|
|||||||
|
|
||||||
const detailFormData = ref<DeptTable>({ code: "" });
|
const detailFormData = ref<DeptTable>({ code: "" });
|
||||||
|
|
||||||
const deptDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
|
const deptDetailItems: import("@/components/display/fa-descriptions/index.vue").DescriptionsItem[] =
|
||||||
[
|
[
|
||||||
{ label: "部门名称", prop: "name" },
|
{ label: "部门名称", prop: "name" },
|
||||||
{ label: "部门编码", prop: "code" },
|
{ label: "部门编码", prop: "code" },
|
||||||
|
|||||||
@@ -407,7 +407,7 @@ const { dialogVisible } = useCrudDialog();
|
|||||||
|
|
||||||
const detailFormData = ref<DictDataTable>({});
|
const detailFormData = ref<DictDataTable>({});
|
||||||
|
|
||||||
const dictDataDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
|
const dictDataDetailItems: import("@/components/display/fa-descriptions/index.vue").DescriptionsItem[] =
|
||||||
[
|
[
|
||||||
{ label: "数据标签", prop: "dict_label" },
|
{ label: "数据标签", prop: "dict_label" },
|
||||||
{ label: "数据类型", prop: "dict_type" },
|
{ label: "数据类型", prop: "dict_type" },
|
||||||
|
|||||||
@@ -120,9 +120,9 @@ import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
|||||||
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
||||||
import FaForm from "@/components/forms/fa-form/index.vue";
|
import FaForm from "@/components/forms/fa-form/index.vue";
|
||||||
import DictDataPanel from "./components/DictDataPanel.vue";
|
import DictDataPanel from "./components/DictDataPanel.vue";
|
||||||
import FaStatusTag from "@/components/others/fa-status-tag/index.vue";
|
import FaStatusTag from "@/components/display/fa-status-tag/index.vue";
|
||||||
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
||||||
import FaDescriptions from "@/components/others/fa-descriptions/index.vue";
|
import FaDescriptions from "@/components/display/fa-descriptions/index.vue";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "Dict",
|
name: "Dict",
|
||||||
@@ -174,7 +174,7 @@ const { dialogVisible } = useCrudDialog();
|
|||||||
|
|
||||||
const detailFormData = ref<DictTable>({});
|
const detailFormData = ref<DictTable>({});
|
||||||
|
|
||||||
const dictDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
|
const dictDetailItems: import("@/components/display/fa-descriptions/index.vue").DescriptionsItem[] =
|
||||||
[
|
[
|
||||||
{ label: "字典名称", prop: "dict_name" },
|
{ label: "字典名称", prop: "dict_name" },
|
||||||
{ label: "字典类型", prop: "dict_type", slot: "dict_type" },
|
{ label: "字典类型", prop: "dict_type", slot: "dict_type" },
|
||||||
|
|||||||
@@ -199,11 +199,11 @@ import {
|
|||||||
import type { IObject } from "@/components/modal/types";
|
import type { IObject } from "@/components/modal/types";
|
||||||
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
|
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
|
||||||
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||||
import FaStatusTag from "@/components/others/fa-status-tag/index.vue";
|
import FaStatusTag from "@/components/display/fa-status-tag/index.vue";
|
||||||
import FaCopyButton from "@/components/others/fa-copy-button/index.vue";
|
import FaCopyButton from "@/components/actions/fa-copy-button/index.vue";
|
||||||
import type { ColumnOption } from "@/types/component";
|
import type { ColumnOption } from "@/types/component";
|
||||||
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
||||||
import FaDescriptions from "@/components/others/fa-descriptions/index.vue";
|
import FaDescriptions from "@/components/display/fa-descriptions/index.vue";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "Log",
|
name: "Log",
|
||||||
@@ -369,7 +369,7 @@ const opExportContentConfig = computed(() => ({
|
|||||||
|
|
||||||
const opFormData = ref<OperationLogTable>({} as OperationLogTable);
|
const opFormData = ref<OperationLogTable>({} as OperationLogTable);
|
||||||
|
|
||||||
const opDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] = [
|
const opDetailItems: import("@/components/display/fa-descriptions/index.vue").DescriptionsItem[] = [
|
||||||
{ label: "描述", prop: "description", span: 8 },
|
{ label: "描述", prop: "description", span: 8 },
|
||||||
{ label: "请求路径", prop: "request_path" },
|
{ label: "请求路径", prop: "request_path" },
|
||||||
{ label: "请求方法", prop: "request_method", slot: "request_method" },
|
{ label: "请求方法", prop: "request_method", slot: "request_method" },
|
||||||
|
|||||||
@@ -19,10 +19,7 @@
|
|||||||
@reset="onResetSearch"
|
@reset="onResetSearch"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ElTabs v-model="menuClientTab" @tab-change="handleMenuClientTabChange">
|
<ElSegmented v-model="menuClientTab" :options="menuSegmentedOptions" @change="handleMenuClientTabChange" />
|
||||||
<ElTabPane label="Web 菜单" name="pc" />
|
|
||||||
<ElTabPane label="APP 菜单" name="app" />
|
|
||||||
</ElTabs>
|
|
||||||
|
|
||||||
<ElCard
|
<ElCard
|
||||||
class="fa-table-card"
|
class="fa-table-card"
|
||||||
@@ -363,9 +360,9 @@ import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"
|
|||||||
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||||
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
||||||
import FaForm from "@/components/forms/fa-form/index.vue";
|
import FaForm from "@/components/forms/fa-form/index.vue";
|
||||||
import FaMenuRouteIcon from "@/components/others/fa-menu-route-icon/index.vue";
|
import FaMenuRouteIcon from "@/components/navigation/fa-menu-route-icon/index.vue";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import FaDescriptions from "@/components/others/fa-descriptions/index.vue";
|
import FaDescriptions from "@/components/display/fa-descriptions/index.vue";
|
||||||
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
@@ -439,6 +436,10 @@ function formatMenuOperationCell(row: MenuTable, ctx: Parameters<typeof buildMen
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const menuClientTab = ref<"pc" | "app">("pc");
|
const menuClientTab = ref<"pc" | "app">("pc");
|
||||||
|
const menuSegmentedOptions = [
|
||||||
|
{ label: "Web 菜单", value: "pc" },
|
||||||
|
{ label: "APP 菜单", value: "app" },
|
||||||
|
];
|
||||||
const searchForm = ref<MenuSearchForm>({
|
const searchForm = ref<MenuSearchForm>({
|
||||||
name: undefined,
|
name: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
@@ -497,7 +498,7 @@ const createParentLocked = ref(false);
|
|||||||
|
|
||||||
const detailFormData = ref<MenuTable>({});
|
const detailFormData = ref<MenuTable>({});
|
||||||
|
|
||||||
const menuDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
|
const menuDetailItems: import("@/components/display/fa-descriptions/index.vue").DescriptionsItem[] =
|
||||||
[
|
[
|
||||||
{ label: "编号", prop: "id" },
|
{ label: "编号", prop: "id" },
|
||||||
{ label: "菜单名称", prop: "name" },
|
{ label: "菜单名称", prop: "name" },
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
|||||||
import FaForm from "@/components/forms/fa-form/index.vue";
|
import FaForm from "@/components/forms/fa-form/index.vue";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
||||||
import FaDescriptions from "@/components/others/fa-descriptions/index.vue";
|
import FaDescriptions from "@/components/display/fa-descriptions/index.vue";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "Notice",
|
name: "Notice",
|
||||||
@@ -243,7 +243,7 @@ const { dialogVisible } = useCrudDialog();
|
|||||||
|
|
||||||
const detailFormData = ref<NoticeTable>({});
|
const detailFormData = ref<NoticeTable>({});
|
||||||
|
|
||||||
const noticeDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
|
const noticeDetailItems: import("@/components/display/fa-descriptions/index.vue").DescriptionsItem[] =
|
||||||
[
|
[
|
||||||
{ label: "标题", prop: "notice_title" },
|
{ label: "标题", prop: "notice_title" },
|
||||||
{ label: "类型", prop: "notice_type", slot: "notice_type" },
|
{ label: "类型", prop: "notice_type", slot: "notice_type" },
|
||||||
|
|||||||
@@ -377,7 +377,7 @@ const positionExportContentConfig = computed(() => ({
|
|||||||
|
|
||||||
const detailFormData = ref<PositionTable>({});
|
const detailFormData = ref<PositionTable>({});
|
||||||
|
|
||||||
const positionDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
|
const positionDetailItems: import("@/components/display/fa-descriptions/index.vue").DescriptionsItem[] =
|
||||||
[
|
[
|
||||||
{ label: "岗位名称", prop: "name" },
|
{ label: "岗位名称", prop: "name" },
|
||||||
{ label: "排序", prop: "order" },
|
{ label: "排序", prop: "order" },
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ import MenuAPI, { MenuTable } from "@/api/module_system/menu";
|
|||||||
import { DeviceEnum } from "@/enums/settings/device.enum";
|
import { DeviceEnum } from "@/enums/settings/device.enum";
|
||||||
import { useUserStore, useAppStore } from "@stores";
|
import { useUserStore, useAppStore } from "@stores";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import type FaMenuTreeTable from "@/components/others/fa-menu-tree-table/index.vue";
|
import type FaMenuTreeTable from "@/components/navigation/fa-menu-tree-table/index.vue";
|
||||||
import FaDrawer from "@/components/modal/fa-drawer/index.vue";
|
import FaDrawer from "@/components/modal/fa-drawer/index.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -172,11 +172,11 @@ import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"
|
|||||||
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
import type FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||||
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
||||||
import FaForm from "@/components/forms/fa-form/index.vue";
|
import FaForm from "@/components/forms/fa-form/index.vue";
|
||||||
import StatusTag from "@/components/others/fa-status-tag/index.vue";
|
import StatusTag from "@/components/display/fa-status-tag/index.vue";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import FaPermissonDrawer from "./components/FaPermissonDrawer.vue";
|
import FaPermissonDrawer from "./components/FaPermissonDrawer.vue";
|
||||||
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
||||||
import FaDescriptions from "@/components/others/fa-descriptions/index.vue";
|
import FaDescriptions from "@/components/display/fa-descriptions/index.vue";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "Role",
|
name: "Role",
|
||||||
@@ -382,7 +382,7 @@ const { dialogVisible } = useCrudDialog();
|
|||||||
|
|
||||||
const detailFormData = ref<RoleTable>({} as RoleTable);
|
const detailFormData = ref<RoleTable>({} as RoleTable);
|
||||||
|
|
||||||
const roleDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
|
const roleDetailItems: import("@/components/display/fa-descriptions/index.vue").DescriptionsItem[] =
|
||||||
[
|
[
|
||||||
{ label: "角色名称", prop: "name" },
|
{ label: "角色名称", prop: "name" },
|
||||||
{ label: "排序", prop: "order" },
|
{ label: "排序", prop: "order" },
|
||||||
|
|||||||
@@ -151,7 +151,7 @@
|
|||||||
@confirm="handleDialogConfirm"
|
@confirm="handleDialogConfirm"
|
||||||
>
|
>
|
||||||
<template v-if="dialogVisible.type === 'detail'">
|
<template v-if="dialogVisible.type === 'detail'">
|
||||||
<div class="max-h-[65vh] overflow-y-auto">
|
<ElScrollbar max-height="65vh">
|
||||||
<FaDescriptions
|
<FaDescriptions
|
||||||
:column="4"
|
:column="4"
|
||||||
:data="detailFormData"
|
:data="detailFormData"
|
||||||
@@ -259,7 +259,7 @@
|
|||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ElScrollbar>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<FaForm
|
<FaForm
|
||||||
@@ -316,7 +316,7 @@
|
|||||||
</FaForm>
|
</FaForm>
|
||||||
|
|
||||||
<!-- 处理工单:展示工单信息 + 回复处理 -->
|
<!-- 处理工单:展示工单信息 + 回复处理 -->
|
||||||
<div v-else class="flex flex-col max-h-[75vh] overflow-y-auto">
|
<ElScrollbar v-else max-height="75vh" class="flex flex-col">
|
||||||
<!-- 工单信息区 -->
|
<!-- 工单信息区 -->
|
||||||
<div class="flex flex-col gap-3 py-1">
|
<div class="flex flex-col gap-3 py-1">
|
||||||
<div class="flex gap-2.5 items-center">
|
<div class="flex gap-2.5 items-center">
|
||||||
@@ -357,7 +357,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ElScrollbar>
|
||||||
</template>
|
</template>
|
||||||
</FaDialog>
|
</FaDialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -397,7 +397,7 @@ import { Plus, Delete, MoreFilled, CircleClose } from "@element-plus/icons-vue";
|
|||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import FaForm from "@/components/forms/fa-form/index.vue";
|
import FaForm from "@/components/forms/fa-form/index.vue";
|
||||||
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
||||||
import FaDescriptions from "@/components/others/fa-descriptions/index.vue";
|
import FaDescriptions from "@/components/display/fa-descriptions/index.vue";
|
||||||
import FaCardGrid from "@/components/cards/fa-card-grid/index.vue";
|
import FaCardGrid from "@/components/cards/fa-card-grid/index.vue";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -623,7 +623,7 @@ const detailFormData = ref<TicketTable & { reply_content?: string }>(
|
|||||||
{} as TicketTable & { reply_content?: string }
|
{} as TicketTable & { reply_content?: string }
|
||||||
);
|
);
|
||||||
|
|
||||||
const ticketDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
|
const ticketDetailItems: import("@/components/display/fa-descriptions/index.vue").DescriptionsItem[] =
|
||||||
[
|
[
|
||||||
{ label: "工单标题", prop: "title", span: 4 },
|
{ label: "工单标题", prop: "title", span: 4 },
|
||||||
{ label: "工单类型", prop: "ticket_type", slot: "ticket_type" },
|
{ label: "工单类型", prop: "ticket_type", slot: "ticket_type" },
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ import PositionAPI from "@/api/module_system/position";
|
|||||||
import DeptAPI from "@/api/module_system/dept";
|
import DeptAPI from "@/api/module_system/dept";
|
||||||
import RoleAPI from "@/api/module_system/role";
|
import RoleAPI from "@/api/module_system/role";
|
||||||
import { useUserStore } from "@stores";
|
import { useUserStore } from "@stores";
|
||||||
import type { DescriptionsItem } from "@/components/others/fa-descriptions/index.vue";
|
import type { DescriptionsItem } from "@/components/display/fa-descriptions/index.vue";
|
||||||
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
|
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
|
||||||
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
|
||||||
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
import type { FormItem } from "@/components/forms/fa-form/index.vue";
|
||||||
@@ -229,7 +229,7 @@ import FaForm from "@/components/forms/fa-form/index.vue";
|
|||||||
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
||||||
import FaTable from "@/components/tables/fa-table/index.vue";
|
import FaTable from "@/components/tables/fa-table/index.vue";
|
||||||
import FaDrawer from "@/components/modal/fa-drawer/index.vue";
|
import FaDrawer from "@/components/modal/fa-drawer/index.vue";
|
||||||
import FaDescriptions from "@/components/others/fa-descriptions/index.vue";
|
import FaDescriptions from "@/components/display/fa-descriptions/index.vue";
|
||||||
import type { IContentConfig, IObject } from "@/components/modal/types";
|
import type { IContentConfig, IObject } from "@/components/modal/types";
|
||||||
import FaDeptTree from "./components/FaDeptTree.vue";
|
import FaDeptTree from "./components/FaDeptTree.vue";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ import type { ColumnOption } from "@/types/component";
|
|||||||
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
|
||||||
import FaTable from "@/components/tables/fa-table/index.vue";
|
import FaTable from "@/components/tables/fa-table/index.vue";
|
||||||
import FaDialog from "@/components/modal/fa-dialog/index.vue";
|
import FaDialog from "@/components/modal/fa-dialog/index.vue";
|
||||||
import FaDescriptions from "@/components/others/fa-descriptions/index.vue";
|
import FaDescriptions from "@/components/display/fa-descriptions/index.vue";
|
||||||
import FaForm from "@/components/forms/fa-form/index.vue";
|
import FaForm from "@/components/forms/fa-form/index.vue";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -324,7 +324,7 @@ const { dialogVisible } = useCrudDialog();
|
|||||||
|
|
||||||
const detailFormData = ref<VersionTable>({});
|
const detailFormData = ref<VersionTable>({});
|
||||||
|
|
||||||
const versionDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
|
const versionDetailItems: import("@/components/display/fa-descriptions/index.vue").DescriptionsItem[] =
|
||||||
[
|
[
|
||||||
{ label: "标题", prop: "title" },
|
{ label: "标题", prop: "title" },
|
||||||
{ label: "版本号", prop: "version" },
|
{ label: "版本号", prop: "version" },
|
||||||
|
|||||||
@@ -167,7 +167,9 @@
|
|||||||
class="job-card-dot"
|
class="job-card-dot"
|
||||||
:class="`job-card-dot--${getJobStatusClass(job.status)}`"
|
:class="`job-card-dot--${getJobStatusClass(job.status)}`"
|
||||||
/>
|
/>
|
||||||
<span class="job-card-name" :title="job.name">{{ job.name }}</span>
|
<ElTooltip :content="job.name" placement="top">
|
||||||
|
<span class="job-card-name">{{ job.name }}</span>
|
||||||
|
</ElTooltip>
|
||||||
<ElTag :type="getJobStatusType(job.status)" size="small" effect="dark">
|
<ElTag :type="getJobStatusType(job.status)" size="small" effect="dark">
|
||||||
{{ getJobStatusLabel(job.status) }}
|
{{ getJobStatusLabel(job.status) }}
|
||||||
</ElTag>
|
</ElTag>
|
||||||
@@ -863,7 +865,7 @@ const logDetailDialog = reactive({
|
|||||||
});
|
});
|
||||||
const logDetailFormData = ref<JobLogTable>({} as JobLogTable);
|
const logDetailFormData = ref<JobLogTable>({} as JobLogTable);
|
||||||
|
|
||||||
const logDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] = [
|
const logDetailItems: import("@/components/display/fa-descriptions/index.vue").DescriptionsItem[] = [
|
||||||
{ label: "任务ID", prop: "job_id" },
|
{ label: "任务ID", prop: "job_id" },
|
||||||
{ label: "任务名称", prop: "job_name" },
|
{ label: "任务名称", prop: "job_name" },
|
||||||
{ label: "触发方式", prop: "trigger_type" },
|
{ label: "触发方式", prop: "trigger_type" },
|
||||||
|
|||||||
Reference in New Issue
Block a user