refactor: frontend TS migration + Tailwind4/daisyUI5 unify + BUILDPLATFORM docker build
- migrate frontend to TypeScript (vue-tsc strict in build), upgrade all deps to latest (Vite 8, Tailwind 4, daisyUI 5, Pinia 4, vue-router 5) - restructure frontend dirs (api/components/common/layouts/styles/types/views) - drop Element Plus, add daisyUI TagInput; main CSS 490KB->163KB, entry JS 618KB->1.3KB - rewrite Dockerfile(.cn): frontend/backend stages pinned to $BUILDPLATFORM, CGO_ENABLED=0 cross-compile, no QEMU in multi-arch builds; add .dockerignore - local dev: Vite /api proxy + make dev targets; go:embed all:dist with .gitkeep so backend runs without prior frontend build - fix latent bugs: Keys.vue users ref, Settings.vue undefined userStore, Login.vue Ref-as-error display, res.error misuse - add REFACTOR_PLAN.md (phased refactor log)
This commit is contained in:
@@ -5,14 +5,15 @@
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, provide } from 'vue';
|
||||
import Toast from './components/Toast.vue';
|
||||
import Toast from '@/components/common/Toast.vue';
|
||||
import { ToastKey } from './composables/toast';
|
||||
import type { ToastMessage, ToastType } from './composables/toast';
|
||||
|
||||
const toastQueue = ref<ToastMessage[]>([]);
|
||||
|
||||
const toastQueue = ref([]);
|
||||
|
||||
const setToast = (message, type = 'info', duration) => {
|
||||
const setToast = (message: string, type: ToastType = 'info', duration?: number) => {
|
||||
toastQueue.value.push({ message, type, duration });
|
||||
};
|
||||
|
||||
@@ -20,5 +21,5 @@ onMounted(() => {
|
||||
// authStore.checkLoginStatus();
|
||||
});
|
||||
|
||||
provide('toast', { setToast });
|
||||
provide(ToastKey, { setToast });
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// src/utils/request.js
|
||||
import axios from 'axios';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
// src/api/client.ts
|
||||
import axios from 'axios'
|
||||
import type { AxiosError, InternalAxiosRequestConfig } from 'axios'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const baseURL = import.meta.env.VITE_API_BASE_URL|| '/api'
|
||||
const baseURL = import.meta.env.VITE_API_BASE_URL || '/api'
|
||||
if (import.meta.env.DEV) { // Vite 的方式判断开发环境
|
||||
console.log(`[Request] API Base URL: ${baseURL}`);
|
||||
} else if (process.env.NODE_ENV === 'development') { // Vue CLI 的方式判断开发环境
|
||||
@@ -10,8 +11,8 @@ if (import.meta.env.DEV) { // Vite 的方式判断开发环境
|
||||
}
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: baseURL,
|
||||
timeout: 6000,
|
||||
baseURL: baseURL,
|
||||
timeout: 6000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
@@ -19,29 +20,29 @@ const service = axios.create({
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
const authStore = useAuthStore();
|
||||
if (!authStore.token) {
|
||||
authStore.loadTokenFromStorage();
|
||||
authStore.loadTokenFromStorage();
|
||||
}
|
||||
if (authStore.token) {
|
||||
config.headers.Authorization = `Bearer ${authStore.token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
error => {
|
||||
console.error('Request error:', error);
|
||||
return Promise.reject(error);
|
||||
(error: AxiosError) => {
|
||||
console.error('Request error:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
(response) => {
|
||||
return response; // 只返回响应数据,便于后续使用
|
||||
},
|
||||
error => {
|
||||
(error: AxiosError) => {
|
||||
// 可以在这里处理响应错误的情况,例如统一处理错误信息, 提示用户等
|
||||
console.error('Response error:', error);
|
||||
// 这里可以做一些统一的错误处理,例如根据状态码判断是否 token 失效,并跳转到登录页面
|
||||
@@ -54,4 +55,4 @@ service.interceptors.response.use(
|
||||
}
|
||||
);
|
||||
|
||||
export default service;
|
||||
export default service;
|
||||
+30
-27
@@ -7,7 +7,7 @@
|
||||
<div ref="centerElement" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-20">
|
||||
<!-- 中心图标本身 -->
|
||||
<div class="w-10 h-10 md:w-16 md:h-16 rounded-full flex items-center justify-center backdrop-blur-md animate-bounce hover:cursor-alias" @click="$router.push('/dashboard')">
|
||||
<img src="../assets/logo.svg" alt="Center Logo" class="rounded-full object-cover">
|
||||
<img src="@/assets/logo.svg" alt="Center Logo" class="rounded-full object-cover">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<div
|
||||
class="absolute top-0 left-0 h-full flex flex-col justify-around items-center py-4 md:py-8 px-2 md:px-4 z-10">
|
||||
<!-- 遍历左侧图标数据 -->
|
||||
<div v-for="icon in leftIcons" :key="icon.id" :ref="el => { if (el) iconRefs[icon.id] = el }"
|
||||
<div v-for="icon in leftIcons" :key="icon.id" :ref="el => { if (el) iconRefs[icon.id] = el as Element }"
|
||||
class="w-8 h-8 md:w-10 md:h-10 lg:w-12 lg:h-12 flex items-center justify-center">
|
||||
<img v-if="icon.img" :src="icon.img" :alt="icon.name" class="w-full h-full object-contain">
|
||||
<div v-else
|
||||
@@ -28,7 +28,7 @@
|
||||
<div
|
||||
class="absolute top-0 right-0 h-full flex flex-col justify-around items-center py-4 md:py-8 px-2 md:px-4 z-10">
|
||||
<!-- 遍历右侧图标数据 -->
|
||||
<div v-for="icon in rightIcons" :key="icon.id" :ref="el => { if (el) iconRefs[icon.id] = el }"
|
||||
<div v-for="icon in rightIcons" :key="icon.id" :ref="el => { if (el) iconRefs[icon.id] = el as Element }"
|
||||
class="w-8 h-8 md:w-10 md:h-10 lg:w-12 lg:h-12 flex items-center justify-center">
|
||||
<img v-if="icon.img" :src="icon.img" :alt="icon.name" class="w-full h-full object-contain">
|
||||
<div v-else
|
||||
@@ -90,16 +90,19 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick, reactive, computed } from 'vue'; // 引入 computed
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, nextTick, reactive } from 'vue';
|
||||
|
||||
type Coords = { x: number; y: number };
|
||||
type FlowIcon = { id: string; name: string; img: string; color: string };
|
||||
|
||||
// --- 图标数据 (保持不变) ---
|
||||
const leftIcons = ref([
|
||||
const leftIcons = ref<FlowIcon[]>([
|
||||
{ id: 'web', name: 'Web', img: 'https://img.icons8.com/?size=100&id=38536&format=png&color=000000', color: '#DB4437' },
|
||||
{ id: 'iphone', name: 'iPhone', img: 'https://img.icons8.com/?size=100&id=ZwGNoFXGbt9n&format=png&color=000000', color: '#eac50c' },
|
||||
{ id: 'mac', name: 'Mac', img: 'https://img.icons8.com/?size=100&id=RHxDgbKmJhUD&format=png&color=000000', color: '#1DB954' },
|
||||
]);
|
||||
const rightIcons = ref([
|
||||
const rightIcons = ref<FlowIcon[]>([
|
||||
{ id: 'openai', name: 'OpenAI', img: 'https://img.icons8.com/?size=100&id=FBO05Dys9QCg&format=png&color=000000', color: '#E4405F' },
|
||||
{ id: 'claude', name: 'Claude', img: 'https://img.icons8.com/?size=100&id=H5H0mqCCr5AV&format=png&color=000000', color: '#229ED9' },
|
||||
{ id: 'gemini', name: 'Gemini', img: 'https://img.icons8.com/?size=100&id=eoxMN35Z6JKg&format=png&color=000000', color: '#FF6600' },
|
||||
@@ -117,14 +120,14 @@ const largeGap = ref(1000); // 一个足够大的间隔,确保只有一个线
|
||||
// --- 结束 Dash 动画参数 ---
|
||||
|
||||
|
||||
const svgCanvas = ref(null);
|
||||
const centerElement = ref(null);
|
||||
const iconRefs = reactive({});
|
||||
const centerCoords = ref(null);
|
||||
const iconCoords = reactive({});
|
||||
const svgCanvas = ref<SVGSVGElement | null>(null);
|
||||
const centerElement = ref<HTMLElement | null>(null);
|
||||
const iconRefs = reactive<Record<string, Element | null>>({});
|
||||
const centerCoords = ref<Coords | null>(null);
|
||||
const iconCoords = reactive<Record<string, Coords | null>>({});
|
||||
|
||||
// (getElementCenterCoords 和 updateCoordinates 函数保持不变)
|
||||
const getElementCenterCoords = (element) => {
|
||||
const getElementCenterCoords = (element: Element | null): Coords | null => {
|
||||
if (!element || !svgCanvas.value) return null;
|
||||
const svgRect = svgCanvas.value.getBoundingClientRect();
|
||||
const elemRect = element.getBoundingClientRect();
|
||||
@@ -157,12 +160,12 @@ const updateCoordinates = () => {
|
||||
// (calculatePathForVisual 函数保持不变,我们不再需要 calculatePathForAnimation)
|
||||
/**
|
||||
* 计算静态视觉连接线的 SVG 路径 (总是从图标到中心)
|
||||
* @param {object} iconCoord 图标坐标 {x, y}
|
||||
* @param {object} centerCoord 中心坐标 {x, y}
|
||||
* @param {'left' | 'right'} side 图标在哪一侧
|
||||
* @returns {string} SVG path 'd' 属性字符串
|
||||
* @param iconCoord 图标坐标 {x, y}
|
||||
* @param centerCoord 中心坐标 {x, y}
|
||||
* @param side 图标在哪一侧
|
||||
* @returns SVG path 'd' 属性字符串
|
||||
*/
|
||||
const calculatePathForVisual = (iconCoord, centerCoord, side) => {
|
||||
const calculatePathForVisual = (iconCoord: Coords | null | undefined, centerCoord: Coords | null, side: 'left' | 'right'): string => {
|
||||
if (!iconCoord || !centerCoord) return '';
|
||||
const { x: startX, y: startY } = iconCoord;
|
||||
const { x: endX, y: endY } = centerCoord;
|
||||
@@ -175,17 +178,17 @@ const calculatePathForVisual = (iconCoord, centerCoord, side) => {
|
||||
|
||||
/**
|
||||
* 计算动画运动的 SVG 路径
|
||||
* @param {object} iconCoord 图标坐标 {x, y}
|
||||
* @param {object} centerCoord 中心坐标 {x, y}
|
||||
* @param {'left' | 'right'} side 图标在哪一侧
|
||||
* @param {'toCenter' | 'fromCenter'} direction 动画方向
|
||||
* @returns {string} SVG path 'd' 属性字符串
|
||||
* @param iconCoord 图标坐标 {x, y}
|
||||
* @param centerCoord 中心坐标 {x, y}
|
||||
* @param side 图标在哪一侧
|
||||
* @param direction 动画方向
|
||||
* @returns SVG path 'd' 属性字符串
|
||||
*/
|
||||
const calculatePathForAnimation = (iconCoord, centerCoord, side, direction) => {
|
||||
const calculatePathForAnimation = (iconCoord: Coords | null | undefined, centerCoord: Coords | null, side: 'left' | 'right', direction: 'toCenter' | 'fromCenter' = 'toCenter'): string => {
|
||||
if (!iconCoord || !centerCoord) return '';
|
||||
|
||||
let startX, startY, endX, endY;
|
||||
let controlX, controlY;
|
||||
let startX: number, startY: number, endX: number, endY: number;
|
||||
let controlX: number, controlY: number;
|
||||
|
||||
if (direction === 'fromCenter') {
|
||||
// --- 动画从中心开始 ---
|
||||
@@ -219,7 +222,7 @@ const calculatePathForAnimation = (iconCoord, centerCoord, side, direction) => {
|
||||
|
||||
|
||||
// --- 生命周期钩子 (保持不变) ---
|
||||
let resizeObserver;
|
||||
let resizeObserver: ResizeObserver | undefined;
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
+19
-27
@@ -71,33 +71,25 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref ,watch} from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
currentPage: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
totalItems: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
pageSizeOptions: {
|
||||
type: Array,
|
||||
default: () => [10, 25, 50, 100],
|
||||
},
|
||||
showSelectPageSize: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
currentPage: number;
|
||||
totalItems: number;
|
||||
pageSize?: number;
|
||||
pageSizeOptions?: number[];
|
||||
showSelectPageSize?: boolean;
|
||||
}>(), {
|
||||
pageSize: 10,
|
||||
pageSizeOptions: () => [10, 25, 50, 100],
|
||||
showSelectPageSize: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['changePage', 'changePageSize']);
|
||||
const emit = defineEmits<{
|
||||
(e: 'changePage', page: number, pageSize: number): void;
|
||||
(e: 'changePageSize', pageSize: number): void;
|
||||
}>();
|
||||
|
||||
const totalPages = computed(() => {
|
||||
return Math.max(1, Math.ceil(props.totalItems / props.pageSize));
|
||||
@@ -116,7 +108,7 @@ watch(() => props.pageSize, (newPageSize) => {
|
||||
localPageSize.value = newPageSize;
|
||||
});
|
||||
|
||||
const emitChangePage = (page, pageSize) => { // 添加了 pageSize 参数
|
||||
const emitChangePage = (page: number, pageSize: number) => { // 添加了 pageSize 参数
|
||||
const validPage = Math.max(1, Math.min(page, totalPages.value));
|
||||
if (validPage !== localCurrentPage.value) {
|
||||
localCurrentPage.value = validPage;
|
||||
@@ -124,8 +116,8 @@ const emitChangePage = (page, pageSize) => { // 添加了 pageSize 参数
|
||||
}
|
||||
};
|
||||
|
||||
const emitChangePageSize = (event) => {
|
||||
const newPageSize = parseInt(event.target.value, 10);
|
||||
const emitChangePageSize = (event: Event) => {
|
||||
const newPageSize = parseInt((event.target as HTMLSelectElement).value, 10);
|
||||
localPageSize.value = newPageSize;
|
||||
emit('changePage', 1, newPageSize); // 确保同时传递 page 和 pageSize
|
||||
};
|
||||
+11
-15
@@ -40,20 +40,16 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import QrcodeVue from 'qrcode.vue';
|
||||
|
||||
// 定义组件接收的 props
|
||||
const props = defineProps({
|
||||
value: { // 二维码的原始值
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
size: { // 二维码的尺寸 (像素)
|
||||
type: Number,
|
||||
default: 160 // 默认大小
|
||||
}
|
||||
// 组件接收的 props
|
||||
const props = withDefaults(defineProps<{
|
||||
value: string; // 二维码的原始值
|
||||
size?: number; // 二维码的尺寸 (像素)
|
||||
}>(), {
|
||||
size: 160, // 默认大小
|
||||
});
|
||||
|
||||
// 使用 ref 创建一个响应式变量,用于存储当前显示的二维码值
|
||||
@@ -76,21 +72,21 @@ const copyValue = async () => {
|
||||
setTimeout(() => {
|
||||
showCopied.value = false;
|
||||
}, 1500); // 1.5 秒后恢复
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error('Failed to copy: ', err);
|
||||
// 可以在这里添加错误提示
|
||||
}
|
||||
};
|
||||
|
||||
const applist = reactive([
|
||||
const applist = reactive<{ name: string; url: string }[]>([
|
||||
// { name: 'openteam', url: '/assets/logo.svg' },
|
||||
{ name: 'botgem', url: 'https://botgem.com/favicon.ico' },
|
||||
{ name: 'opencat', url: 'https://opencat.app/favicon.ico' },
|
||||
])
|
||||
|
||||
const applyPrefix = (name) => {
|
||||
const applyPrefix = (name: string) => {
|
||||
let origin = window.location.origin;
|
||||
|
||||
|
||||
switch (name) {
|
||||
case 'botgem':
|
||||
currentValue.value = `ama://set-api-key?server=${origin}&key=${props.value}`;
|
||||
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div
|
||||
class="input input-sm input-bordered w-full h-auto min-h-9 flex flex-wrap items-center gap-1 py-1 px-2"
|
||||
:class="{ 'input-disabled': disabled }"
|
||||
@click="focusInput"
|
||||
>
|
||||
<span
|
||||
v-for="(tag, index) in tags"
|
||||
:key="`${tag}-${index}`"
|
||||
class="badge badge-sm badge-ghost gap-1 py-2"
|
||||
>
|
||||
{{ tag }}
|
||||
<button
|
||||
v-if="!disabled"
|
||||
type="button"
|
||||
class="hover:text-error"
|
||||
:aria-label="`Remove ${tag}`"
|
||||
@click.stop="removeTag(index)"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</span>
|
||||
<input
|
||||
ref="inputRef"
|
||||
v-model="draft"
|
||||
type="text"
|
||||
class="grow min-w-20 bg-transparent border-none outline-none focus:outline-none p-0 m-0 h-7"
|
||||
:placeholder="tags.length ? '' : placeholder"
|
||||
:disabled="disabled"
|
||||
@keydown.enter.prevent="commitDraft"
|
||||
@keydown.backspace="removeLastOnEmpty"
|
||||
@blur="commitDraft"
|
||||
/>
|
||||
<button
|
||||
v-if="clearable && tags.length && !disabled"
|
||||
type="button"
|
||||
class="text-base-content/40 hover:text-error"
|
||||
aria-label="Clear all"
|
||||
@click.stop="clearAll"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
// 替代 Element Plus 的 el-input-tag:Enter 添加标签、可逐个删除、可清空
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: string[] | undefined
|
||||
placeholder?: string
|
||||
clearable?: boolean
|
||||
disabled?: boolean
|
||||
/** 触发提交的按键,仅支持 Enter(与 el-input-tag 的 trigger 对齐) */
|
||||
trigger?: string
|
||||
}>(), {
|
||||
placeholder: 'Please input',
|
||||
clearable: false,
|
||||
disabled: false,
|
||||
trigger: 'Enter',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string[]): void
|
||||
(e: 'change', value: string[]): void
|
||||
}>()
|
||||
|
||||
const tags = computed(() => props.modelValue ?? [])
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
const draft = ref('')
|
||||
|
||||
const commitDraft = () => {
|
||||
const value = draft.value.trim()
|
||||
if (!value) return
|
||||
if (!tags.value.includes(value)) {
|
||||
const next = [...tags.value, value]
|
||||
emit('update:modelValue', next)
|
||||
emit('change', next)
|
||||
}
|
||||
draft.value = ''
|
||||
}
|
||||
|
||||
const removeTag = (index: number) => {
|
||||
const next = tags.value.filter((_, i) => i !== index)
|
||||
emit('update:modelValue', next)
|
||||
emit('change', next)
|
||||
}
|
||||
|
||||
const removeLastOnEmpty = () => {
|
||||
if (draft.value.length === 0 && tags.value.length) {
|
||||
removeTag(tags.value.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const clearAll = () => {
|
||||
emit('update:modelValue', [])
|
||||
emit('change', [])
|
||||
}
|
||||
|
||||
const focusInput = () => {
|
||||
inputRef.value?.focus()
|
||||
}
|
||||
</script>
|
||||
@@ -14,19 +14,17 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onUnmounted, watch, onMounted } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { ref, onUnmounted, watch } from 'vue';
|
||||
import type { ToastMessage } from '@/composables/toast';
|
||||
|
||||
const props = defineProps({
|
||||
queue: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const props = defineProps<{
|
||||
queue: ToastMessage[];
|
||||
}>();
|
||||
|
||||
const show = ref(false);
|
||||
const currentMessage = ref(null);
|
||||
let timer = null;
|
||||
const currentMessage = ref<ToastMessage | null>(null);
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const processQueue = () => {
|
||||
if (props.queue.length === 0) {
|
||||
@@ -34,14 +32,14 @@ const processQueue = () => {
|
||||
currentMessage.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
currentMessage.value = props.queue.shift();
|
||||
|
||||
currentMessage.value = props.queue.shift() ?? null;
|
||||
show.value = true;
|
||||
|
||||
|
||||
timer = setTimeout(() => {
|
||||
processQueue();
|
||||
}, currentMessage.value.duration || 3000);
|
||||
|
||||
}, currentMessage.value?.duration || 3000);
|
||||
|
||||
};
|
||||
|
||||
watch(() => props.queue, () => {
|
||||
@@ -49,7 +47,7 @@ watch(() => props.queue, () => {
|
||||
processQueue()
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
@@ -22,42 +22,39 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: null // 默认为null,将从路由中获取
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
title?: string | null; // 默认为null,将从路由中获取
|
||||
// Optional custom breadcrumb items
|
||||
customBreadcrumbs: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
customBreadcrumbs?: { name: string; path: string }[];
|
||||
}>(), {
|
||||
title: null,
|
||||
customBreadcrumbs: () => [],
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
// Generate breadcrumb items based on current route
|
||||
// 生成面包屑项
|
||||
const breadcrumbItems = computed(() => {
|
||||
const breadcrumbItems = computed<{ name: string; path: string }[]>(() => {
|
||||
if (props.customBreadcrumbs.length > 0) {
|
||||
return props.customBreadcrumbs;
|
||||
}
|
||||
|
||||
|
||||
// 获取当前路径并分割成段
|
||||
const pathSegments = route.path.split('/').filter(segment => segment);
|
||||
|
||||
|
||||
return pathSegments.map((segment, index) => {
|
||||
const name = segment.charAt(0).toUpperCase() + segment.slice(1);
|
||||
|
||||
|
||||
// 对于最后一段,不设置链接
|
||||
if (index === pathSegments.length - 1) {
|
||||
return { name, path: '' };
|
||||
}
|
||||
|
||||
|
||||
// 创建到此段的路径
|
||||
const path = '/' + pathSegments.slice(0, index + 1).join('/');
|
||||
return { name, path };
|
||||
@@ -69,13 +66,13 @@ const displayTitle = computed(() => {
|
||||
if (props.title) {
|
||||
return props.title;
|
||||
}
|
||||
|
||||
|
||||
const pathSegments = route.path.split('/').filter(segment => segment);
|
||||
if (pathSegments.length > 0) {
|
||||
const lastSegment = pathSegments[pathSegments.length - 1];
|
||||
return lastSegment.charAt(0).toUpperCase() + lastSegment.slice(1);
|
||||
}
|
||||
|
||||
|
||||
return 'Dashboard';
|
||||
});
|
||||
</script>
|
||||
@@ -30,7 +30,7 @@
|
||||
<div v-if="item.badge" class="badge badge-sm">{{ item.badge }}</div>
|
||||
</summary>
|
||||
<ul>
|
||||
<li v-for="subItem in item.children" :key="subItem.label">
|
||||
<li v-for="subItem in (item.children as MenuLink[])" :key="subItem.label">
|
||||
<router-link :to="subItem.to" :class="{ 'active': isActive(subItem.to) }">
|
||||
<component :is="subItem.icon" class="w-4" />
|
||||
{{ subItem.label }}
|
||||
@@ -44,22 +44,11 @@
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
LayoutDashboardIcon,
|
||||
ShieldPlus,
|
||||
UsersRoundIcon,
|
||||
KeyRoundIcon,
|
||||
MessageSquareIcon,
|
||||
SettingsIcon,
|
||||
UserIcon,
|
||||
CommandIcon,
|
||||
BracesIcon,
|
||||
} from 'lucide-vue-next'
|
||||
import { ref, reactive, onMounted ,computed} from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import {routes,generateMenuItemsFromRoutes}from '@/utils/router_menu.js'
|
||||
import { useAuthStore } from '@/stores/auth.js';
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { routes, generateMenuItemsFromRoutes, type MenuItem, type MenuLink } from '@/utils/router_menu'
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const userrole = computed(() => {
|
||||
@@ -68,31 +57,12 @@ const userrole = computed(() => {
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
// 判断当前路由是否激活菜单项
|
||||
const isActive = (path) => {
|
||||
const isActive = (path: string) => {
|
||||
return route.path === path;
|
||||
// return router.currentRoute.value.fullPath.startsWith(path);
|
||||
};
|
||||
|
||||
let menuItems = reactive([
|
||||
{ type: 'link', label: 'Overview', to: '/dashboard/overview', icon: LayoutDashboardIcon },
|
||||
{ type: 'title', label: 'Apps' },
|
||||
{ type: 'link', label: 'Tokens', to: '/dashboard/tokens', icon: BracesIcon },
|
||||
{
|
||||
type: 'submenu', label: 'Manager', icon: CommandIcon, open: true, badge: 'Admin',
|
||||
children: [
|
||||
{ label: 'Users', to: '/dashboard/manager/users', icon: UsersRoundIcon },
|
||||
{ label: 'ApiKeys', to: '/dashboard/manager/keys', icon: KeyRoundIcon },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'submenu', label: 'Settings', icon: SettingsIcon,open: false,
|
||||
children: [
|
||||
{ label: 'Profile', to: '/dashboard/settings/profile', icon: UserIcon },
|
||||
]
|
||||
},
|
||||
]);
|
||||
menuItems = computed(() => generateMenuItemsFromRoutes(routes, userrole.value));
|
||||
const menuItems = computed<MenuItem[]>(() => generateMenuItemsFromRoutes(routes, userrole.value));
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { inject } from 'vue'
|
||||
import type { InjectionKey } from 'vue'
|
||||
|
||||
export type ToastType = 'info' | 'success' | 'error'
|
||||
|
||||
export type ToastMessage = {
|
||||
message: string
|
||||
type?: ToastType
|
||||
duration?: number
|
||||
}
|
||||
|
||||
export type ToastContext = {
|
||||
setToast: (message: string, type?: ToastType, duration?: number) => void
|
||||
}
|
||||
|
||||
export const ToastKey: InjectionKey<ToastContext> = Symbol('toast')
|
||||
|
||||
export function useToast(): ToastContext {
|
||||
const ctx = inject(ToastKey)
|
||||
if (!ctx) {
|
||||
throw new Error('ToastContext 未提供:请在 App 根组件 provide(ToastKey, ...)')
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
@@ -42,7 +42,7 @@
|
||||
<div v-if="userInfo.avatar" class="avatar">
|
||||
<div class="mask mask-squircle w-8 h-8">
|
||||
<img :src="userInfo.avatar" :alt="userInfo.name">
|
||||
<!-- <img src='../assets/logo.svg' :alt="userInfo.name"> -->
|
||||
<!-- <img src='@/assets/logo.svg' :alt="userInfo.name"> -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- 没有头像时显示首字母 -->
|
||||
@@ -87,25 +87,34 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import { MenuIcon, SunIcon, MoonIcon, BellIcon, User, Settings, LogOut } from 'lucide-vue-next'
|
||||
import type { Component } from 'vue'
|
||||
import { MenuIcon, SunIcon, MoonIcon, BellIcon, User, LogOut } from '@lucide/vue'
|
||||
import Sidebar from '@/components/dashboard/Sidebar.vue'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { UserInfo } from '@/types'
|
||||
|
||||
type NavItem = {
|
||||
name?: string;
|
||||
icon?: Component;
|
||||
type?: 'divider';
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const isDark = ref(false)
|
||||
const isLargeSidebarOpen = ref(true)
|
||||
|
||||
const userInfo = computed(() => {
|
||||
const userInfo = computed<Partial<UserInfo>>(() => {
|
||||
return authStore.user || {}
|
||||
})
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
if (!userInfo) {
|
||||
if (!userInfo.value) {
|
||||
await authStore.getProfile()
|
||||
}
|
||||
|
||||
@@ -135,13 +144,13 @@ const userInitials = computed(() => {
|
||||
return 'U';
|
||||
});
|
||||
|
||||
const userNavigation = reactive([
|
||||
const userNavigation = reactive<NavItem[]>([
|
||||
{ name: 'Profile', icon: User },
|
||||
{ type: 'divider' },
|
||||
{ name: 'Logout', icon: LogOut, class: 'text-error' }
|
||||
]);
|
||||
|
||||
const handleNavigation = (item) => {
|
||||
const handleNavigation = (item: NavItem) => {
|
||||
if (item.name === 'Profile') {
|
||||
router.push('/dashboard/settings/profile')
|
||||
} else if (item.name === 'Logout'){
|
||||
@@ -1,18 +1,15 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import './style.css'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import './styles/main.css'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import request from '@/utils/request'
|
||||
import request from '@/api/client'
|
||||
|
||||
const pinia = createPinia()
|
||||
const app = createApp(App)
|
||||
|
||||
app.provide('request', request)
|
||||
app.use(ElementPlus)
|
||||
app.use(router)
|
||||
app.use(pinia)
|
||||
app.mount('#app')
|
||||
@@ -1,53 +0,0 @@
|
||||
import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router'
|
||||
import { routes } from '@/utils/router_menu.js'
|
||||
|
||||
let defaultroutes = [
|
||||
{ path: '/', name: 'Home', component: () => import('@/views/Home.vue') },
|
||||
{ path: '/404', name: '404', component: () => import('@/views/404.vue') },
|
||||
|
||||
{ path: '/login', name: 'Login', component: () => import('@/views/Login.vue') },
|
||||
{ path: '/signup', name: 'Signup', component: () => import('@/views/Signup.vue') },
|
||||
|
||||
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('@/views/404.vue') }, // Catch all 404
|
||||
{
|
||||
path: '/dashboard', name: 'Dashboard', component: () => import('@/views/DashBoard.vue'), meta: { requiresAuth: true }, redirect: '/dashboard/overview', children: [
|
||||
{ path: 'overview', name: 'Overview', component: () => import('@/views/dashboard/Overview.vue'), meta: { title: 'Overview' } },
|
||||
{ path: 'tokens', name: 'Tokens', component: () => import('@/views/dashboard/Tokens.vue'), meta: { title: 'Tokens' } },
|
||||
{
|
||||
path: 'manager', name: 'Manager', meta: { title: 'Manager' }, redirect: '/dashboard/manager/users', children: [
|
||||
{ path: 'users', name: 'User', component: () => import('@/views/dashboard/User.vue'), meta: { title: 'Users' } },
|
||||
{ path: 'users/new', name: 'UserNew', component: () => import('@/views/dashboard/UserNew.vue'), meta: { title: 'UserNew' } },
|
||||
{ path: 'users/view', name: 'UserView', component: () => import('@/views/dashboard/UserView.vue'), meta: { title: 'UserView' } },
|
||||
{ path: 'keys', name: 'ApiKey', component: () => import('@/views/dashboard/Keys.vue'), meta: { title: 'Keys' } },
|
||||
{ path: 'keys/view', name: 'ApiKeyView', component: () => import('@/views/dashboard/KeyView.vue'), meta: { title: 'KeyView' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'settings', name: 'Settings', meta: { title: 'Settings' }, redirect: '/dashboard/settings/profile', children: [
|
||||
{ path: 'profile', name: 'Profile', component: () => import('@/views/dashboard/Profile.vue'), meta: { title: 'Profile' } },
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
// const router = createRouter({
|
||||
// history: createWebHistory(process.env.BASE_URL),
|
||||
// routes
|
||||
// })
|
||||
router.beforeEach((to, from, next) => {
|
||||
const isAuthenticated = localStorage.getItem('token')
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
next('/login')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { routes } from '@/utils/router_menu'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const isAuthenticated = localStorage.getItem('token')
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
next('/login')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -1,20 +1,21 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
import request from '@/utils/request'
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import request from '@/api/client'
|
||||
import type { UserInfo, AuthCredentials, TokenPayload, TokenInfo } from '@/types';
|
||||
import { useRouter } from 'vue-router';
|
||||
// import { jwtDecode } from 'jwt-decode';
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const user = ref(null);
|
||||
const role = computed(() => {
|
||||
const user = ref<UserInfo | null>(null);
|
||||
const role = computed<number>(() => {
|
||||
if (!user.value) return 0;
|
||||
return user.value.role;
|
||||
})
|
||||
|
||||
const token = ref(localStorage.getItem('token') || '');
|
||||
|
||||
const token = ref<string>(localStorage.getItem('token') || '');
|
||||
|
||||
const isAdmin = computed(() => {
|
||||
if (!user.value || user.value.role === 0) return false;
|
||||
@@ -23,34 +24,34 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
const isLoggedIn = computed(() => !!token.value);
|
||||
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
const setToken = (newToken) => {
|
||||
const setToken = (newToken: string) => {
|
||||
token.value = newToken;
|
||||
localStorage.setItem('token', newToken);
|
||||
}
|
||||
|
||||
const loadTokenFromStorage=()=> {
|
||||
const loadTokenFromStorage = () => {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
if (storedToken) {
|
||||
token.value = storedToken;
|
||||
}
|
||||
}
|
||||
|
||||
const register = async (userInfo) => {
|
||||
const register = async (userInfo: AuthCredentials) => {
|
||||
error.value = null;
|
||||
try {
|
||||
const res = await request.post('/auth/register', userInfo)
|
||||
return res
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '注册失败';
|
||||
throw error // 或者您可以在这里处理错误,例如显示错误消息
|
||||
}finally{
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const login = async (userInfo) => {
|
||||
const login = async (userInfo: AuthCredentials) => {
|
||||
error.value = null;
|
||||
try {
|
||||
const res = await request.post('/auth/login', userInfo)
|
||||
@@ -59,10 +60,10 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
await getProfile() // 登录成功后获取用户信息
|
||||
return res
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '登录失败';
|
||||
throw error // 或者您可以在这里处理错误,例如显示错误消息
|
||||
}finally{
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
@@ -78,11 +79,11 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
user.value = res.data.data
|
||||
return res
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '获取用户信息失败';
|
||||
|
||||
throw error
|
||||
}finally {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
@@ -96,99 +97,99 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
user.value = res.data.data
|
||||
}
|
||||
return res
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.error || '获取用户信息失败';
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '获取用户信息失败';
|
||||
throw error
|
||||
}finally{
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const updateProfile = async (userInfo) => {
|
||||
const updateProfile = async (userInfo: Partial<UserInfo>) => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const res = await request.post('/profile/update', userInfo)
|
||||
console.log('auth.js updateProfile', res.data);
|
||||
console.log('auth updateProfile', res.data);
|
||||
return res
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '更新用户信息失败';
|
||||
throw error
|
||||
}finally{
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const updatePassword = async (payload) => {
|
||||
const updatePassword = async (payload: { password: string; newpassword: string }) => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const res = await request.post('/profile/update/password', payload)
|
||||
console.log('auth.js updatePassword', res.data);
|
||||
console.log('auth updatePassword', res.data);
|
||||
return res
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '更新密码失败';
|
||||
throw error
|
||||
}finally{
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const createToken = async (newToken) => {
|
||||
const createToken = async (newToken: TokenPayload) => {
|
||||
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await request.post('/tokens', newToken)
|
||||
const response: AxiosResponse = await request.post('/tokens', newToken)
|
||||
console.log('createToken', response.data);
|
||||
return response
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '创建token失败';
|
||||
throw error
|
||||
}finally{
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const resetToken = async (id) => {
|
||||
const resetToken = async (id: number | string) => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await request.post(`/tokens/reset/${id}`)
|
||||
const response: AxiosResponse = await request.post(`/tokens/reset/${id}`)
|
||||
return response
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '重置token失败';
|
||||
throw error
|
||||
}finally{
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const updateToken = async (token) => {
|
||||
const updateToken = async (tokenInfo: Partial<TokenInfo>) => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await request.put(`/tokens/${token.id}`, token)
|
||||
const response: AxiosResponse = await request.put(`/tokens/${tokenInfo.id}`, tokenInfo)
|
||||
return response
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '更新token失败';
|
||||
throw error
|
||||
}finally {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const deleteToken = async (id) => {
|
||||
const deleteToken = async (id: number | string) => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await request.delete(`/tokens/${id}`)
|
||||
const response: AxiosResponse = await request.delete(`/tokens/${id}`)
|
||||
return response
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '删除token失败';
|
||||
throw err
|
||||
}finally {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
@@ -206,14 +207,14 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
|
||||
return {
|
||||
loading,error,
|
||||
user,role,token,
|
||||
isLoggedIn,
|
||||
setToken,loadTokenFromStorage,
|
||||
login,register,
|
||||
getProfile,updateProfile,updatePassword,refreshProfile,
|
||||
createToken,deleteToken,resetToken,updateToken,
|
||||
loading, error,
|
||||
user, role, token,
|
||||
isLoggedIn, isAdmin,
|
||||
setToken, loadTokenFromStorage,
|
||||
login, register,
|
||||
getProfile, updateProfile, updatePassword, refreshProfile,
|
||||
createToken, deleteToken, resetToken, updateToken,
|
||||
clear,
|
||||
logout
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,18 @@
|
||||
// src/stores/key.js
|
||||
// src/stores/key.ts
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import request from '@/utils/request';
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import request from '@/api/client';
|
||||
import type { ApiKey, NewApiKeyPayload } from '@/types';
|
||||
|
||||
export const useKeyStore = defineStore('key', () => {
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const error = ref<string | null>(null);
|
||||
const totalKeys = ref(0);
|
||||
const keys = ref([]);
|
||||
const key = ref(null);
|
||||
const keys = ref<ApiKey[]>([]);
|
||||
const key = ref<ApiKey | null>(null);
|
||||
|
||||
const fetchKeys = async (pageSize = 20, page = 1, active) => {
|
||||
const fetchKeys = async (pageSize = 20, page = 1, active?: boolean | boolean[]) => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
@@ -21,10 +23,10 @@ export const useKeyStore = defineStore('key', () => {
|
||||
active,
|
||||
},
|
||||
});
|
||||
|
||||
keys.value = response.data.data?.keys;
|
||||
totalKeys.value = response.data.data?.total;
|
||||
} catch (err) {
|
||||
|
||||
keys.value = response.data.data?.keys ?? [];
|
||||
totalKeys.value = response.data.data?.total ?? 0;
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '获取ApiKeys失败';
|
||||
throw error
|
||||
} finally {
|
||||
@@ -32,7 +34,7 @@ export const useKeyStore = defineStore('key', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchKey = async (id) => {
|
||||
const fetchKey = async (id: number | string) => {
|
||||
if (keys.value.length > 0) {
|
||||
const findkey = keys.value.find(item => item.id === id)
|
||||
if (findkey) {
|
||||
@@ -42,19 +44,17 @@ export const useKeyStore = defineStore('key', () => {
|
||||
}
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
// const findkey = keys.find(item=>item.id === id)
|
||||
// console.log('findkey',findkey)
|
||||
try {
|
||||
const response = await request.get(`/keys/${id}`);
|
||||
key.value = response.data.data;
|
||||
if (key.value.support_models.length < 3) {
|
||||
if (key.value && (key.value.support_models?.length ?? 0) < 3) {
|
||||
key.value.support_models = key.value.support_models_array ? JSON.stringify(key.value.support_models) : ''
|
||||
}
|
||||
if (!key.value.support_models_array) {
|
||||
if (key.value && !key.value.support_models_array) {
|
||||
key.value.support_models_array = key.value.support_models ? JSON.parse(key.value.support_models) : []
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '获取ApiKey失败';
|
||||
throw error
|
||||
} finally {
|
||||
@@ -62,19 +62,19 @@ export const useKeyStore = defineStore('key', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const refreshKey = async (id) => {
|
||||
const refreshKey = async (id: number | string) => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await request.get(`/keys/${id}`);
|
||||
key.value = response.data.data;
|
||||
if (key.value.support_models.length < 3) {
|
||||
if (key.value && (key.value.support_models?.length ?? 0) < 3) {
|
||||
key.value.support_models = key.value.support_models_array ? JSON.stringify(key.value.support_models) : ''
|
||||
}
|
||||
if (!key.value.support_models_array) {
|
||||
if (key.value && !key.value.support_models_array) {
|
||||
key.value.support_models_array = key.value.support_models ? JSON.parse(key.value.support_models) : []
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '获取ApiKey失败';
|
||||
throw error
|
||||
} finally {
|
||||
@@ -82,13 +82,13 @@ export const useKeyStore = defineStore('key', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const createKey = async (data) => {
|
||||
const createKey = async (data: NewApiKeyPayload) => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await request.post('/keys', data);
|
||||
const response: AxiosResponse = await request.post('/keys', data);
|
||||
return response;
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '创建ApiKey失败';
|
||||
throw error
|
||||
} finally {
|
||||
@@ -96,13 +96,13 @@ export const useKeyStore = defineStore('key', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const updateKey = async (key) => {
|
||||
const updateKey = async (keyInfo: ApiKey) => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await request.put(`/keys/${key.id}`, key);
|
||||
const response: AxiosResponse = await request.put(`/keys/${keyInfo.id}`, keyInfo);
|
||||
return response;
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '更新ApiKey失败';
|
||||
throw error
|
||||
} finally {
|
||||
@@ -110,14 +110,14 @@ export const useKeyStore = defineStore('key', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const keyOption = async (option, ids) => {
|
||||
const keyOption = async (option: string, ids: (number | string)[]) => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await request.post(`/keys/batch/${option}`, { ids });
|
||||
const response: AxiosResponse = await request.post(`/keys/batch/${option}`, { ids });
|
||||
|
||||
return response
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '操作失败';
|
||||
throw err;
|
||||
} finally {
|
||||
@@ -137,4 +137,3 @@ export const useKeyStore = defineStore('key', () => {
|
||||
keyOption,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
// src/stores/user.js
|
||||
// src/stores/user.ts
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import request from '@/utils/request';
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import request from '@/api/client';
|
||||
import type { UserInfo, NewUserPayload } from '@/types';
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const users = ref([]);
|
||||
const users = ref<UserInfo[]>([]);
|
||||
const totalUsers = ref(0);
|
||||
const user = ref(null);
|
||||
const user = ref<UserInfo | null>(null);
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
async function createUser(userData) {
|
||||
async function createUser(userData: NewUserPayload) {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await request.post('/users', userData);
|
||||
const response: AxiosResponse = await request.post('/users', userData);
|
||||
return response
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.error || '创建用户失败'
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '创建用户失败'
|
||||
throw err;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function listUser(pageSize = 20, page = 1, active) {
|
||||
async function listUser(pageSize = 20, page = 1, active?: boolean | boolean[]) {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
@@ -35,9 +37,9 @@ export const useUserStore = defineStore('user', () => {
|
||||
active,
|
||||
},
|
||||
});
|
||||
users.value = response.data.data?.users;
|
||||
totalUsers.value = response.data.data?.total;
|
||||
} catch (err) {
|
||||
users.value = response.data.data?.users ?? [];
|
||||
totalUsers.value = response.data.data?.total ?? 0;
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '获取用户列表失败';
|
||||
throw err;
|
||||
} finally {
|
||||
@@ -45,15 +47,15 @@ export const useUserStore = defineStore('user', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function getUser(id) {
|
||||
async function getUser(id: number | string) {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await request.get(`/users/${id}`);
|
||||
console.log('getUser response',response);
|
||||
console.log('getUser response', response);
|
||||
user.value = response.data.data;
|
||||
return response
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '获取用户信息失败';
|
||||
throw err;
|
||||
} finally {
|
||||
@@ -61,15 +63,15 @@ export const useUserStore = defineStore('user', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshUser(id) {
|
||||
async function refreshUser(id: number | string) {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await request.get(`/users/${id}`);
|
||||
console.log('getUser response',response);
|
||||
console.log('getUser response', response);
|
||||
user.value = response.data.data;
|
||||
return response
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '获取用户信息失败';
|
||||
throw err;
|
||||
} finally {
|
||||
@@ -77,14 +79,14 @@ export const useUserStore = defineStore('user', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function editUser(id, userData) {
|
||||
async function editUser(id: number | string, userData: Partial<UserInfo>) {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response= await request.put(`/users/${id}`, userData);
|
||||
console.log('editUser',response);
|
||||
const response: AxiosResponse = await request.put(`/users/${id}`, userData);
|
||||
console.log('editUser', response);
|
||||
return response
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '编辑用户失败';
|
||||
throw err;
|
||||
} finally {
|
||||
@@ -92,13 +94,13 @@ export const useUserStore = defineStore('user', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser(id) {
|
||||
async function deleteUser(id: number | string) {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await request.delete(`/users/${id}`);
|
||||
const response: AxiosResponse = await request.delete(`/users/${id}`);
|
||||
return response
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '删除用户失败';
|
||||
throw err;
|
||||
} finally {
|
||||
@@ -106,14 +108,14 @@ export const useUserStore = defineStore('user', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function userOption(option, ids) {
|
||||
async function userOption(option: string, ids: (number | string)[]) {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await request.post(`/users/batch/${option}`, { ids });
|
||||
|
||||
const response: AxiosResponse = await request.post(`/users/batch/${option}`, { ids });
|
||||
|
||||
return response
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '操作失败';
|
||||
throw err;
|
||||
} finally {
|
||||
@@ -135,4 +137,4 @@ export const useUserStore = defineStore('user', () => {
|
||||
deleteUser,
|
||||
userOption,
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -1,18 +1,20 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import request from "@/utils/request";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import request from "@/api/client";
|
||||
import { useRouter } from "vue-router";
|
||||
import { startRegistration, startAuthentication } from "@simplewebauthn/browser";
|
||||
import { useAuthStore } from "./auth";
|
||||
import type { PasskeyInfo } from "@/types";
|
||||
|
||||
export const useWebAuthStore = defineStore("webauth", () => {
|
||||
const router = useRouter();
|
||||
// const token = ref(localStorage.getItem("token") || "");
|
||||
|
||||
const passkeys = ref(null);
|
||||
const passkeys = ref<PasskeyInfo[] | null>(null);
|
||||
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
const addPasskey = async () => {
|
||||
error.value = "";
|
||||
@@ -29,10 +31,10 @@ export const useWebAuthStore = defineStore("webauth", () => {
|
||||
let attestation;
|
||||
try {
|
||||
// Pass 'undefined' as the second argument if you are not using an AbortSignal
|
||||
attestation = await startRegistration({optionsJSON: options});
|
||||
attestation = await startRegistration({ optionsJSON: options });
|
||||
// console.log("WebAuthn 注册结果 (Attestation):", JSON.stringify(attestation));
|
||||
error.value = null;
|
||||
} catch (regError) {
|
||||
} catch (regError: any) {
|
||||
// console.log("WebAuthn 注册失败或取消:", regError);
|
||||
if (regError.name === "NotAllowedError") {
|
||||
error.value = "Passkey 操作被取消或不允许。";
|
||||
@@ -43,11 +45,11 @@ export const useWebAuthStore = defineStore("webauth", () => {
|
||||
}
|
||||
|
||||
// 3. 将注册结果 (Attestation) 发送到后端进行验证和保存
|
||||
const res2 = await request.post("/profile/passkey", attestation);
|
||||
const res2: AxiosResponse = await request.post("/profile/passkey", attestation);
|
||||
// console.log("end:", res2);
|
||||
return res2;
|
||||
} catch (err) {
|
||||
error.value =err.response?.data?.error || "添加 Passkey 失败,请稍后重试。";
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || "添加 Passkey 失败,请稍后重试。";
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -62,13 +64,13 @@ export const useWebAuthStore = defineStore("webauth", () => {
|
||||
const res = await request.get("/auth/passkey/begin");
|
||||
// console.log("login begin:", res.data);
|
||||
const options = res.data.data.publicKey;
|
||||
|
||||
|
||||
// 2. 调用 Web Authentication API 进行认证
|
||||
let assertion;
|
||||
try {
|
||||
assertion = await startAuthentication({ optionsJSON: options });
|
||||
// console.log("WebAuthn 认证结果 (Assertion):", JSON.stringify(assertion));
|
||||
} catch (loginError) {
|
||||
} catch (loginError: any) {
|
||||
if (loginError.name === "NotAllowedError") {
|
||||
error.value = "Passkey 登录被取消或不允许。";
|
||||
} else {
|
||||
@@ -76,11 +78,11 @@ export const useWebAuthStore = defineStore("webauth", () => {
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
// 3. 将认证结果 (Assertion) 发送到后端进行验证并获取 Token
|
||||
const challenge = options.challenge; // 从 begin 接口返回的 options 中获取 challenge
|
||||
const res2 = await request.post(`/auth/passkey/finish?challenge=${challenge}`, assertion);
|
||||
|
||||
const res2: AxiosResponse = await request.post(`/auth/passkey/finish?challenge=${challenge}`, assertion);
|
||||
|
||||
// 4. 处理登录成功的响应,通常包含 Token
|
||||
if (res2.status === 200 && !!res2.data.data?.token) {
|
||||
const token = res2.data.data.token
|
||||
@@ -89,7 +91,7 @@ export const useWebAuthStore = defineStore("webauth", () => {
|
||||
await authStore.getProfile()
|
||||
return res2.data
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || err.value || "Passkey 登录失败,请稍后重试。";
|
||||
throw error;
|
||||
} finally {
|
||||
@@ -104,24 +106,24 @@ export const useWebAuthStore = defineStore("webauth", () => {
|
||||
const response = await request.get('/profile/passkeys')
|
||||
// console.log('getPasskeys',response.data.data)
|
||||
passkeys.value = response.data.data
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || '获取token列表失败';
|
||||
throw error
|
||||
}finally {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const deletePasskey = async (id) => {
|
||||
const deletePasskey = async (id: string | number) => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await request.delete(`/profile/passkeys/${id}`)
|
||||
const response: AxiosResponse = await request.delete(`/profile/passkeys/${id}`)
|
||||
return response
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || `删除passkey ${id} 失败`;
|
||||
throw error
|
||||
}finally {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
@@ -135,4 +137,4 @@ export const useWebAuthStore = defineStore("webauth", () => {
|
||||
getPasskeys,
|
||||
deletePasskey,
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@plugin "daisyui" {
|
||||
themes: light --default, dark, cupcake, emerald, pastel;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// 后端 API 数据结构,字段以后端实际返回为准,均为可选宽松定义
|
||||
export type UserInfo = {
|
||||
id: number
|
||||
username: string
|
||||
name?: string
|
||||
email?: string
|
||||
password?: string
|
||||
avatar_url?: string
|
||||
avatar?: string
|
||||
role: number
|
||||
active: boolean
|
||||
email_verified?: boolean
|
||||
timezone?: string
|
||||
language?: string
|
||||
unlimited_quota?: boolean
|
||||
used_quota?: number
|
||||
quota?: number
|
||||
created_at?: number
|
||||
updated_at?: number
|
||||
expired_at?: number
|
||||
format_expired_at?: string
|
||||
tokens?: TokenInfo[]
|
||||
// 后端返回字段较松散,允许扩展
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type TokenInfo = {
|
||||
id: number
|
||||
name: string
|
||||
key?: string
|
||||
active: boolean
|
||||
quota?: number
|
||||
used_quota?: number
|
||||
unlimited_quota?: boolean
|
||||
expired_at?: number
|
||||
// 部分视图沿用旧字段名
|
||||
expiredAt?: number
|
||||
userid?: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type ApiKey = {
|
||||
id: number
|
||||
type: string
|
||||
name: string
|
||||
apikey?: string
|
||||
active: boolean
|
||||
endpoint?: string
|
||||
resource_name?: string
|
||||
api_secret?: string
|
||||
model_prefix?: string
|
||||
model_alias?: string
|
||||
parameters?: string
|
||||
support_models?: string
|
||||
support_models_array?: string[]
|
||||
selected?: boolean
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type PasskeyInfo = {
|
||||
id: string | number
|
||||
name?: string
|
||||
created_at?: number
|
||||
sign_count?: number
|
||||
device_type?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type AuthCredentials = {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export type TokenPayload = {
|
||||
name: string
|
||||
key?: string
|
||||
user_id?: number | string
|
||||
active: boolean
|
||||
quota: number
|
||||
unlimited_quota: boolean
|
||||
expired_at: number
|
||||
format_expired_at?: string
|
||||
never_expired?: boolean
|
||||
}
|
||||
|
||||
export type NewApiKeyPayload = {
|
||||
name: string
|
||||
type: string
|
||||
apikey: string
|
||||
active: boolean
|
||||
endpoint?: string
|
||||
resource_name?: string
|
||||
api_secret?: string
|
||||
model_prefix?: string
|
||||
model_alias?: string
|
||||
parameters?: string
|
||||
support_models?: string
|
||||
support_models_array?: string[]
|
||||
}
|
||||
|
||||
export type NewUserPayload = {
|
||||
username: string
|
||||
password: string
|
||||
email?: string
|
||||
name?: string
|
||||
role?: number
|
||||
active?: boolean
|
||||
quota?: number
|
||||
unlimited_quota?: boolean
|
||||
language?: string
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
// src/utils/format-date.js
|
||||
// src/utils/format-date.ts
|
||||
|
||||
export function dateToUnix(dateString) {
|
||||
export function dateToUnix(dateString: string): number {
|
||||
const date = new Date(dateString);
|
||||
return Math.floor(date.getTime() / 1000);
|
||||
}
|
||||
|
||||
export function unixToDate(timestamp) {
|
||||
export function unixToDate(timestamp: number): string {
|
||||
const date = new Date(timestamp * 1000);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
@@ -13,7 +13,7 @@ export function unixToDate(timestamp) {
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
export function formatDateTime(unixTimestamp) {
|
||||
export function formatDateTime(unixTimestamp: number | undefined | null): string {
|
||||
// 如果时间戳不存在或为0,返回'未知'
|
||||
if (!unixTimestamp) return "未知";
|
||||
|
||||
@@ -1,24 +1,43 @@
|
||||
import type { Component } from 'vue'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import {
|
||||
LayoutDashboardIcon,
|
||||
ShieldPlus,
|
||||
UsersRoundIcon,
|
||||
KeyRoundIcon,
|
||||
MessageSquareIcon,
|
||||
SettingsIcon,
|
||||
UserIcon,
|
||||
CommandIcon,
|
||||
BracesIcon,
|
||||
} from 'lucide-vue-next'
|
||||
} from '@lucide/vue'
|
||||
|
||||
export const routes = [
|
||||
export type MenuLink = { type: 'link'; label: string; to: string; icon?: Component }
|
||||
|
||||
export type MenuItem =
|
||||
| MenuLink
|
||||
| { type: 'title'; label: string }
|
||||
| { type: 'submenu'; label: string; to?: string; icon?: Component; open?: boolean; badge?: string; children?: MenuItem[] }
|
||||
|
||||
// 路由 meta 扩展字段
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
title?: string
|
||||
icon?: Component
|
||||
showInSidebar?: boolean
|
||||
requiresAuth?: boolean
|
||||
open?: boolean
|
||||
badge?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
{ path: '/', name: 'Home',component: () => import('@/views/Home.vue') },
|
||||
{ path: '/404', name: '404',component: () => import('@/views/404.vue') },
|
||||
{ path: '/404', name: '404',component: () => import('@/views/error/NotFound.vue') },
|
||||
|
||||
{ path: '/login', name: 'Login', component: () => import('@/views/Login.vue') },
|
||||
{ path: '/signup', name: 'Signup', component: () => import('@/views/Signup.vue') },
|
||||
{ path: '/login', name: 'Login', component: () => import('@/views/auth/Login.vue') },
|
||||
{ path: '/signup', name: 'Signup', component: () => import('@/views/auth/Signup.vue') },
|
||||
|
||||
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('@/views/404.vue')}, // Catch all 404
|
||||
{ path: '/dashboard', name: 'Dashboard', component: ()=>import('@/views/DashBoard.vue'), meta: { requiresAuth: true, title: 'Dashboard', showInSidebar: false },redirect: '/dashboard/overview', children:[
|
||||
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('@/views/error/NotFound.vue')}, // Catch all 404
|
||||
{ path: '/dashboard', name: 'Dashboard', component: ()=>import('@/layouts/DashboardLayout.vue'), meta: { requiresAuth: true, title: 'Dashboard', showInSidebar: false },redirect: '/dashboard/overview', children:[
|
||||
{ path: 'overview', name: 'Overview', component: ()=>import('@/views/dashboard/Overview.vue'),meta: { title: 'Overview', icon: LayoutDashboardIcon, showInSidebar: true } },
|
||||
{ path: 'tokens', name: 'Tokens', component: ()=>import('@/views/dashboard/Tokens.vue'),meta: { title: 'Tokens', icon: BracesIcon, showInSidebar: true } },
|
||||
{ path: 'manager', name: 'Manager',meta: { title: 'Manager', icon: CommandIcon, showInSidebar: true, open: true, badge: 'Admin' }, redirect: '/dashboard/manager/users',children:[
|
||||
@@ -34,42 +53,55 @@ export const routes = [
|
||||
]},
|
||||
];
|
||||
|
||||
export function generateMenuItemsFromRoutes(routes, userRole, parentPath = '') {
|
||||
const menuItems = [];
|
||||
export function generateMenuItemsFromRoutes(routes: RouteRecordRaw[], userRole: number, parentPath = ''): MenuItem[] {
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
for (const route of routes) {
|
||||
if (route.meta && route.meta.title && route.meta.showInSidebar) {
|
||||
const fullPath = parentPath + '/' + route.path.replace(/^\//, '');
|
||||
const menuItem = {
|
||||
label: route.meta.title,
|
||||
to: fullPath,
|
||||
icon: route.meta.icon,
|
||||
};
|
||||
|
||||
if (route.children && route.children.length > 0) {
|
||||
if (route.name === 'Manager' && userRole < 10) {
|
||||
continue;
|
||||
}
|
||||
menuItem.type = 'submenu';
|
||||
menuItem.open = route.meta.open !== undefined ? route.meta.open : false;
|
||||
menuItem.badge = route.meta.badge;
|
||||
menuItem.children = generateMenuItemsFromRoutes(route.children, userRole, fullPath);
|
||||
} else {
|
||||
menuItem.type = 'link';
|
||||
}
|
||||
const menuItem: MenuItem = {
|
||||
type: 'submenu',
|
||||
label: route.meta.title,
|
||||
to: fullPath,
|
||||
icon: route.meta.icon,
|
||||
open: route.meta.open !== undefined ? route.meta.open : false,
|
||||
badge: route.meta.badge,
|
||||
children: generateMenuItemsFromRoutes(route.children, userRole, fullPath),
|
||||
};
|
||||
|
||||
if (route.name === 'Overview') {
|
||||
menuItems.push(menuItem);
|
||||
menuItems.push({ type: 'title', label: 'Apps' });
|
||||
continue
|
||||
}
|
||||
|
||||
if (route.name === 'Overview') {
|
||||
menuItems.push(menuItem);
|
||||
menuItems.push({ type: 'title', label: 'Apps' });
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
const menuItem: MenuItem = {
|
||||
type: 'link',
|
||||
label: route.meta.title,
|
||||
to: fullPath,
|
||||
icon: route.meta.icon,
|
||||
};
|
||||
|
||||
menuItems.push(menuItem);
|
||||
if (route.name === 'Overview') {
|
||||
menuItems.push(menuItem);
|
||||
menuItems.push({ type: 'title', label: 'Apps' });
|
||||
continue
|
||||
}
|
||||
|
||||
menuItems.push(menuItem);
|
||||
}
|
||||
} else if (route.path === '/dashboard' && route.children) {
|
||||
|
||||
|
||||
menuItems.push(...generateMenuItemsFromRoutes(route.children, userRole, '/dashboard'));
|
||||
}
|
||||
}
|
||||
|
||||
return menuItems;
|
||||
}
|
||||
}
|
||||
+11
-10
@@ -3,7 +3,7 @@
|
||||
<div class="navbar fixed w-full top-0 z-50 backdrop-blur-sm bg-base-100/50">
|
||||
<div class="container mx-auto flex justify-between items-center p-1 rounded-box">
|
||||
<div class="flex items-center h-12 w-12 rounded-full text-l">
|
||||
<img src="../assets/logo.svg" alt="Logo" class="select-none">
|
||||
<img src="@/assets/logo.svg" alt="Logo" class="select-none">
|
||||
<span class="hidden sm:flex text-xl font-bold">
|
||||
<a href="/" class="text-base-content hover:no-underline">OpenTeam</a>
|
||||
</span>
|
||||
@@ -25,7 +25,7 @@
|
||||
<main class="flex-grow flex flex-col justify-center items-center pt-16">
|
||||
<div class="text-center">
|
||||
<div class="flex items-center justify-center my-4 outline-none select-none">
|
||||
<img src="../assets/openteam.png" alt="Project Logo" class="h-40">
|
||||
<img src="@/assets/openteam.png" alt="Project Logo" class="h-40">
|
||||
</div>
|
||||
<h1 class="text-4xl font-bold mb-4">
|
||||
<a class="text-gray-600" href="https://github.com/mirrors2/opencatd-open">OpenTeam</a>
|
||||
@@ -65,7 +65,7 @@
|
||||
<p class="mb-2">欢迎加入我们的Telegram频道,获取最新动态和帮助</p>
|
||||
<div class="flex justify-center mb-4">
|
||||
<a href="https://t.me/OpenTeamLLM" target="_blank" class="tooltip tooltip-bottom backdrop-blur-0" data-tip="Telegram Channel">
|
||||
<img src="../assets/openteam_channel.jpg" alt="Telegram Group QR Code"
|
||||
<img src="@/assets/openteam_channel.jpg" alt="Telegram Group QR Code"
|
||||
class="w-40 fill-current backdrop-blur-0 select-none">
|
||||
</a>
|
||||
|
||||
@@ -95,27 +95,28 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, inject } from 'vue';
|
||||
import LineSegmentFlow from '@/components/LineSegmentFlow.vue';
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import LineSegmentFlow from '@/components/common/LineSegmentFlow.vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { useToast } from '@/composables/toast';
|
||||
|
||||
const currentYear = ref('');
|
||||
const url = ref('');
|
||||
const { setToast } = inject('toast');
|
||||
const { setToast } = useToast();
|
||||
|
||||
const copyUrl = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(url.value);
|
||||
setToast('复制成功!', 'info');
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
setToast('复制失败,请手动复制。', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const star = ref(0);
|
||||
const getGithubStars = async () => {
|
||||
const res = await fetch('https://ungh.cc/repos/mirrors2/openteam', { next: { revalidate: 3600 } });
|
||||
const getGithubStars = async (): Promise<number> => {
|
||||
const res = await fetch('https://ungh.cc/repos/mirrors2/openteam', { next: { revalidate: 3600 } } as RequestInit);
|
||||
const data = await res.json();
|
||||
return data.repo.stars;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="min-h-screen flex items-center justify-center p-4">
|
||||
<div class="card w-full max-w-md bg-base-100 shadow-xl">
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<img src="../assets/openteam.webp" alt="Company Logo" class="h-32 w-auto mx-auto mb-0 pb-0 select-none hover:cursor-pointer"
|
||||
<img src="@/assets/openteam.webp" alt="Company Logo" class="h-32 w-auto mx-auto mb-0 pb-0 select-none hover:cursor-pointer"
|
||||
@click="$router.push('/')" />
|
||||
|
||||
<h2 class="card-title text-md sm:text-xl mb-6 justify-center flex">
|
||||
@@ -103,19 +103,20 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, inject, onMounted } from 'vue'
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useWebAuthStore } from '@/stores/webauth';
|
||||
// import request from '@/utils/request';
|
||||
import { useToast } from '@/composables/toast';
|
||||
// import request from '@/api/client';
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore();
|
||||
const webauthStore = useWebAuthStore();
|
||||
const { setToast } = inject('toast');
|
||||
const { setToast } = useToast();
|
||||
|
||||
const error = ref(null)
|
||||
const error = ref<string | null>(null)
|
||||
const user = reactive({
|
||||
username: localStorage.getItem('account') || '',
|
||||
password: localStorage.getItem('password') || '',
|
||||
@@ -130,6 +131,13 @@ onMounted(() => {
|
||||
supportWebAuth.value = !!window.PublicKeyCredential;
|
||||
})
|
||||
|
||||
// store 的 catch 里 throw 的是 error ref,这里统一取出可展示的错误信息
|
||||
const errMsg = (err: any): string => {
|
||||
if (typeof err === 'string') return err
|
||||
if (err?.__v_isRef) return errMsg(err.value)
|
||||
return err?.response?.data?.error || err?.message || String(err)
|
||||
}
|
||||
|
||||
|
||||
const handleLogin = async () => {
|
||||
error.value = null;
|
||||
@@ -139,7 +147,7 @@ const handleLogin = async () => {
|
||||
if (user.rember) {
|
||||
localStorage.setItem('account', user.username);
|
||||
localStorage.setItem('password', user.password);
|
||||
localStorage.setItem('rember', user.rember);
|
||||
localStorage.setItem('rember', String(user.rember));
|
||||
} else {
|
||||
localStorage.removeItem('account');
|
||||
localStorage.removeItem('password');
|
||||
@@ -148,9 +156,9 @@ const handleLogin = async () => {
|
||||
setToast('登录成功', 'success');
|
||||
router.push('/dashboard');
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error('Login error:', err);
|
||||
error.value = err
|
||||
error.value = errMsg(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,13 +166,13 @@ const handlePasskeyLogin = async () => {
|
||||
error.value = null;
|
||||
try {
|
||||
const res = await webauthStore.loginPasskey();
|
||||
if (!!res.code && res.code === 200) {
|
||||
if (!!res?.code && res.code === 200) {
|
||||
setToast('登录成功', 'success');
|
||||
router.push('/dashboard');
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error('Passkey login error:', err);
|
||||
error.value = err
|
||||
error.value = errMsg(err)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="min-h-screen flex items-center justify-center p-4">
|
||||
<div class="card w-full max-w-md bg-base-100 shadow-xl">
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<img src="../assets/openteam.webp" alt="Logo" class="h-32 w-auto mx-auto mb-0 pb-0 select-none hover:cursor-pointer" @click="$router.push('/')"/>
|
||||
<img src="@/assets/openteam.webp" alt="Logo" class="h-32 w-auto mx-auto mb-0 pb-0 select-none hover:cursor-pointer" @click="$router.push('/')"/>
|
||||
|
||||
<h2 class="card-title text-md sm:text-xl mb-2 justify-center flex">
|
||||
Create Your Account
|
||||
@@ -53,14 +53,15 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue'
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useToast } from '@/composables/toast'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const { setToast } = inject('toast');
|
||||
const { setToast } = useToast();
|
||||
|
||||
const error = ref('');
|
||||
|
||||
@@ -72,7 +73,7 @@ const handleRegister = async () => {
|
||||
if (password.value !== confirmPassword.value) {
|
||||
alert("密码不一致");
|
||||
return
|
||||
}
|
||||
}
|
||||
try {
|
||||
let res = await authStore.register({
|
||||
username: username.value,
|
||||
@@ -46,7 +46,7 @@
|
||||
<option value="github">Github</option>
|
||||
<option value="openai-compatible">OpenAI Compatible</option>
|
||||
</select>
|
||||
<button type="button" @click="togglePasswordVisibility" tabindex="-1"
|
||||
<button type="button" tabindex="-1"
|
||||
class="absolute inset-y-0 left-0 px-3 flex items-center text-base-content/60 hover:text-base-content/80 focus:outline-none focus:ring-0 rounded-r-md"
|
||||
id="password-visibility-toggle">
|
||||
<img :src="apiKeyImageUrl(newApiKey.type)" class="w-5 h-5" alt="">
|
||||
@@ -135,7 +135,7 @@
|
||||
</label>
|
||||
<!-- <textarea id="support_models" v-model="newApiKey.support_models_text"
|
||||
placeholder='["model1", "model2"]' class="textarea textarea-sm textarea-bordered w-full"></textarea> -->
|
||||
<el-input-tag v-model="newApiKey.support_models_array" :trigger="'Enter'" clearable
|
||||
<TagInput v-model="newApiKey.support_models_array" clearable
|
||||
placeholder="Please input" @change="onchange_supportmodel" />
|
||||
</div>
|
||||
|
||||
@@ -180,22 +180,23 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, inject } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useKeyStore } from '@/stores/key';
|
||||
import BreadcrumbHeader from '@/components/dashboard/BreadcrumbHeader.vue';
|
||||
import TagInput from '@/components/common/TagInput.vue';
|
||||
import { useToast } from '@/composables/toast';
|
||||
import type { NewApiKeyPayload } from '@/types';
|
||||
|
||||
const router = useRouter()
|
||||
const keyStore = useKeyStore()
|
||||
const { setToast } = inject('toast')
|
||||
const error = ref(null)
|
||||
const { setToast } = useToast()
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// Control advanced options visibility
|
||||
const showAdvancedOptions = ref(false)
|
||||
|
||||
// Initialize API key object
|
||||
const newApiKey = ref({
|
||||
const newApiKey = ref<NewApiKeyPayload>({
|
||||
name: '',
|
||||
type: '',
|
||||
apikey: '',
|
||||
@@ -245,7 +246,7 @@ const cancel = () => {
|
||||
emit('closeModal', true)
|
||||
}
|
||||
|
||||
const apiKeyImageMap = {
|
||||
const apiKeyImageMap: Record<string, string> = {
|
||||
'openai': '/assets/openai.svg',
|
||||
'claude': '/assets/claude.svg',
|
||||
'gemini': '/assets/gemini.svg',
|
||||
@@ -254,7 +255,7 @@ const apiKeyImageMap = {
|
||||
|
||||
};
|
||||
|
||||
const apiKeyImageUrl = (keytype) => {
|
||||
const apiKeyImageUrl = (keytype: string) => {
|
||||
return apiKeyImageMap[keytype] || '/assets/logo.svg';
|
||||
};
|
||||
|
||||
@@ -277,7 +278,7 @@ const createApiKey = async () => {
|
||||
|
||||
// Attempt to parse parameters JSON
|
||||
try {
|
||||
JSON.parse(newApiKey.value.parameters);
|
||||
JSON.parse(newApiKey.value.parameters || '{}');
|
||||
} catch (e) {
|
||||
setToast('Invalid JSON format for Parameters.', 'error');
|
||||
return;
|
||||
@@ -291,17 +292,19 @@ const createApiKey = async () => {
|
||||
// Optionally navigate or reset form
|
||||
emit('closeModal', true)
|
||||
} else {
|
||||
setToast(res.error || res.data?.message || 'Failed to create API Key', 'error')
|
||||
setToast(res.data?.error || res.data?.message || 'Failed to create API Key', 'error')
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.log('createApiKey error:', err)
|
||||
error.value = err || 'Failed to create API Key'
|
||||
error.value = err?.message || String(err) || 'Failed to create API Key'
|
||||
// setToast(error.response?.data?.error || 'Failed to create API Key', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const emit = defineEmits(['closeModal'])
|
||||
const emit = defineEmits<{
|
||||
(e: 'closeModal', value: boolean): void
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -125,8 +125,8 @@
|
||||
</label>
|
||||
<!-- <textarea id="support_models" v-model="key.support_models_text"
|
||||
placeholder='["model1", "model2"]' class="textarea textarea-sm textarea-bordered w-full"></textarea> -->
|
||||
<el-input-tag v-model="key.support_models_array" :trigger="'Enter'" clearable
|
||||
placeholder="Please input" @change="onchange_supportmodel"/>
|
||||
<TagInput v-model="key.support_models_array" clearable
|
||||
placeholder="Please input" @change="onchange_supportmodel" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
@@ -157,31 +157,28 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, inject, reactive } from 'vue';
|
||||
import { useRoute,useRouter } from 'vue-router';
|
||||
import { Eye, EyeOff, BadgeCheck, Send, CircleX, CircleCheckBig, TrashIcon, Infinity } from 'lucide-vue-next';
|
||||
import { useKeyStore } from '../../stores/key';
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useKeyStore } from '@/stores/key';
|
||||
import BreadcrumbHeader from '@/components/dashboard/BreadcrumbHeader.vue';
|
||||
import TagInput from '@/components/common/TagInput.vue';
|
||||
import { useToast } from '@/composables/toast';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const keyStore = useKeyStore();
|
||||
const { setToast } = inject('toast');
|
||||
const { setToast } = useToast();
|
||||
|
||||
const keyId = computed(() => route.query.id);
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
});
|
||||
|
||||
const key = computed(() => keyStore.key);
|
||||
const loading = computed(() => keyStore.loading);
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('keyId', keyId.value)
|
||||
if (keyId.value) {
|
||||
await keyStore.fetchKey(keyId.value);
|
||||
await keyStore.fetchKey(keyId.value as string);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -194,7 +191,7 @@ const keyOption = reactive([
|
||||
{name: 'openai-compatible', label: 'OpenAI Compatible'}
|
||||
])
|
||||
|
||||
const apiKeyImageMap = {
|
||||
const apiKeyImageMap: Record<string, string> = {
|
||||
'openai': '/assets/openai.svg',
|
||||
'claude': '/assets/claude.svg',
|
||||
'gemini': '/assets/gemini.svg',
|
||||
@@ -202,16 +199,17 @@ const apiKeyImageMap = {
|
||||
'github': '/assets/github.svg'
|
||||
};
|
||||
|
||||
const apiKeyImageUrl = (keytype) => {
|
||||
const apiKeyImageUrl = (keytype: string) => {
|
||||
return apiKeyImageMap[keytype] || '/assets/logo.svg';
|
||||
};
|
||||
|
||||
const onchange_supportmodel = () => {
|
||||
if (!key.value) return;
|
||||
key.value.support_models = JSON.stringify(key.value.support_models_array)
|
||||
}
|
||||
|
||||
const updateKey = async () => {
|
||||
|
||||
if (!key.value) return;
|
||||
try {
|
||||
const res = await keyStore.updateKey(key.value);
|
||||
console.log('updateKey', res)
|
||||
@@ -219,7 +217,7 @@ const updateKey = async () => {
|
||||
setToast(`Key ${key.value.name} updated`, 'success');
|
||||
}
|
||||
await keyStore.refreshKey(key.value.id);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error('Error updating key:', err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -148,27 +148,29 @@
|
||||
|
||||
<!-- Pagination -->
|
||||
<Pagination :currentPage="currentPage" :totalItems="totalItems" :pageSize="pageSize"
|
||||
:pageSizeOptions="[10, 20, 50, 100]" @changePage="changePage" @changePageSize="changePageSize" />
|
||||
:pageSizeOptions="[10, 20, 50, 100]" @changePage="changePage" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, inject, computed } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import BreadcrumbHeader from '@/components/dashboard/BreadcrumbHeader.vue';
|
||||
import Pagination from '@/components/Pagination.vue';
|
||||
import Pagination from '@/components/common/Pagination.vue';
|
||||
import KeyNew from '@/views/dashboard/KeyNew.vue';
|
||||
import { useKeyStore } from '@/stores/key';
|
||||
import { useToast } from '@/composables/toast';
|
||||
import type { ApiKey } from '@/types';
|
||||
|
||||
import {
|
||||
BadgeXIcon, BadgeCheckIcon, EyeIcon, PlusIcon, Settings2Icon,
|
||||
TrashIcon, Infinity
|
||||
} from 'lucide-vue-next';
|
||||
} from '@lucide/vue';
|
||||
|
||||
const router = useRouter();
|
||||
const keyStore = useKeyStore();
|
||||
const { setToast } = inject('toast');
|
||||
const { setToast } = useToast();
|
||||
|
||||
onMounted(async () => {
|
||||
await keyStore.fetchKeys();
|
||||
@@ -184,19 +186,14 @@ const totalItems = computed(() => keyStore.totalKeys);
|
||||
|
||||
|
||||
// 封装公共的用户列表获取方法
|
||||
const fetchKeys = async (size = pageSize.value, page = currentPage.value, active = selectedStatuses.map(status => status.value)) => {
|
||||
const fetchKeys = async (size?: number, page?: number, active?: boolean[] | boolean) => {
|
||||
currentPage.value = page || currentPage.value;
|
||||
// console.log('pagesize', pageSize.value, 'page', currentPage.value, 'active', selectedStatuses.map(status => status.value));
|
||||
await keyStore.fetchKeys(size, page, active);
|
||||
await keyStore.fetchKeys(size ?? pageSize.value, page ?? currentPage.value, active ?? selectedStatuses.map(status => status.value));
|
||||
};
|
||||
|
||||
// 组件挂载时加载用户数据
|
||||
// onMounted(async () => {
|
||||
// await fetchKeys();
|
||||
// });
|
||||
|
||||
// 分页与页面大小变化
|
||||
const changePage = async (page, size) => {
|
||||
const changePage = async (page: number, size: number) => {
|
||||
if (page == currentPage.value && size == pageSize.value) {
|
||||
return
|
||||
}
|
||||
@@ -205,11 +202,9 @@ const changePage = async (page, size) => {
|
||||
await fetchKeys();
|
||||
};
|
||||
|
||||
const changePageSize = changePage;
|
||||
|
||||
// 复选框选择状态
|
||||
const selectAll = ref(false)
|
||||
const selectedKeys = ref([])
|
||||
const selectedKeys = ref<ApiKey[]>([])
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
if (keys.value.length === 0) {
|
||||
@@ -219,7 +214,7 @@ const toggleSelectAll = () => {
|
||||
|
||||
if (selectAll.value) {
|
||||
// Select all on the current page
|
||||
selectedKeys.value = users.value.map(user => user)
|
||||
selectedKeys.value = keys.value.map(key => key)
|
||||
} else {
|
||||
// Clear all selections
|
||||
selectedKeys.value = []
|
||||
@@ -227,7 +222,7 @@ const toggleSelectAll = () => {
|
||||
|
||||
}
|
||||
|
||||
const toggleUserSelection = (key) => {
|
||||
const toggleUserSelection = (key: ApiKey) => {
|
||||
if (selectedKeys.value.includes(key)) {
|
||||
selectedKeys.value = selectedKeys.value.filter(selected => selected !== key);
|
||||
} else {
|
||||
@@ -238,9 +233,9 @@ const toggleUserSelection = (key) => {
|
||||
|
||||
// 状态筛选
|
||||
const statusOptions = ['Active', 'Inactive'];
|
||||
const selectedStatuses = reactive([]);
|
||||
const selectedStatuses = reactive<{ status: string; value: boolean }[]>([]);
|
||||
|
||||
const toggleStatusFilter = async (status) => {
|
||||
const toggleStatusFilter = async (status: string) => {
|
||||
const statusValue = status === 'Active';
|
||||
const index = selectedStatuses.findIndex(item => item.status === status);
|
||||
|
||||
@@ -254,12 +249,12 @@ const toggleStatusFilter = async (status) => {
|
||||
};
|
||||
|
||||
// 处理批量操作
|
||||
const handleBatchAction = async (action) => {
|
||||
const handleBatchAction = async (action: string) => {
|
||||
if (selectedKeys.value.length === 0) {
|
||||
return setToast('请选择数据', 'error');
|
||||
}
|
||||
if (!['enable', 'disable', 'delete'].includes(action)) {
|
||||
return setToast('无效的操作 ${action}', 'error');
|
||||
return setToast(`无效的操作 ${action}`, 'error');
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -273,14 +268,14 @@ const handleBatchAction = async (action) => {
|
||||
selectAll.value = false;
|
||||
await fetchKeys();
|
||||
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error(`批量操作 ${action} 失败:`, error);
|
||||
setToast('批量操作失败', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// 更新用户状态
|
||||
const updateStatus = async (key) => {
|
||||
const updateStatus = async (key: ApiKey) => {
|
||||
try {
|
||||
const action = key.active ? 'enable' : 'disable';
|
||||
const res = await keyStore.keyOption(action, [key.id]);
|
||||
@@ -289,24 +284,24 @@ const updateStatus = async (key) => {
|
||||
setToast(`Key ${key.name} has been ${action}`, 'success');
|
||||
}
|
||||
await fetchKeys();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('状态更新失败:', error);
|
||||
setToast('状态更新失败', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const viewKey = (key) => {
|
||||
const viewKey = (key: ApiKey) => {
|
||||
router.push({ name: 'ApiKeyView', query: { id: key.id } });
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const confirmDeleteKey = async (key) => {
|
||||
const confirmDeleteKey = async (key: ApiKey) => {
|
||||
if (confirm(`确认删除 ${key.name}?`)) {
|
||||
await deleteKey(key);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteKey = async (key) => {
|
||||
const deleteKey = async (key: ApiKey) => {
|
||||
try {
|
||||
const res = await keyStore.keyOption('delete', [key.id]);
|
||||
if (res.data?.code === 200) {
|
||||
@@ -314,13 +309,13 @@ const deleteKey = async (key) => {
|
||||
}
|
||||
|
||||
await fetchKeys();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('删除失败:', error);
|
||||
setToast('删除失败', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const displayIcon = (apitype) => {
|
||||
const displayIcon = (apitype: string) => {
|
||||
switch (apitype) {
|
||||
case 'openai':
|
||||
return '/assets/openai.svg';
|
||||
@@ -339,7 +334,7 @@ const displayIcon = (apitype) => {
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
const modalRef = ref(null);
|
||||
const modalRef = ref<HTMLDialogElement | null>(null);
|
||||
const closeModal = async () => {
|
||||
if (modalRef.value) {
|
||||
modalRef.value.close();
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold text-base-content/70 w-20">角色</span>
|
||||
<span class="badge" :class="user?.role > 0 ? 'badge-warning' : 'badge-ghost'">{{
|
||||
<span class="badge" :class="(user?.role || 0) > 0 ? 'badge-warning' : 'badge-ghost'">{{
|
||||
getRoleName(user?.role || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -149,8 +149,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
<script setup lang="ts">
|
||||
import { onMounted, computed } from 'vue'
|
||||
import BreadcrumbHeader from '@/components/dashboard/BreadcrumbHeader.vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useRouter } from 'vue-router';
|
||||
@@ -167,18 +167,18 @@ onMounted(async () => {
|
||||
});
|
||||
|
||||
|
||||
const getTimeOfDay = () => {
|
||||
const getTimeOfDay = (): string => {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 12) return '早上好';
|
||||
if (hour < 18) return '下午好';
|
||||
return '晚上好';
|
||||
};
|
||||
|
||||
const formatQuota = (used, total) => {
|
||||
const formatQuota = (used: number, total: number): string => {
|
||||
if (total === 0) return '无限制';
|
||||
|
||||
// 格式化金额
|
||||
const formatCurrency = (amount) => {
|
||||
const formatCurrency = (amount: number): string => {
|
||||
if (amount === 0) return '$0';
|
||||
return `$${amount.toFixed(2)}`;
|
||||
};
|
||||
@@ -188,7 +188,7 @@ const formatQuota = (used, total) => {
|
||||
};
|
||||
|
||||
|
||||
const getRoleName = (role) => {
|
||||
const getRoleName = (role: number): string => {
|
||||
switch (role) {
|
||||
case 20: return 'Root';
|
||||
case 10: return 'Admin';
|
||||
@@ -197,7 +197,7 @@ const getRoleName = (role) => {
|
||||
};
|
||||
|
||||
// 格式化日期时间
|
||||
function formatDateTime(unixTimestamp) {
|
||||
function formatDateTime(unixTimestamp?: number): string {
|
||||
// 如果时间戳不存在或为0,返回'未知'
|
||||
if (!unixTimestamp) return '未知';
|
||||
|
||||
@@ -216,7 +216,7 @@ function formatDateTime(unixTimestamp) {
|
||||
}
|
||||
|
||||
// 获取背景渐变类
|
||||
const getGradientClass = () => {
|
||||
const getGradientClass = (): string => {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 6) return 'bg-gradient-to-r from-[#e0f2f1] to-[#1a1a1a] bg-opacity-50 backdrop-blur-lg'; // 深夜到黎明:柔和的薄荷绿渐变到微黑
|
||||
if (hour < 12) return 'bg-gradient-to-r from-[#8cc7f1] to-[#cf6f26] bg-opacity-50 backdrop-blur-lg'; // 早晨:温暖的杏仁色渐变到深灰
|
||||
@@ -229,7 +229,7 @@ const getGradientClass = () => {
|
||||
// 计算配额百分比
|
||||
const quotaPercentage = computed(() => {
|
||||
if (!user.value || user.value.unlimited_quota || !user.value.quota) return 0;
|
||||
return (user.value.used_quota / user.value.quota) * 100;
|
||||
return (user.value.used_quota ?? 0) / user.value.quota * 100;
|
||||
});
|
||||
|
||||
// 获取配额颜色
|
||||
@@ -249,12 +249,12 @@ const getQuotaColorClass = computed(() => {
|
||||
});
|
||||
|
||||
// 用户最后活动时间
|
||||
const getLastActive = () => {
|
||||
const getLastActive = (): string => {
|
||||
if (!user.value || !user.value?.updated_at) return '未知';
|
||||
|
||||
const lastActive = new Date(user.value.updated_at * 1000);
|
||||
const now = new Date();
|
||||
const diff = now - lastActive;
|
||||
const diff = now.getTime() - lastActive.getTime();
|
||||
|
||||
// 转换为天/小时/分钟
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
|
||||
@@ -251,7 +251,7 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="border rounded-md p-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Github class="w-6 h-6 text-base-content/80" />
|
||||
<img src="/assets/github.svg" alt="GitHub" class="w-6 h-6 text-base-content/80" />
|
||||
<span>GitHub</span>
|
||||
</div>
|
||||
<button class="btn btn-sm" :class="isGithubConnected ? 'btn-success' : 'btn-outline'">
|
||||
@@ -274,26 +274,25 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, inject } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { Eye, EyeOff, BadgeCheck, Send, CircleX, CircleCheckBig, TrashIcon, Bookmark, Infinity, Github, Info } from 'lucide-vue-next'; // Ensure lucide-vue-next is installed
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { Eye, EyeOff, BadgeCheck, Send, CircleX, CircleCheckBig, TrashIcon, Bookmark, Infinity, Info } from '@lucide/vue';
|
||||
import BreadcrumbHeader from '@/components/dashboard/BreadcrumbHeader.vue';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
import { useWebAuthStore } from '../../stores/webauth';
|
||||
import { formatDateTime} from '@/utils/format-date';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useWebAuthStore } from '@/stores/webauth';
|
||||
import { formatDateTime } from '@/utils/format-date';
|
||||
import { useToast } from '@/composables/toast';
|
||||
import type { UserInfo, PasskeyInfo } from '@/types';
|
||||
|
||||
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
const webAuthStore = useWebAuthStore();
|
||||
const { setToast } = inject('toast');
|
||||
const { setToast } = useToast();
|
||||
|
||||
const loading = computed(() => authStore.loading);
|
||||
const user = computed(() => authStore.user);
|
||||
|
||||
const basicinfo_error = ref(null);
|
||||
const password_error = ref(null);
|
||||
const basicinfo_error = ref<string | null>(null);
|
||||
const password_error = ref<string | null>(null);
|
||||
|
||||
const basicinfo = ref({
|
||||
name: user.value?.name || '',
|
||||
@@ -345,9 +344,9 @@ const updateBasicInfo = async () => {
|
||||
|
||||
await authStore.refreshProfile(); // Refresh user data
|
||||
basicinfo_error.value = null; // Clear error
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.log('Error updating basic info:', err);
|
||||
basicinfo_error.value = err || '更新失败';
|
||||
basicinfo_error.value = err?.message || String(err) || '更新失败';
|
||||
setToast('Failed to update basic information', 'error');
|
||||
}
|
||||
};
|
||||
@@ -372,18 +371,18 @@ const updatePassword = async () => {
|
||||
passwordData.value.newPassword = '';
|
||||
passwordData.value.confirmPassword = '';
|
||||
password_error.value = null; // Clear error
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error('Error updating password:', err);
|
||||
password_error.value = err || '更新失败';
|
||||
password_error.value = err?.message || String(err) || '更新失败';
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化角色
|
||||
const formatRole = (role) => {
|
||||
const formatRole = (role?: number): string => {
|
||||
switch (true) {
|
||||
case role > 10:
|
||||
case (role ?? 0) > 10:
|
||||
return 'Root';
|
||||
case role > 0:
|
||||
case (role ?? 0) > 0:
|
||||
return 'Admin';
|
||||
default:
|
||||
return 'User';
|
||||
@@ -422,38 +421,38 @@ const toggleTelegramConnection = () => {
|
||||
const newpasskey = async () => {
|
||||
try {
|
||||
let res = await webAuthStore.addPasskey();
|
||||
if (res.data?.code == 200) {
|
||||
if (res?.data?.code == 200) {
|
||||
await getPasskeys();
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.log('err', err);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const passkeys = computed(() => webAuthStore.passkeys);
|
||||
const passkeys = computed<PasskeyInfo[] | null>(() => webAuthStore.passkeys);
|
||||
|
||||
const getPasskeys = async () => {
|
||||
try {
|
||||
await webAuthStore.getPasskeys();
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.log('err', err);
|
||||
}
|
||||
}
|
||||
|
||||
const confirmRmovePasskey = async (passkey) => {
|
||||
const confirmRmovePasskey = async (passkey: PasskeyInfo) => {
|
||||
if(confirm(`确认删除 ${passkey.name}?`)) {
|
||||
await removePasskey(passkey.id)
|
||||
}
|
||||
}
|
||||
const removePasskey = async (id) => {
|
||||
const removePasskey = async (id: PasskeyInfo['id']) => {
|
||||
try {
|
||||
const res = await webAuthStore.deletePasskey(id);
|
||||
if (res.data?.code == 200) {
|
||||
if (res?.data?.code == 200) {
|
||||
setToast('Passkey removed successfully', 'success');
|
||||
}
|
||||
await getPasskeys();
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.log('err', err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,28 +189,29 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, inject } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { Eye, EyeOff, BadgeCheck, Send, CircleX, CircleCheckBig, TrashIcon, Infinity } from 'lucide-vue-next'; // Ensure lucide-vue-next is installed
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { Eye, EyeOff, BadgeCheck, Send, CircleX, CircleCheckBig } from '@lucide/vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import BreadcrumbHeader from '@/components/dashboard/BreadcrumbHeader.vue';
|
||||
import { useToast } from '@/composables/toast';
|
||||
import type { UserInfo } from '@/types';
|
||||
|
||||
const route = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
const { setToast } = inject('toast');
|
||||
const { setToast } = useToast();
|
||||
|
||||
const loading = computed(() => authStore.loading);
|
||||
const loading = computed(() => authStore.loading);
|
||||
const user = computed(() => authStore.user);
|
||||
|
||||
onMounted(async () => {
|
||||
await authStore.refreshProfile()
|
||||
});
|
||||
|
||||
// 原实现误引用了未定义的 userStore/userId,这里改为更新当前登录用户资料
|
||||
const updateUser = async () => {
|
||||
if (!user.value) return;
|
||||
try {
|
||||
const payload = {
|
||||
const payload: Partial<UserInfo> = {
|
||||
name: user.value.name,
|
||||
username: user.value.username,
|
||||
email: user.value.email,
|
||||
@@ -219,13 +220,13 @@ const updateUser = async () => {
|
||||
if (user.value.password) {
|
||||
payload.password = user.value.password;
|
||||
}
|
||||
const res = await userStore.editUser(userId.value, payload);
|
||||
const res = await authStore.updateProfile(payload);
|
||||
console.log('updateUser', res)
|
||||
if (res.data?.code == 200) {
|
||||
setToast(`User ${userId.value} updated`, 'success');
|
||||
setToast(`User ${user.value.username} updated`, 'success');
|
||||
}
|
||||
await userStore.refreshUser(userId.value);
|
||||
} catch (err) {
|
||||
await authStore.refreshProfile();
|
||||
} catch (err: any) {
|
||||
console.error('Error updating user:', err.response?.data?.data?.error);
|
||||
}
|
||||
};
|
||||
@@ -239,11 +240,11 @@ const togglePasswordVisibility = () => {
|
||||
};
|
||||
|
||||
// 格式化角色
|
||||
const formatRole = (role) => {
|
||||
const formatRole = (role?: number): string => {
|
||||
switch (true) {
|
||||
case role > 10:
|
||||
case (role ?? 0) > 10:
|
||||
return 'Root';
|
||||
case role > 0:
|
||||
case (role ?? 0) > 0:
|
||||
return 'Admin';
|
||||
default:
|
||||
return 'U';
|
||||
|
||||
@@ -133,27 +133,27 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, inject, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import BreadcrumbHeader from '@/components/dashboard/BreadcrumbHeader.vue';
|
||||
import { Eye, EyeOff } from 'lucide-vue-next'
|
||||
import { dateToUnix } from '@/utils/format-date.js'
|
||||
import { Eye, EyeOff } from '@lucide/vue'
|
||||
import { dateToUnix } from '@/utils/format-date';
|
||||
import { useToast } from '@/composables/toast';
|
||||
import type { TokenPayload } from '@/types';
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const { setToast } = inject('toast')
|
||||
const error = ref(null)
|
||||
const { setToast } = useToast()
|
||||
const error = ref<string | null>(null)
|
||||
const user = computed(() => authStore.user);
|
||||
|
||||
const showAdvancedOptions = ref(false)
|
||||
|
||||
|
||||
const newToken = ref({
|
||||
const newToken = ref<TokenPayload>({
|
||||
name: '',
|
||||
key: '',
|
||||
user_id: user.user_id,
|
||||
user_id: user.value?.user_id as number | undefined,
|
||||
active: true,
|
||||
quota: 0,
|
||||
unlimited_quota: true,
|
||||
@@ -216,7 +216,7 @@ const createToken = async () => {
|
||||
console.log(res)
|
||||
error.value = res.data?.error || 'Failed to create token'
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || 'Failed to create token'
|
||||
|
||||
}
|
||||
@@ -227,10 +227,6 @@ const cancel = () => {
|
||||
emit('closeModal', false)
|
||||
}
|
||||
|
||||
const deleteToken = async (id) => {
|
||||
console.log(id)
|
||||
}
|
||||
|
||||
// 显示密码
|
||||
const isTokenVisible = ref(false);
|
||||
|
||||
@@ -238,7 +234,9 @@ function toggleTokenVisibility() {
|
||||
isTokenVisible.value = !isTokenVisible.value;
|
||||
}
|
||||
|
||||
const emit = defineEmits(['closeModal'])
|
||||
const emit = defineEmits<{
|
||||
(e: 'closeModal', value: boolean): void
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
@change="updateStatus(token)" />
|
||||
</td>
|
||||
<!-- <td class="font-mono text-xs px-2 py-3">{{ token.key }}</td> -->
|
||||
<td class="px-2 py-3">{{ token.expired_at == 0 ? 'Never' : unixToDate(token.expired_at) }}</td>
|
||||
<td class="px-2 py-3">{{ token.expired_at == 0 ? 'Never' : unixToDate(token.expired_at ?? 0) }}</td>
|
||||
<td class="px-2 py-3">
|
||||
<template v-if="token.unlimited_quota">
|
||||
<Infinity />
|
||||
@@ -103,54 +103,54 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, inject, computed, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, computed } from 'vue';
|
||||
import BreadcrumbHeader from '@/components/dashboard/BreadcrumbHeader.vue';
|
||||
import QRCodeCard from '@/components/QRCodeCard.vue';
|
||||
import QRCodeCard from '@/components/common/QRCodeCard.vue';
|
||||
import TokenNew from '@/views/dashboard/TokenNew.vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import {
|
||||
EyeIcon, PlusIcon, TrashIcon, Infinity, Eraser
|
||||
} from 'lucide-vue-next';
|
||||
} from '@lucide/vue';
|
||||
import { unixToDate } from '@/utils/format-date';
|
||||
import { useToast } from '@/composables/toast';
|
||||
import type { TokenInfo } from '@/types';
|
||||
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
const user = computed(() => authStore.user);
|
||||
const { setToast } = inject('toast');
|
||||
const { setToast } = useToast();
|
||||
|
||||
onMounted(async () => {
|
||||
await authStore.refreshProfile();
|
||||
})
|
||||
|
||||
watch(() => authStore.user, async (newUser) => {
|
||||
if (newUser.expired_at > 0) {
|
||||
watch(() => authStore.user, (newUser) => {
|
||||
if (newUser && newUser.expired_at && newUser.expired_at > 0) {
|
||||
newUser.format_expired_at = unixToDate(newUser.expired_at);
|
||||
}
|
||||
})
|
||||
|
||||
const updateStatus = async (token) => {
|
||||
const updateStatus = async (token: TokenInfo) => {
|
||||
console.log(token);
|
||||
try {
|
||||
const res = await authStore.updateToken({ userid: token.userid, id: token.id, name: token.name, active: token.active });
|
||||
if (res.data?.code == 200) {
|
||||
setToast(`Token ${token.name} updated`, 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
token.active = !token.active
|
||||
console.log(error.response.data.error);
|
||||
setToast(error.response.data.error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
const confirmRevokeToken = async (token) => {
|
||||
const confirmRevokeToken = async (token: TokenInfo) => {
|
||||
if (confirm(`确认删除 ${token.name}?`)) {
|
||||
await revokeToken(token);
|
||||
}
|
||||
}
|
||||
|
||||
const revokeToken = async (token) => {
|
||||
const revokeToken = async (token: TokenInfo) => {
|
||||
try {
|
||||
const res = await authStore.deleteToken(token.id);
|
||||
if (res.data?.code == 200) {
|
||||
@@ -158,12 +158,12 @@ const revokeToken = async (token) => {
|
||||
}
|
||||
await authStore.refreshProfile();
|
||||
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
setToast(error.response.data.error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
const cleanUsedToken = async (token) => {
|
||||
const cleanUsedToken = async (token: TokenInfo) => {
|
||||
|
||||
if (token.used_quota == 0 || token.used_quota == null) {
|
||||
return;
|
||||
@@ -175,19 +175,19 @@ const cleanUsedToken = async (token) => {
|
||||
setToast(`Token ${token.name} used quota reset`, 'success');
|
||||
}
|
||||
await authStore.refreshProfile();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
setToast(error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
const showTokenModel = ref(false);
|
||||
const tokenRef = ref(null);
|
||||
const viewToken = (token) => {
|
||||
const tokenRef = ref<HTMLDialogElement | null>(null);
|
||||
const viewToken = (token: TokenInfo) => {
|
||||
const dialog = tokenRef.value;
|
||||
if (dialog) {
|
||||
if (!dialog.hasAttribute('open')) {
|
||||
qrCodeValue.value = token.key;
|
||||
qrCodeValue.value = token.key || '';
|
||||
dialog.showModal();
|
||||
} else {
|
||||
if (dialog.hasAttribute('open')) {
|
||||
@@ -202,7 +202,7 @@ const qrCodeValue = ref('');
|
||||
|
||||
|
||||
// 关闭模态框
|
||||
const modalRef = ref(null);
|
||||
const modalRef = ref<HTMLDialogElement | null>(null);
|
||||
const closeModal = async () => {
|
||||
if (modalRef.value) {
|
||||
modalRef.value.close();
|
||||
|
||||
@@ -144,27 +144,29 @@
|
||||
|
||||
<!-- Pagination -->
|
||||
<Pagination :currentPage="currentPage" :totalItems="totalItems" :pageSize="pageSize"
|
||||
:pageSizeOptions="[10, 20, 50, 100]" @changePage="changePage" @changePageSize="changePageSize" />
|
||||
:pageSizeOptions="[10, 20, 50, 100]" @changePage="changePage" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, inject, computed } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import BreadcrumbHeader from '@/components/dashboard/BreadcrumbHeader.vue';
|
||||
import Pagination from '@/components/Pagination.vue';
|
||||
import Pagination from '@/components/common/Pagination.vue';
|
||||
import UserNew from '@/views/dashboard/UserNew.vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useToast } from '@/composables/toast';
|
||||
import type { UserInfo } from '@/types';
|
||||
import {
|
||||
BadgeXIcon, BadgeCheckIcon, EyeIcon, PlusIcon, Settings2Icon,
|
||||
TrashIcon, Infinity
|
||||
} from 'lucide-vue-next';
|
||||
} from '@lucide/vue';
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const users = computed(() => userStore.users);
|
||||
const { setToast } = inject('toast');
|
||||
const { setToast } = useToast();
|
||||
|
||||
// 用户数据
|
||||
const currentPage = ref(1);
|
||||
@@ -172,10 +174,10 @@ const pageSize = ref(10);
|
||||
const totalItems = computed(() => userStore.totalUsers);
|
||||
|
||||
// 封装公共的用户列表获取方法
|
||||
const listUsers = async (size = pageSize.value, page = currentPage.value, active = selectedStatuses.map(status => status.value)) => {
|
||||
const listUsers = async (size?: number, page?: number, active?: boolean[] | boolean) => {
|
||||
currentPage.value = page || currentPage.value;
|
||||
// console.log('pagesize', pageSize.value, 'page', currentPage.value, 'active', selectedStatuses.map(status => status.value));
|
||||
await userStore.listUser(size, page, active);
|
||||
await userStore.listUser(size ?? pageSize.value, page ?? currentPage.value, active ?? selectedStatuses.map(status => status.value));
|
||||
};
|
||||
|
||||
// 组件挂载时加载用户数据
|
||||
@@ -184,7 +186,7 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
// 分页与页面大小变化
|
||||
const changePage = async (page, size) => {
|
||||
const changePage = async (page: number, size: number) => {
|
||||
if (page == currentPage.value && size == pageSize.value) {
|
||||
return
|
||||
}
|
||||
@@ -193,11 +195,9 @@ const changePage = async (page, size) => {
|
||||
await listUsers();
|
||||
};
|
||||
|
||||
const changePageSize = changePage;
|
||||
|
||||
// 复选框选择状态
|
||||
const selectAll = ref(false)
|
||||
const selectedUsers = ref([])
|
||||
const selectedUsers = ref<UserInfo[]>([])
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
users.value.forEach(key => key.selected = selectAll.value)
|
||||
@@ -212,7 +212,7 @@ const toggleSelectAll = () => {
|
||||
|
||||
}
|
||||
|
||||
const toggleUserSelection = (user) => {
|
||||
const toggleUserSelection = (user: UserInfo) => {
|
||||
if (selectedUsers.value.includes(user)) {
|
||||
selectedUsers.value = selectedUsers.value.filter(selectedUser => selectedUser !== user);
|
||||
} else {
|
||||
@@ -223,9 +223,9 @@ const toggleUserSelection = (user) => {
|
||||
|
||||
// 状态筛选
|
||||
const statusOptions = ['Active', 'Inactive'];
|
||||
const selectedStatuses = reactive([]);
|
||||
const selectedStatuses = reactive<{ status: string; value: boolean }[]>([]);
|
||||
|
||||
const toggleStatusFilter = async (status) => {
|
||||
const toggleStatusFilter = async (status: string) => {
|
||||
const statusValue = status === 'Active';
|
||||
const index = selectedStatuses.findIndex(item => item.status === status);
|
||||
|
||||
@@ -239,12 +239,12 @@ const toggleStatusFilter = async (status) => {
|
||||
};
|
||||
|
||||
// 处理批量操作
|
||||
const handleBatchAction = async (action) => {
|
||||
const handleBatchAction = async (action: string) => {
|
||||
if (selectedUsers.value.length === 0) {
|
||||
return setToast('请选择用户', 'error');
|
||||
}
|
||||
if (!['enable', 'disable', 'delete'].includes(action)) {
|
||||
return setToast('无效的操作 ${action}', 'error');
|
||||
return setToast(`无效的操作 ${action}`, 'error');
|
||||
}
|
||||
if (selectedUsers.value.length === 0) {
|
||||
return setToast('请选择用户', 'error');
|
||||
@@ -255,20 +255,20 @@ const handleBatchAction = async (action) => {
|
||||
if (res.data?.code === 200) {
|
||||
setToast(`Users ${action} Success`, 'success');
|
||||
} else {
|
||||
setToast(res.error || `${action} Failed`, 'error');
|
||||
setToast(res.data?.error || `${action} Failed`, 'error');
|
||||
}
|
||||
selectedUsers.value = [];
|
||||
selectAll.value = false;
|
||||
await listUsers();
|
||||
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error(`批量操作 ${action} 失败:`, error);
|
||||
setToast('批量操作失败', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// 更新用户状态
|
||||
const updateStatus = async (user) => {
|
||||
const updateStatus = async (user: UserInfo) => {
|
||||
try {
|
||||
const action = user.active ? 'enable' : 'disable';
|
||||
const res = await userStore.userOption(action, [user.id]);
|
||||
@@ -276,45 +276,45 @@ const updateStatus = async (user) => {
|
||||
if (res.data?.code === 200) {
|
||||
setToast(`User ${user.name} has been ${action}`, 'success');
|
||||
} else {
|
||||
setToast(res.error || `用户 ${user.id} ${action} 失败`, 'error');
|
||||
setToast(res.data?.error || `用户 ${user.id} ${action} 失败`, 'error');
|
||||
}
|
||||
|
||||
await listUsers();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('状态更新失败:', error);
|
||||
setToast('状态更新失败', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const viewUser = (user) => {
|
||||
const viewUser = (user: UserInfo) => {
|
||||
router.push({ name: 'UserView', query: { id: user.id } });
|
||||
}
|
||||
|
||||
const confirmDeleteUser = (user) => {
|
||||
const confirmDeleteUser = (user: UserInfo) => {
|
||||
if (confirm(`确认删除 ${user.username}?`)) {
|
||||
deleteUser(user);
|
||||
}
|
||||
};
|
||||
// 删除用户
|
||||
const deleteUser = async (user) => {
|
||||
const deleteUser = async (user: UserInfo) => {
|
||||
try {
|
||||
const res = await userStore.userOption('delete', [user.id]);
|
||||
|
||||
if (res.data?.code === 200) {
|
||||
setToast('用户删除成功', 'success');
|
||||
} else {
|
||||
setToast(res.error || '删除失败', 'error');
|
||||
setToast(res.data?.error || '删除失败', 'error');
|
||||
}
|
||||
|
||||
await listUsers();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('删除失败:', error);
|
||||
setToast('删除失败', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭模态框
|
||||
const modalRef = ref(null);
|
||||
const modalRef = ref<HTMLDialogElement | null>(null);
|
||||
const closeModal = async () => {
|
||||
if (modalRef.value) {
|
||||
modalRef.value.close();
|
||||
|
||||
@@ -151,23 +151,23 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, inject } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import BreadcrumbHeader from '@/components/dashboard/BreadcrumbHeader.vue';
|
||||
import { Eye, EyeOff } from 'lucide-vue-next'
|
||||
import { Eye, EyeOff } from '@lucide/vue'
|
||||
import { useToast } from '@/composables/toast';
|
||||
import type { NewUserPayload } from '@/types';
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const { setToast } = inject('toast')
|
||||
const error = ref(null)
|
||||
const { setToast } = useToast()
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// Control advanced options visibility
|
||||
const showAdvancedOptions = ref(false)
|
||||
|
||||
// Initialize user object
|
||||
const newUser = ref({
|
||||
const newUser = ref<NewUserPayload>({
|
||||
name: '',
|
||||
username: '',
|
||||
email: '',
|
||||
@@ -188,7 +188,7 @@ const resetNewUser = () => {
|
||||
role: 0, // Default to Regular User
|
||||
active: true, // Default to Active
|
||||
quota: 0, // Default quota value (relevant if not unlimited)
|
||||
unlimitedQuota: true, // Default to unlimited
|
||||
unlimited_quota: true, // Default to unlimited
|
||||
language: 'en', // Default language
|
||||
}
|
||||
}
|
||||
@@ -226,9 +226,9 @@ const createUser = async () => {
|
||||
// Optionally navigate or reset form
|
||||
emit('closeModal', true)
|
||||
} else {
|
||||
setToast(res.error || res.data?.message || 'Failed to create user', 'error')
|
||||
setToast(res.data?.error || res.data?.message || 'Failed to create user', 'error')
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.error || 'Failed to create user'
|
||||
// setToast(error.response?.data?.error || 'Failed to create user', 'error')
|
||||
}
|
||||
@@ -241,7 +241,9 @@ function togglePasswordVisibility() {
|
||||
isPasswordVisible.value = !isPasswordVisible.value;
|
||||
}
|
||||
|
||||
const emit = defineEmits(['closeModal'])
|
||||
const emit = defineEmits<{
|
||||
(e: 'closeModal', value: boolean): void
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -214,22 +214,24 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, inject } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { Eye, EyeOff, BadgeCheck, Send, CircleX, CircleCheckBig, TrashIcon, Infinity } from 'lucide-vue-next'; // Ensure lucide-vue-next is installed
|
||||
import { useUserStore } from '../../stores/user';
|
||||
import { Eye, EyeOff, BadgeCheck, Send, CircleX, CircleCheckBig, TrashIcon, Infinity } from '@lucide/vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import BreadcrumbHeader from '@/components/dashboard/BreadcrumbHeader.vue';
|
||||
import { useToast } from '@/composables/toast';
|
||||
import type { UserInfo, TokenInfo } from '@/types';
|
||||
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
const { setToast } = inject('toast');
|
||||
const { setToast } = useToast();
|
||||
|
||||
const userId = computed(() => route.query.id);
|
||||
|
||||
onMounted(async () => {
|
||||
if (userId.value) {
|
||||
await userStore.getUser(userId.value);
|
||||
await userStore.getUser(userId.value as string);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -237,7 +239,7 @@ const user = computed(() => userStore.user);
|
||||
const loading = computed(() => userStore.loading); // Access loading state
|
||||
|
||||
// 更新状态
|
||||
const updateStatus = async (user) => {
|
||||
const updateStatus = async (user: UserInfo) => {
|
||||
try {
|
||||
const action = user.active ? 'enable' : 'disable';
|
||||
const res = await userStore.userOption(action, [user.id]);
|
||||
@@ -247,7 +249,7 @@ const updateStatus = async (user) => {
|
||||
setToast(res.data?.error || `用户 ${user.id} ${action} 失败`, 'error');
|
||||
}
|
||||
await userStore.refreshUser(user.id);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
user.active = !user.active;
|
||||
console.error('状态更新失败:', error);
|
||||
// setToast(error.response.data?.error || '状态更新失败', 'error');
|
||||
@@ -257,7 +259,7 @@ const updateStatus = async (user) => {
|
||||
const updateUser = async () => {
|
||||
if (!user.value) return;
|
||||
try {
|
||||
const payload = {
|
||||
const payload: Partial<UserInfo> = {
|
||||
name: user.value.name,
|
||||
username: user.value.username,
|
||||
email: user.value.email,
|
||||
@@ -270,13 +272,13 @@ const updateUser = async () => {
|
||||
if (user.value.password) {
|
||||
payload.password = user.value.password;
|
||||
}
|
||||
const res = await userStore.editUser(userId.value, payload);
|
||||
const res = await userStore.editUser(userId.value as string, payload);
|
||||
console.log('updateUser', res)
|
||||
if (res.data?.code == 200) {
|
||||
setToast(`User ${userId.value} updated`, 'success');
|
||||
}
|
||||
await userStore.refreshUser(userId.value);
|
||||
} catch (err) {
|
||||
await userStore.refreshUser(userId.value as string);
|
||||
} catch (err: any) {
|
||||
console.error('Error updating user:', err.response?.data?.data?.error);
|
||||
}
|
||||
};
|
||||
@@ -290,11 +292,11 @@ const togglePasswordVisibility = () => {
|
||||
};
|
||||
|
||||
// 格式化角色
|
||||
const formatRole = (role) => {
|
||||
const formatRole = (role?: number): string => {
|
||||
switch (true) {
|
||||
case role > 10:
|
||||
case (role ?? 0) > 10:
|
||||
return 'Root';
|
||||
case role > 0:
|
||||
case (role ?? 0) > 0:
|
||||
return 'Admin';
|
||||
default:
|
||||
return 'U';
|
||||
@@ -316,7 +318,7 @@ const formatRole = (role) => {
|
||||
// }
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
const formatDate = (dateString?: number): string | null => {
|
||||
if (!dateString) return null;
|
||||
try {
|
||||
return new Intl.DateTimeFormat('sv-SE', { dateStyle: 'short', timeStyle: 'short' }).format(new Date(dateString * 1000)); // Multiply by 1000 for JavaScript Date
|
||||
@@ -327,7 +329,7 @@ const formatDate = (dateString) => {
|
||||
};
|
||||
|
||||
// 删除token
|
||||
const revokeToken = (tokenId) => {
|
||||
const revokeToken = (tokenId: TokenInfo['id']) => {
|
||||
console.log('Revoking token:', tokenId);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<header class="fixed w-full top-0 z-50 backdrop-blur-md bg-base-100/50">
|
||||
<div class="container mx-auto flex justify-between items-center p-4">
|
||||
<div class="flex items-center h-12 w-12 rounded-full text-l">
|
||||
<img src="../assets/logo.svg" alt="Logo" class="select-none">
|
||||
<img src="@/assets/logo.svg" alt="Logo" class="select-none">
|
||||
<span class="hidden sm:flex text-xl font-bold">
|
||||
<a href="/" class="text-base-content hover:no-underline">OpenTeam</a>
|
||||
</span>
|
||||
@@ -16,7 +16,7 @@
|
||||
<main class="flex-grow flex flex-col justify-center items-center pt-16">
|
||||
<div class="text-center">
|
||||
<div class="flex items-center justify-center my-8 outline-none select-none">
|
||||
<!-- <img src="../assets/404.svg" alt="404 Not Found" class="h-48"> -->
|
||||
<!-- <img src="@/assets/404.svg" alt="404 Not Found" class="h-48"> -->
|
||||
</div>
|
||||
<h1 class="text-5xl font-bold mb-4 text-rose-300">
|
||||
404
|
||||
@@ -50,7 +50,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
const currentYear = ref('');
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
/** 后端 API 基础路径,默认 /api */
|
||||
readonly VITE_API_BASE_URL?: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
Reference in New Issue
Block a user