init:搭建基座框架

This commit is contained in:
liangdong
2025-12-29 19:20:21 +08:00
parent a360d36605
commit 6a2c315a87
35 changed files with 6313 additions and 2 deletions

21
src/App.vue Normal file
View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useLang } from "@/utils/lang";
import { getUserList } from '@/api';
const { t } = useI18n();
const { changeLang } = useLang();
onMounted(() => {
getUserList().then(res => {
console.log(res);
})
})
</script>
<template>
<div>Hello Vue {{ t("message.title") }}</div>
</template>
<style scoped lang="scss"></style>

6
src/api/index.ts Normal file
View File

@@ -0,0 +1,6 @@
import request from '@/request';
// 设置请求的参数信息
export const getUserList = (params?: any) => {
return request.get('/api/user/list', params);
};

1
src/assets/vue.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

90
src/auto-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,90 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue').EffectScope
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
const computed: typeof import('vue').computed
const createApp: typeof import('vue').createApp
const createPinia: typeof import('pinia').createPinia
const customRef: typeof import('vue').customRef
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
const defineComponent: typeof import('vue').defineComponent
const defineStore: typeof import('pinia').defineStore
const effectScope: typeof import('vue').effectScope
const getActivePinia: typeof import('pinia').getActivePinia
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
const h: typeof import('vue').h
const inject: typeof import('vue').inject
const isProxy: typeof import('vue').isProxy
const isReactive: typeof import('vue').isReactive
const isReadonly: typeof import('vue').isReadonly
const isRef: typeof import('vue').isRef
const isShallow: typeof import('vue').isShallow
const mapActions: typeof import('pinia').mapActions
const mapGetters: typeof import('pinia').mapGetters
const mapState: typeof import('pinia').mapState
const mapStores: typeof import('pinia').mapStores
const mapWritableState: typeof import('pinia').mapWritableState
const markRaw: typeof import('vue').markRaw
const nextTick: typeof import('vue').nextTick
const onActivated: typeof import('vue').onActivated
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
const onDeactivated: typeof import('vue').onDeactivated
const onErrorCaptured: typeof import('vue').onErrorCaptured
const onMounted: typeof import('vue').onMounted
const onRenderTracked: typeof import('vue').onRenderTracked
const onRenderTriggered: typeof import('vue').onRenderTriggered
const onScopeDispose: typeof import('vue').onScopeDispose
const onServerPrefetch: typeof import('vue').onServerPrefetch
const onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const provide: typeof import('vue').provide
const reactive: typeof import('vue').reactive
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const resolveComponent: typeof import('vue').resolveComponent
const setActivePinia: typeof import('pinia').setActivePinia
const setMapStoreSuffix: typeof import('pinia').setMapStoreSuffix
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const storeToRefs: typeof import('pinia').storeToRefs
const toRaw: typeof import('vue').toRaw
const toRef: typeof import('vue').toRef
const toRefs: typeof import('vue').toRefs
const toValue: typeof import('vue').toValue
const triggerRef: typeof import('vue').triggerRef
const unref: typeof import('vue').unref
const useAttrs: typeof import('vue').useAttrs
const useCssModule: typeof import('vue').useCssModule
const useCssVars: typeof import('vue').useCssVars
const useId: typeof import('vue').useId
const useLink: typeof import('vue-router').useLink
const useModel: typeof import('vue').useModel
const useRoute: typeof import('vue-router').useRoute
const useRouter: typeof import('vue-router').useRouter
const useSlots: typeof import('vue').useSlots
const useTemplateRef: typeof import('vue').useTemplateRef
const watch: typeof import('vue').watch
const watchEffect: typeof import('vue').watchEffect
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

View File

