add notify ai

This commit is contained in:
Chuck1sn
2025-06-13 11:43:35 +08:00
parent 81b02e68e7
commit a00c3e129f
21 changed files with 397 additions and 233 deletions

View File

@@ -87,6 +87,7 @@ import LoadingIcon from "@/components/icons/LoadingIcon.vue";
import { useAiAction } from "@/composables/ai/useAiAction";
import { useDepartmentQuery } from "@/composables/department/useDepartmentQuery";
import { useDepartmentUpsert } from "@/composables/department/useDepartmentUpsert";
import { useActionExcStore } from "@/composables/store/useActionExcStore";
import useAlertStore from "@/composables/store/useAlertStore";
import type { DepartmentUpsertModel } from "@/types/department";
import DOMPurify from "dompurify";
@@ -118,7 +119,7 @@ const { deleteUserByUsername, deleteDepartmentByName } = useAiAction();
const departmentDeleteModal = ref<ModalInterface>();
const currentDeleteUsername = ref<string>();
const currentDeleteDepartmentName = ref<string>();
const actionExcStore = useActionExcStore();
const { availableDepartments, fetchAvailableDepartments } =
useDepartmentQuery();
@@ -194,6 +195,7 @@ const handleUpsertUserSubmit = async (data: UserUpsertSubmitModel) => {
content: "操作成功",
level: "success",
});
actionExcStore.notify(true);
};
const handleUpsertDepartmentSubmit = async (
@@ -205,6 +207,7 @@ const handleUpsertDepartmentSubmit = async (
content: "操作成功",
level: "success",
});
actionExcStore.notify(true);
};
const handleDeleteUserSubmit = async () => {
@@ -214,6 +217,7 @@ const handleDeleteUserSubmit = async () => {
content: "操作成功",
level: "success",
});
actionExcStore.notify(true);
};
const handleDeleteDepartmentSubmit = async () => {
@@ -223,6 +227,7 @@ const handleDeleteDepartmentSubmit = async () => {
content: "操作成功",
level: "success",
});
actionExcStore.notify(true);
};
watch(
@@ -261,6 +266,7 @@ const chatByMode = async (
await searchAction(message);
} else if (mode === "execute") {
await executeAction(message);
actionExcStore.notify(true);
} else {
await chat(message);
}

View File

@@ -35,21 +35,21 @@ import { computed } from "vue";
import type { RouteLocationRaw } from "vue-router";
interface BreadcrumbItem {
name: string;
route?: RouteLocationRaw;
name: string;
route?: RouteLocationRaw;
}
const props = defineProps<{
names: string[];
routes?: RouteLocationRaw[];
names: string[];
routes?: RouteLocationRaw[];
}>();
const breadcrumbs = computed<BreadcrumbItem[]>(() => {
return props.names.map((name, index) => {
return {
name,
route: props.routes?.[index]
};
});
return props.names.map((name, index) => {
return {
name,
route: props.routes?.[index],
};
});
});
</script>

View File

@@ -0,0 +1,35 @@
<template>
<div id="date-range-picker" date-rangepicker class="flex items-center">
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
fill="currentColor" viewBox="0 0 20 20">
<path
d="M20 4a2 2 0 0 0-2-2h-2V1a1 1 0 0 0-2 0v1h-3V1a1 1 0 0 0-2 0v1H6V1a1 1 0 0 0-2 0v1H2a2 2 0 0 0-2 2v2h20V4ZM0 18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8H0v10Zm5-8h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2Z" />
</svg>
</div>
<input id="datepicker-range-start" name="start" type="text"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Select date start">
</div>
<span class="mx-4 text-gray-500">to</span>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
fill="currentColor" viewBox="0 0 20 20">
<path
d="M20 4a2 2 0 0 0-2-2h-2V1a1 1 0 0 0-2 0v1h-3V1a1 1 0 0 0-2 0v1H6V1a1 1 0 0 0-2 0v1H2a2 2 0 0 0-2 2v2h20V4ZM0 18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8H0v10Zm5-8h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2Z" />
</svg>
</div>
<input id="datepicker-range-end" name="end" type="text"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Select date end">
</div>
</div>
</template>
<script setup>
</script>
<style scoped></style>

View File

@@ -4,7 +4,7 @@
<div class="flex items-center justify-between">
<div class="flex items-center justify-start rtl:justify-end">
<button type="button" @click="handleSidebarToggle"
class="inline-flex items-center p-2 text-sm text-gray-500 rounded-lg sm:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200">
class="inline-flex items-center p-2 text-sm text-gray-500 rounded-lg sm:hidden hover:bg-gray-100">
<span class="sr-only">Open sidebar</span>
<svg class="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
@@ -13,7 +13,7 @@
</path>
</svg>
</button>
<a href="https://github.com/ccmjga/zhilu-admin" target="_blank" class="flex items-center ms-2 md:me-24">
<a href="https://github.com/ccmjga/zhilu-admin" target="_blank" class="flex items-center ms-2 md:me-24 ">
<img class="me-3" src="/logo.svg" alt="logo">
<span class="self-center text-lg sm:text-xl md:text-2xl font-semibold whitespace-nowrap">知路后台管理</span>
</a>
@@ -36,8 +36,7 @@
</button>
<div class="flex items-center ms-2 sm:ms-3">
<div>
<button type="button" id="dropdown-button"
class="flex text-sm bg-gray-800 rounded-full focus:ring-4 focus:ring-gray-300 cursor-pointer"
<button type="button" id="dropdown-button" class="flex text-sm bg-gray-800 rounded-full cursor-pointer"
aria-expanded="false" data-dropdown-toggle="dropdown-user">
<span class="sr-only">打开用户菜单</span>
<img class="w-8 h-8 rounded-full" src="/java.svg" alt="user photo">

View File

@@ -32,18 +32,18 @@
</template>
<script setup generic="T" lang="ts">
import { ref } from 'vue';
import { ref } from "vue";
/** 通用对象类型 */
type ItemRecord = Record<string, unknown>;
const props = defineProps<{
/** 数据项数组 */
items: T[] | undefined;
/** 数据项ID字段名 */
idField?: string;
/** 数据项唯一键字段名 */
keyField?: string;
/** 数据项数组 */
items: T[] | undefined;
/** 数据项ID字段名 */
idField?: string;
/** 数据项唯一键字段名 */
keyField?: string;
}>();
/**
@@ -53,17 +53,17 @@ const props = defineProps<{
* @returns 唯一键
*/
const getItemKey = (item: T, index: number): string | number => {
if (props.keyField) {
const key = (item as ItemRecord)[props.keyField];
if (key !== undefined) return String(key);
}
if (props.idField) {
const id = (item as ItemRecord)[props.idField];
if (id !== undefined) return String(id);
}
const id = (item as ItemRecord).id;
return id !== undefined ? String(id) : index;
if (props.keyField) {
const key = (item as ItemRecord)[props.keyField];
if (key !== undefined) return String(key);
}
if (props.idField) {
const id = (item as ItemRecord)[props.idField];
if (id !== undefined) return String(id);
}
const id = (item as ItemRecord).id;
return id !== undefined ? String(id) : index;
};
</script>

View File

@@ -37,24 +37,24 @@
</template>
<script setup generic="T" lang="ts">
import { ref, watch } from 'vue';
import { ref, watch } from "vue";
/** 通用对象类型 */
type ItemRecord = Record<string, unknown>;
const props = defineProps<{
/** 数据项数组 */
items: T[] | undefined;
/** 数据项ID字段名 */
idField?: string;
/** 数据项唯一键字段名 */
keyField?: string;
/** 选中项的值数组 */
modelValue?: (string | number)[];
/** 数据项数组 */
items: T[] | undefined;
/** 数据项ID字段名 */
idField?: string;
/** 数据项唯一键字段名 */
keyField?: string;
/** 选中项的值数组 */
modelValue?: (string | number)[];
}>();
const emit = defineEmits<{
'update:modelValue': [checkedItems: (string | number)[]];
"update:modelValue": [checkedItems: (string | number)[]];
}>();
const checkedItems = ref<(string | number)[]>(props.modelValue || []);
@@ -66,13 +66,13 @@ const checkedItems = ref<(string | number)[]>(props.modelValue || []);
* @returns 唯一键
*/
const getItemKey = (item: T, index: number): string | number => {
if (props.keyField) {
const key = (item as ItemRecord)[props.keyField];
if (key !== undefined) return String(key);
}
const id = getItemId(item);
return id !== undefined ? id : index;
if (props.keyField) {
const key = (item as ItemRecord)[props.keyField];
if (key !== undefined) return String(key);
}
const id = getItemId(item);
return id !== undefined ? id : index;
};
/**
@@ -81,21 +81,28 @@ const getItemKey = (item: T, index: number): string | number => {
* @returns ID值
*/
const getItemId = (item: T): string | number => {
if (props.idField) {
return (item as ItemRecord)[props.idField] as string | number;
}
return (item as ItemRecord).id as string | number || item as unknown as string | number;
if (props.idField) {
return (item as ItemRecord)[props.idField] as string | number;
}
return (
((item as ItemRecord).id as string | number) ||
(item as unknown as string | number)
);
};
// 监听选中项变化
watch(checkedItems, (newVal) => {
emit('update:modelValue', newVal);
emit("update:modelValue", newVal);
});
// 监听modelValue变化
watch(() => props.modelValue, (newVal) => {
if (newVal) {
checkedItems.value = newVal;
}
}, { deep: true });
watch(
() => props.modelValue,
(newVal) => {
if (newVal) {
checkedItems.value = newVal;
}
},
{ deep: true },
);
</script>

View File

@@ -14,70 +14,78 @@
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { computed } from "vue";
export type ButtonVariant = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info';
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg';
export type ButtonVariant =
| "primary"
| "secondary"
| "success"
| "danger"
| "warning"
| "info";
export type ButtonSize = "xs" | "sm" | "md" | "lg";
const props = defineProps<{
/** 按钮变体类型 */
variant?: ButtonVariant;
/** 按钮尺寸 */
size?: ButtonSize;
/** 是否禁用 */
disabled?: boolean;
/** 自定义CSS类名 */
className?: string;
/** 是否为移动端尺寸 */
isMobile?: boolean;
/** 按钮变体类型 */
variant?: ButtonVariant;
/** 按钮尺寸 */
size?: ButtonSize;
/** 是否禁用 */
disabled?: boolean;
/** 自定义CSS类名 */
className?: string;
/** 是否为移动端尺寸 */
isMobile?: boolean;
}>();
const emit = defineEmits<{
'click': [event: MouseEvent];
click: [event: MouseEvent];
}>();
/** 按钮颜色样式映射 */
const colorClasses = computed(() => {
const variants: Record<ButtonVariant, string> = {
primary: 'text-white bg-blue-700 hover:bg-blue-800 focus:ring-blue-300',
secondary: 'text-gray-900 bg-white border border-gray-300 hover:bg-gray-100 focus:ring-gray-100',
success: 'text-white bg-green-700 hover:bg-green-800 focus:ring-green-300',
danger: 'text-white bg-red-700 hover:bg-red-800 focus:ring-red-300',
warning: 'text-gray-900 bg-yellow-400 hover:bg-yellow-500 focus:ring-yellow-300',
info: 'text-white bg-cyan-700 hover:bg-cyan-800 focus:ring-cyan-300'
};
return variants[props.variant || 'primary'];
const variants: Record<ButtonVariant, string> = {
primary: "text-white bg-blue-700 hover:bg-blue-800 focus:ring-blue-300",
secondary:
"text-gray-900 bg-white border border-gray-300 hover:bg-gray-100 focus:ring-gray-100",
success: "text-white bg-green-700 hover:bg-green-800 focus:ring-green-300",
danger: "text-white bg-red-700 hover:bg-red-800 focus:ring-red-300",
warning:
"text-gray-900 bg-yellow-400 hover:bg-yellow-500 focus:ring-yellow-300",
info: "text-white bg-cyan-700 hover:bg-cyan-800 focus:ring-cyan-300",
};
return variants[props.variant || "primary"];
});
/** 按钮尺寸样式映射 */
const sizeClasses = computed(() => {
// 移动端尺寸
if (props.isMobile) {
const sizes: Record<ButtonSize, string> = {
xs: 'text-xs px-2 py-1',
sm: 'text-xs px-3 py-1.5',
md: 'text-sm px-3 py-2',
lg: 'text-sm px-4 py-2.5'
};
return sizes[props.size || 'sm'];
}
// PC端尺寸
const sizes: Record<ButtonSize, string> = {
xs: 'text-xs px-3 py-1.5',
sm: 'text-sm px-3 py-2',
md: 'text-sm px-4 py-2.5',
lg: 'text-base px-5 py-3'
};
return sizes[props.size || 'md'];
// 移动端尺寸
if (props.isMobile) {
const sizes: Record<ButtonSize, string> = {
xs: "text-xs px-2 py-1",
sm: "text-xs px-3 py-1.5",
md: "text-sm px-3 py-2",
lg: "text-sm px-4 py-2.5",
};
return sizes[props.size || "sm"];
}
// PC端尺寸
const sizes: Record<ButtonSize, string> = {
xs: "text-xs px-3 py-1.5",
sm: "text-sm px-3 py-2",
md: "text-sm px-4 py-2.5",
lg: "text-base px-5 py-3",
};
return sizes[props.size || "md"];
});
/** 处理点击事件 */
const handleClick = (event: MouseEvent) => {
if (!props.disabled) {
emit('click', event);
}
if (!props.disabled) {
emit("click", event);
}
};
</script>

View File

@@ -51,44 +51,44 @@
</template>
<script setup generic="T" lang="ts">
import { defineEmits, ref, watch } from 'vue';
import { defineEmits, ref, watch } from "vue";
/**
* 表格列配置接口
*/
export interface Column {
/** 列标题 */
title: string;
/** 数据字段名 */
field: string;
/** 是否可排序 */
sortable?: boolean;
/** 自定义CSS类名 */
class?: string;
/** 列标题 */
title: string;
/** 数据字段名 */
field: string;
/** 是否可排序 */
sortable?: boolean;
/** 自定义CSS类名 */
class?: string;
}
/** 通用对象类型 */
type ItemRecord = Record<string, unknown>;
const props = defineProps<{
/** 数据项数组 */
items: T[];
/** 列配置数组 */
columns: Column[];
/** 是否显示复选框 */
hasCheckbox?: boolean;
/** 数据项ID字段名 */
idField?: string;
/** 数据项唯一键字段名 */
keyField?: string;
/** 选中项的值数组用于v-model绑定 */
modelValue?: (string | number)[];
/** 数据项数组 */
items: T[];
/** 列配置数组 */
columns: Column[];
/** 是否显示复选框 */
hasCheckbox?: boolean;
/** 数据项ID字段名 */
idField?: string;
/** 数据项唯一键字段名 */
keyField?: string;
/** 选中项的值数组用于v-model绑定 */
modelValue?: (string | number)[];
}>();
const emit = defineEmits<{
'update:modelValue': [checkedItems: (string | number)[]];
'sort': [field: string];
'all-checked-change': [checked: boolean];
"update:modelValue": [checkedItems: (string | number)[]];
sort: [field: string];
"all-checked-change": [checked: boolean];
}>();
const checkedItems = ref<(string | number)[]>(props.modelValue || []);
@@ -101,13 +101,13 @@ const allChecked = ref(false);
* @returns 唯一键
*/
const getItemKey = (item: T, index: number): string | number => {
if (props.keyField) {
const key = (item as ItemRecord)[props.keyField];
if (key !== undefined) return String(key);
}
const id = getItemId(item);
return id !== undefined ? id : index;
if (props.keyField) {
const key = (item as ItemRecord)[props.keyField];
if (key !== undefined) return String(key);
}
const id = getItemId(item);
return id !== undefined ? id : index;
};
/**
@@ -116,10 +116,13 @@ const getItemKey = (item: T, index: number): string | number => {
* @returns ID值
*/
const getItemId = (item: T): string | number => {
if (props.idField) {
return (item as ItemRecord)[props.idField] as string | number;
}
return (item as ItemRecord).id as string | number || item as unknown as string | number;
if (props.idField) {
return (item as ItemRecord)[props.idField] as string | number;
}
return (
((item as ItemRecord).id as string | number) ||
(item as unknown as string | number)
);
};
/**
@@ -129,26 +132,34 @@ const getItemId = (item: T): string | number => {
* @returns 字段值
*/
const getItemValue = (item: T, field: string): string => {
if (!field) return '';
return String(field.split('.').reduce<unknown>((obj, key) =>
obj && typeof obj === 'object' && key in (obj as Record<string, unknown>)
? (obj as Record<string, unknown>)[key]
: '',
item as ItemRecord));
if (!field) return "";
return String(
field
.split(".")
.reduce<unknown>(
(obj, key) =>
obj &&
typeof obj === "object" &&
key in (obj as Record<string, unknown>)
? (obj as Record<string, unknown>)[key]
: "",
item as ItemRecord,
),
);
};
/**
* 处理全选/取消全选
*/
const handleAllCheckedChange = () => {
if (allChecked.value) {
checkedItems.value = props.items.map(getItemId);
} else {
checkedItems.value = [];
}
emit('all-checked-change', allChecked.value);
emit('update:modelValue', checkedItems.value);
if (allChecked.value) {
checkedItems.value = props.items.map(getItemId);
} else {
checkedItems.value = [];
}
emit("all-checked-change", allChecked.value);
emit("update:modelValue", checkedItems.value);
};
/**
@@ -156,32 +167,40 @@ const handleAllCheckedChange = () => {
* @param field 排序字段
*/
const handleSortClick = (field: string) => {
emit('sort', field);
emit("sort", field);
};
// 监听选中项变化
watch(checkedItems, (newVal) => {
emit('update:modelValue', newVal);
// 更新全选状态
if (props.items.length > 0) {
allChecked.value = newVal.length === props.items.length;
} else {
allChecked.value = false;
}
emit("update:modelValue", newVal);
// 更新全选状态
if (props.items.length > 0) {
allChecked.value = newVal.length === props.items.length;
} else {
allChecked.value = false;
}
});
// 监听modelValue变化
watch(() => props.modelValue, (newVal) => {
if (newVal) {
checkedItems.value = newVal;
}
}, { deep: true });
watch(
() => props.modelValue,
(newVal) => {
if (newVal) {
checkedItems.value = newVal;
}
},
{ deep: true },
);
// 监听items变化重置选中状态
watch(() => props.items, () => {
if (allChecked.value) {
checkedItems.value = props.items.map(getItemId);
emit('update:modelValue', checkedItems.value);
}
}, { deep: true });
watch(
() => props.items,
() => {
if (allChecked.value) {
checkedItems.value = props.items.map(getItemId);
emit("update:modelValue", checkedItems.value);
}
},
{ deep: true },
);
</script>