Merge pull request #495 from fastapiadmin/dev

Dev
This commit is contained in:
fastapiadmin
2026-07-30 23:07:28 +08:00
committed by GitHub
72 changed files with 1346 additions and 174 deletions
+3 -1
View File
@@ -79,4 +79,6 @@ frontend/app/*
.plan .plan
CLAUDE.md CLAUDE.md
AGENTS.md AGENTS.md
.codebuddy .codebuddy
.agent
.workbuddy
+99
View File
@@ -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 抽屉
loadingElement 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 水印
+3 -38
View File
@@ -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;
}
});
/** /**
* 应用根组件生命周期: * 应用根组件生命周期:
* *
@@ -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>
@@ -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>
@@ -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>
@@ -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";
+16 -1
View File
@@ -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>
支持 OpenAIDeepSeekOllama 等任何 OpenAI 兼容服务<br /> <template #description>
配置后即可在 AI 助手页一键切换 <div class="empty-title">添加你的第一个 AI 模型</div>
</div> <div class="empty-desc">
支持 OpenAIDeepSeekOllama 等任何 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
View File
@@ -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";
@@ -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" },