@@ -0,0 +1,314 @@
<template>
<div class="base-form">
<el-form
ref="formRef"
:model="localFormData"
:rules="formRules"
label-position="top"
:inline="inline"
:disabled="disabled"
@submit.prevent
>
<el-row :gutter="20">
<template v-for="field in formFields" :key="field.prop">
<el-col v-if="!field.hidden" :span="field.span || 24">
<el-form-item
:prop="field.prop"
:label="field.label"
:required="field.required"
>
<!-- 根据字段类型渲染不同的表单组件 -->
<component
:is="getFormFieldComponent(field)"
v-model="localFormData[field.prop]"
v-bind="getFieldProps(field)"
@blur="onFieldBlur(field)"
@keyup.enter="onFieldEnter(field)"
/>
</el-form-item>
</el-col>
</template>
</el-row>
<!-- 表单操作按钮 -->
<div v-if="showActions" class="form-actions">
<slot name="actions">
<el-button
v-if="showSubmit"
type="primary"
@click="onSubmit"
:loading="submitting"
>
{{ submitText }}
</el-button>
<el-button
v-if="showReset"
@click="onReset"
>
{{ resetText }}
</el-button>
<el-button
v-if="showCancel"
@click="onCancel"
>
{{ cancelText }}
</el-button>
</slot>
</div>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, nextTick } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
// 定义 props
interface FormField {
prop: string
label: string
type: 'input' | 'select' | 'date' | 'datetime' | 'number' | 'textarea' | 'switch' | 'checkbox' | 'radio' | 'password' | 'time'
span?: number
required?: boolean
options?: { label: string; value: any }[]
placeholder?: string
disabled?: boolean
hidden?: boolean
rules?: any[] // 字段特定验证规则
editable?: boolean // 是否支持双击编辑
[key: string]: any
}
interface Props {
// 表单配置
formFields: FormField[]
formData?: Record<string, any>
formRules?: FormRules
inline?: boolean
disabled?: boolean
showActions?: boolean
showSubmit?: boolean
showReset?: boolean
showCancel?: boolean
submitText?: string
resetText?: string
cancelText?: string
// 编辑模式配置
editMode?: 'double-click' | 'always' | 'never' // 编辑模式
autoSave?: boolean // 是否自动保存
// 加载状态
submitting?: boolean
}
const props = withDefaults(defineProps<Props>(), {
formData: () => ({}),
formRules: () => ({}),
inline: false,
disabled: false,
showActions: true,
showSubmit: true,
showReset: true,
showCancel: false,
submitText: '提交',
resetText: '重置',
cancelText: '取消',
editMode: 'double-click',
autoSave: false,
submitting: false,
})
// 定义 emits
const emit = defineEmits<{
submit: [data: Record<string, any>]
reset: []
cancel: []
fieldChange: [field: FormField, value: any]
fieldBlur: [field: FormField, value: any]
fieldEnter: [field: FormField, value: any]
update: [data: Record<string, any>]
}>()
// 响应式数据
const formRef = ref<FormInstance>()
const localFormData = ref<Record<string, any>>({})
const originalData = ref<Record<string, any>>({})
const isEditing = ref<Record<string, boolean>>({})
// 计算属性
const computedFormRules = computed(() => {
const rules: FormRules = { ...props.formRules }
// 合并字段特定规则
props.formFields.forEach(field => {
if (field.rules && field.prop) {
rules[field.prop] = field.rules
}
})
return rules
})
// 监听表单数据变化
watch(
() => props.formData,
(newVal) => {
localFormData.value = { ...newVal }
originalData.value = { ...newVal }
},
{ deep: true, immediate: true }
)
// 方法
const getFormFieldComponent = (field: FormField) => {
const componentMap: Record<string, string> = {
input: 'el-input',
select: 'el-select',
date: 'el-date-picker',
datetime: 'el-date-picker',
time: 'el-time-picker',
number: 'el-input-number',
textarea: 'el-input',
switch: 'el-switch',
checkbox: 'el-checkbox-group',
radio: 'el-radio-group',
password: 'el-input'
}
return componentMap[field.type] || 'el-input'
}
const getFieldProps = (field: FormField) => {
const props: Record<string, any> = {
placeholder: field.placeholder || `请输入${field.label}`,
...field
}
if (field.type === 'select') {
props.options = field.options
} else if (field.type === 'textarea') {
props.type = 'textarea'
} else if (field.type === 'password') {
props.type = 'password'
} else if (field.type === 'number') {
props.min = field.min
props.max = field.max
}
// 根据编辑模式设置双击事件
if (props.editable !== false && props.editMode === 'double-click') {
props.onDblclick = () => onFieldDoubleClick(field)
}
return props
}
const onFieldChange = (field: FormField, value: any) => {
if (localFormData.value) {
localFormData.value[field.prop] = value
}
emit('fieldChange', field, value)
emit('update', { ...localFormData.value })
}
const onFieldBlur = (field: FormField) => {
const value = localFormData.value[field.prop]
emit('fieldBlur', field, value)
// 如果启用自动保存,失去焦点时自动保存
if (props.autoSave) {
saveField(field)
}
}
const onFieldEnter = (field: FormField) => {
const value = localFormData.value[field.prop]
emit('fieldEnter', field, value)
// 回车时也自动保存
if (props.autoSave) {
saveField(field)
}
}
const onFieldDoubleClick = (field: FormField) => {
if (props.editMode === 'double-click' && !field.disabled) {
// 双击编辑字段
// 这里可以添加特殊逻辑,比如切换到编辑状态
console.log(`双击编辑字段: ${field.label}`)
}
}
const saveField = async (field: FormField) => {
if (!formRef.value) return
try {
// 验证特定字段
await formRef.value.validateField(field.prop)
// 这里可以调用 API 保存字段
console.log(`保存字段 ${field.prop}:`, localFormData.value[field.prop])
} catch (error) {
console.error(`字段 ${field.prop} 验证失败:`, error)
}
}
const onSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
emit('submit', { ...localFormData.value })
} catch (error) {
console.error('表单验证失败:', error)
}
}
const onReset = () => {
if (formRef.value) {
formRef.value.resetFields()
}
// 恢复原始数据
localFormData.value = { ...originalData.value }
emit('reset')
}
const onCancel = () => {
// 恢复原始数据
localFormData.value = { ...originalData.value }
emit('cancel')
}
// 暴露方法给父组件
defineExpose({
formRef,
validate: async () => {
if (formRef.value) {
return await formRef.value.validate()
}
},
validateField: async (prop: string) => {
if (formRef.value) {
return await formRef.value.validateField(prop)
}
},
resetFields: () => {
if (formRef.value) {
formRef.value.resetFields()
}
},
clearValidate: () => {
if (formRef.value) {
formRef.value.clearValidate()
}
}
})
</script>
<style scoped lang="scss">
.base-form {
.form-actions {
margin-top: 20px;
text-align: right;
}
}
</style>

View File

@@ -0,0 +1,32 @@
import { useUserStore } from '@/store';
export default function usePermission() {
const userStore = useUserStore();
return {
accessRouter(route) {
return (
!route.meta?.requiresAuth ||
!route.meta?.roles ||
route.meta?.roles?.includes('*') ||
route.meta?.roles?.includes(userStore.role)
);
},
findFirstPermissionRoute(_routers: any, role = 'admin') {
const cloneRouters = [..._routers];
while (cloneRouters.length) {
const firstElement = cloneRouters.shift();
if (
firstElement?.meta?.roles?.find((el: string[]) => {
return el.includes('*') || el.includes(role);
})
)
return { name: firstElement.name };
if (firstElement?.children) {
cloneRouters.push(...firstElement.children);
}
}
return null;
},
// You can add any rules you want
};
}

View File

@@ -0,0 +1,90 @@
// useTokenRefresh.ts
import { ref } from "vue";
import axios from "axios";
interface TokenData {
accessToken: string; // token内容
refreshToken: string; // 刷新的token内容
expiresAt?: number; // 过期时间戳
}
export default function useTokenRefresh(baseUrl: string) {
let refreshPromise: Promise<TokenData> | null = null;
let lastRefreshError: any = null;
// 当前Token数据使用localStorage持久化 可根据实际情况改成localStorage存储
const tokenData = ref<TokenData>({
accessToken: localStorage.getItem("accessToken") || "",
refreshToken: localStorage.getItem("refreshToken") || "",
expiresAt: Number(localStorage.getItem("expiresAt")) || 0,
});
// 核心刷新方法
const refreshToken = async (): Promise<TokenData> => {
if (refreshPromise) return refreshPromise;
// 如果上次刷新失败,短时间内不重复刷新,直接抛错
if (lastRefreshError) {
return Promise.reject(lastRefreshError);
}
const plainClient = axios.create({
baseURL: baseUrl,
timeout: 5 * 1000,
headers: {
"Content-Type": "application/json;charset=utf-8",
},
});
refreshPromise = (async () => {
try {
const refreshToken = getRefreshToken();
if (!refreshToken) throw new Error("refreshToken is null");
const res = await plainClient.post("/auth/refresh", {
refreshToken,
});
const newTokenData: TokenData = {
accessToken: res.data.accessToken,
refreshToken: res.data.refreshToken,
expiresAt: Date.now() + res.data.expiresIn * 1000,
};
setTokens(newTokenData);
lastRefreshError = null; // 成功清除错误
return newTokenData;
} catch (error) {
// 刷新失败处理
lastRefreshError = error;
clearTokens();
throw error;
} finally {
refreshPromise = null;
}
})();
return refreshPromise;
};
// 保存token
const setTokens = (data: TokenData) => {
tokenData.value = data;
localStorage.setItem("accessToken", data.accessToken);
localStorage.setItem("refreshToken", data.refreshToken);
localStorage.setItem("expiresAt", data.expiresAt.toString());
};
// 清理Token
const clearTokens = () => {
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
localStorage.removeItem("expiresAt");
tokenData.value = { accessToken: "", refreshToken: "", expiresAt: 0 };
};
// 获取当前Token
const getAccessToken = () => tokenData.value.accessToken;
// 获取refreshToken
const getRefreshToken = () => tokenData.value.refreshToken;
return {
refreshToken,
getRefreshToken,
setTokens,
getAccessToken,
clearTokens,
};
}

7
src/locales/en.ts Normal file
View File

@@ -0,0 +1,7 @@
export default {
message: {
title: 'Hello World',
hello: 'Hello',
welcome: 'Welcome',
},
}

7
src/locales/zh.ts Normal file
View File

@@ -0,0 +1,7 @@
export default {
message: {
title: '你好世界',
hello: '你好',
welcome: '欢迎使用',
},
}

57
src/main.ts Normal file
View File

@@ -0,0 +1,57 @@
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { createI18n } from "vue-i18n";
import { createPinia } from "pinia";
import ElementPlus from "element-plus";
import zhCn from "element-plus/es/locale/lang/zh-cn";
import en from "element-plus/es/locale/lang/en";
import Directives from '@/utils/directives';
const pinia = createPinia();
const app = createApp(App);
// 导入全局的i18n文件
const loadLocalMessages = async (lang: string) => {
const messages: Record<string, any> = {};
const locales = import.meta.glob("./locales/*.ts");
for (const path in locales) {
const lang = path.match(/\.\/locales\/(.+)\.ts$/)?.[1];
if (lang) {
const module = await locales[path]();
messages[lang] = module.default;
}
}
return messages;
};
// 获取当前选中的语言选项
const getLocalLang = () => {
const localLang = localStorage.getItem("lang");
if (localLang) {
return localLang;
}
const lang = navigator.language;
if (lang === "zh-CN") {
return "zh";
}
return "en";
};
// 设置全局的i18语言显示
const i18n = createI18n({
locale: getLocalLang(),
messages: await loadLocalMessages(),
legacy: false,
});
const elementLocale = getLocalLang() === "zh" ? zhCn : en;
app
.use(i18n)
.use(router)
.use(pinia)
.use(Directives)
.use(ElementPlus, { locale: elementLocale, size: "medium" })
.mount("#app");

13
src/pages/Home/index.vue Normal file
View File

@@ -0,0 +1,13 @@
<template>
<div class="">
Home
</div>
</template>
<script setup lang="ts">
import {reactive,ref,onMounted} from "vue"
defineOptions({})
</script>
<style lang="scss" scoped>
</style>

12
src/pages/Login/index.vue Normal file
View File

@@ -0,0 +1,12 @@
<template>
<div class="">
{{ t("message.title") }}
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
defineOptions({ name: "Login" });
</script>
<style lang="scss" scoped></style>

258
src/request/index.ts Normal file
View File

@@ -0,0 +1,258 @@
import axios from "axios";
import type {AxiosInstance, AxiosRequestConfig, AxiosResponse} from 'axios';
import useTokenRefresh from "@/hooks/useTokenRefresh";
import 'element-plus/es/components/notification/style/css'
import { ElNotification } from 'element-plus'
const baseUrl = import.meta.env.VITE_APP_BASE_API;
// 定义响应数据类型
interface ApiResponse<T = any> {
code: number;
data: T;
msg: string;
[key: string]: any;
}
// 定义错误类型
interface ApiError {
code: number;
msg: string;
[key: string]: any;
}
// 封装axios请求
class HttpRequest {
private baseUrl: string;
private instanceMap: Map<string, AxiosInstance> = new Map();
constructor(baseUrl?: string) {
this.baseUrl = baseUrl || "";
}
// 获取基础配置
private getBaseConfig(): AxiosRequestConfig {
return {
baseURL: this.baseUrl || baseUrl,
timeout: 50 * 1000,
headers: {
"Content-Type": "application/json;charset=utf-8",
},
};
}
// 创建 axios 实例
private createAxiosInstance(config: AxiosRequestConfig): AxiosInstance {
return axios.create(config);
}
// 设置请求和响应拦截器
private setupInterceptors(instance: AxiosInstance, url: string) {
const { refreshToken, getAccessToken, clearTokens } = useTokenRefresh(this.baseUrl);
// 请求拦截器
instance.interceptors.request.use(
(config: AxiosRequestConfig) => {
// 添加 token
const token = getAccessToken();
if (token) {
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error: any) => {
console.error('Request error:', error);
return Promise.reject(error);
}
);
// 响应拦截器
instance.interceptors.response.use(
async (response: AxiosResponse<ApiResponse>) => {
const { data: responseData, status } = response;
const originalRequest = response.config as AxiosRequestConfig & { _retry?: boolean };
switch (responseData.code) {
case 0:
// 成功
return Promise.resolve(responseData);
case 302:
// 重定向
if (responseData.msg) {
window.location.href = responseData.msg;
}
return Promise.reject(responseData);
case 401:
// 未授权需要刷新token
if (
originalRequest &&
!originalRequest._retry &&
!/\/auth\/refresh$/i.test(originalRequest.url || "")
) {
originalRequest._retry = true;
try {
// 刷新 token
await refreshToken();
// 重试原请求
const newToken = getAccessToken();
if (newToken) {
originalRequest.headers = originalRequest.headers || {};
originalRequest.headers.Authorization = `Bearer ${newToken}`;
}
return instance(originalRequest);
} catch (refreshErr) {
// 刷新失败,清理 token 并跳转登录
clearTokens();
// 可以在这里触发跳转到登录页
// router.push('/login');
console.error('Token refresh failed:', refreshErr);
return Promise.reject(refreshErr);
}
}
return Promise.reject(responseData);
default:
// 其他业务错误
ElNotification({
title:'提示',
message: responseData.msg || '请求失败',
type: 'error'
})
return Promise.reject(responseData);
}
},
(error: any) => {
// 网络错误或其他错误
console.error('Response error:', error);
// 判断错误类型
if (error.response) {
// 服务器返回错误状态码
const { status, data } = error.response;
if (status === 401) {
// 未授权,清理 token
const { clearTokens } = useTokenRefresh(this.baseUrl);
clearTokens();
} else if (status >= 500) {
// 服务器错误
return Promise.reject({
code: status,
msg: '服务器内部错误,请稍后重试',
} as ApiError);
}
} else if (error.request) {
// 网络错误
return Promise.reject({
code: -1,
msg: '网络连接失败,请检查网络',
} as ApiError);
}
return Promise.reject({
code: -1,
msg: error.message || '请求失败',
} as ApiError);
}
);
}
// 获取实例的缓存键
private getInstanceKey(baseURL: string, timeout: number): string {
return `${baseURL || this.baseUrl}__${timeout}`;
}
// 获取 axios 实例
public getInstance(config: AxiosRequestConfig = {}): AxiosInstance {
const baseURL = config.baseURL || this.baseUrl || baseUrl;
const timeout = config.timeout || 50 * 1000;
const key = this.getInstanceKey(baseURL, timeout);
if (this.instanceMap.has(key)) {
return this.instanceMap.get(key)!;
}
const instanceConfig = {
...this.getBaseConfig(),
...config,
};
const instance = this.createAxiosInstance(instanceConfig);
this.setupInterceptors(instance, baseURL);
this.instanceMap.set(key, instance);
return instance;
}
// 通用请求方法
public request<T = any>(config: AxiosRequestConfig): Promise<ApiResponse<T>> {
const instance = this.getInstance(config);
return instance(config).catch((error) => {
// 统一错误处理
throw error;
}) as Promise<ApiResponse<T>>;
}
// GET 请求
public get<T = any>(url: string, params?: any, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
return this.request<T>({
url,
method: 'get',
params,
...config
});
}
// POST 请求
public post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
return this.request<T>({
url,
method: 'post',
data,
...config
});
}
// PUT 请求
public put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
return this.request<T>({
url,
method: 'put',
data,
...config
});
}
// DELETE 请求
public delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
return this.request<T>({
url,
method: 'delete',
...config
});
}
}
// 创建实例
const httpRequest = new HttpRequest(baseUrl);
// 导出方法
export const request = {
get: <T = any>(url: string, params?: any, config?: AxiosRequestConfig) =>
httpRequest.get<T>(url, params, config),
post: <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
httpRequest.post<T>(url, data, config),
put: <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
httpRequest.put<T>(url, data, config),
delete: <T = any>(url: string, config?: AxiosRequestConfig) =>
httpRequest.delete<T>(url, config),
};
export default request;

16
src/router/index.ts Normal file
View File

@@ -0,0 +1,16 @@
import { createWebHistory, createRouter } from 'vue-router'
import Login from '@/pages/Login/index.vue';
import HomeView from '@/pages/Home/index.vue';
const routes = [
{ path: '/', component: HomeView },
{ path: '/login', component: Login },
]
const router = createRouter({
history: createWebHistory(),
routes,
})
export default router

3
src/store/index.ts Normal file
View File

@@ -0,0 +1,3 @@
import useUserStore from './modules/user';
export { useUserStore };

12
src/store/modules/user.ts Normal file
View File

@@ -0,0 +1,12 @@
import { defineStore } from "pinia";
const useUserStore = defineStore("user", {
state: () => {
return {
name: "user",
};
},
});
export default useUserStore;

View File

@@ -0,0 +1 @@

36
src/utils/directives.ts Normal file
View File

@@ -0,0 +1,36 @@
import { useUserStore } from "@/store";
function permissionDirective(el: HTMLElement,binding:any) {
const appStore = useUserStore();
const userPermissions = appStore.role; // 假設從store中獲取用戶權限
let requiredPermissions = binding.value;
if (typeof requiredPermissions === "string") {
requiredPermissions = [requiredPermissions];
}
const hasPermission = requiredPermissions.some((permission) =>
userPermissions.includes(permission)
);
if (!hasPermission) {
el.parentNode?.removeChild(el);
}
}
const permission = {
mounted(el: HTMLElement, binding: any) {
permissionDirective(el, binding);
},
updated(el: HTMLElement, binding: any) {
permissionDirective(el, binding);
},
};
const directives = {
permission,
};
export default (app: any) => {
Object.keys(directives).forEach((key) => {
app.directive(key, directives[key]);
});
};

20
src/utils/lang.ts Normal file
View File

@@ -0,0 +1,20 @@
import { useI18n } from 'vue-i18n'
export const useLang = () => {
const { locale } = useI18n()
const changeLang = (lang: string) => {
locale.value = lang
localStorage.setItem('lang', lang)
}
const getCurrentLang = () => {
return localStorage.getItem('lang') || 'en'
}
return {
changeLang,
getCurrentLang,
currentLang: locale
}
}