init:搭建基座框架
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
||||||
202
Jenkinsfile
vendored
Normal file
202
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage("初始化环境变量") {
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def parts = env.GIT_URL.split("/")
|
||||||
|
def groupName = parts[parts.size()-2]
|
||||||
|
def projectName = parts[parts.size()-1][0..-5]
|
||||||
|
env['groupName'] = groupName
|
||||||
|
env['projectName'] = projectName
|
||||||
|
env['versionShell'] = sh(script: "sed -n 's/.*\"version\": \"\\([^\\\"]*\\)\".*/\\1/p' package.json", returnStdout: true).trim()
|
||||||
|
env['targetVersion'] =env.branch=="dev"?"dev":env.branch=="test"?"test":env.versionShell
|
||||||
|
env['targetVersion'] =env.branch=="dev"||env.branch==~/^f_.*_dev$/?"dev":env.branch=="test"||env.branch==~/^f_.*_test$/?"test":env.branch=="pre"||env.branch==~/^f_.*_pre$/?"pre":env.versionShell
|
||||||
|
env['branchConfig']=env.branch=="dev"||env.branch==~/^f_.*_dev$/?"dev":env.branch=="test"||env.branch==~/^f_.*_test$/?"test":env.branch=="pre"||env.branch==~/^f_.*_pre$/?"pre":env.branch
|
||||||
|
env['environment']=env.branch=="dev"||env.branch==~/^f_.*_dev$/?"dev":env.branch=="test"||env.branch==~/^f_.*_test$/?"test":"prod"
|
||||||
|
}
|
||||||
|
sh 'printenv'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('拉取部署代码') {
|
||||||
|
steps {
|
||||||
|
checkout([
|
||||||
|
$class : 'GitSCM',
|
||||||
|
branches : [[name: '*/main']],
|
||||||
|
extensions : [
|
||||||
|
[$class: 'RelativeTargetDirectory', relativeTargetDir: 'docker'],
|
||||||
|
],
|
||||||
|
userRemoteConfigs: [[credentialsId: 'jenkins', url: 'https://gitlab.fengchaoit.com/basic/buildscripts.git']]
|
||||||
|
])
|
||||||
|
sh "echo '拷贝Dockerfile文件'"
|
||||||
|
sh "cp ./docker/front/Dockerfile ."
|
||||||
|
script {
|
||||||
|
def projectPrefix = env.prefix
|
||||||
|
sh(script: "sed -i \"s@^export const VITE_PROJECT_PREFIX = '/'.*@export const VITE_PROJECT_PREFIX = '${projectPrefix}';@g\" config.js")
|
||||||
|
sh(script: "cat config.js")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('项目打包为镜像') {
|
||||||
|
steps {
|
||||||
|
sh "docker build --rm --build-arg ENV=$environment --build-arg PROJECTCODE=$projectName --build-arg PROJECTVERSION=$targetVersion -t $projectName:$targetVersion ."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('推送镜像到镜像仓库') {
|
||||||
|
steps {
|
||||||
|
sh "echo '改镜像标签'"
|
||||||
|
sh "docker tag $projectName:$targetVersion harbor.fengchaoit.com/$groupName/$projectName:$targetVersion"
|
||||||
|
sh "echo '镜像入库'"
|
||||||
|
sh "docker push harbor.fengchaoit.com/$groupName/$projectName:$targetVersion"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('保存项目镜像到project文件夹') {
|
||||||
|
steps {
|
||||||
|
sh "mkdir -p project"
|
||||||
|
sh "docker save $projectName:$targetVersion -o ${env.projectName}_${env.targetVersion}.tar"
|
||||||
|
sh "tar zcf ${env.projectName}_${env.targetVersion}.tar.gz ${env.projectName}_${targetVersion}.tar"
|
||||||
|
sh "cp *.txt *.tar.gz project/"
|
||||||
|
sh "cp docker/front/apprun.sh project/"
|
||||||
|
sh "sed -i \"s/^PROJECT_NAME.*/PROJECT_NAME=${env.projectName}/g\" project/apprun.sh"
|
||||||
|
sh "sed -i \"s/^IMAGE_NAME.*/IMAGE_NAME=${env.projectName}/g\" project/apprun.sh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
stage('保存到发布版本目录'){
|
||||||
|
when {
|
||||||
|
expression { env.branch == "main" }
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
sh "mkdir -p /projects/$groupName/$targetVersion/$projectName"
|
||||||
|
sh "rm -f /projects/$groupName/$targetVersion/$projectName/*"
|
||||||
|
sh "cp project/* /projects/$groupName/$targetVersion/$projectName/"
|
||||||
|
script {
|
||||||
|
if(Boolean.parseBoolean(proddeploy)) {
|
||||||
|
sshPublisher(
|
||||||
|
continueOnError: false,
|
||||||
|
failOnError: true,
|
||||||
|
publishers: [
|
||||||
|
sshPublisherDesc(
|
||||||
|
configName: env.branchConfig,
|
||||||
|
transfers: [
|
||||||
|
sshTransfer(
|
||||||
|
cleanRemote: false,
|
||||||
|
excludes: '',
|
||||||
|
execCommand: [
|
||||||
|
"echo 定位到项目位置",
|
||||||
|
"cd /usr/local/fengchaoit/$projectName",
|
||||||
|
"echo 授予启动脚本权限",
|
||||||
|
"chmod +x ./apprun.sh",
|
||||||
|
"echo 停止正在运行服务",
|
||||||
|
"./apprun.sh remove",
|
||||||
|
"echo 启动新构建服务",
|
||||||
|
"./apprun.sh restart"
|
||||||
|
].join('\n'),
|
||||||
|
execTimeout: 120000,
|
||||||
|
flatten: false,
|
||||||
|
makeEmptyDirs: false,
|
||||||
|
noDefaultExcludes: false,
|
||||||
|
patternSeparator: '[, ]+',
|
||||||
|
remoteDirectory: "/usr/local/fengchaoit/$projectName",
|
||||||
|
remoteDirectorySDF: false,
|
||||||
|
removePrefix: 'project',
|
||||||
|
sourceFiles: 'project/*'
|
||||||
|
)
|
||||||
|
],
|
||||||
|
usePromotionTimestamp: false,
|
||||||
|
useWorkspaceInPromotion: false,
|
||||||
|
verbose: true
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
stage('部署项目到服务器'){
|
||||||
|
when {
|
||||||
|
expression { env.branch == "dev" || env.branch == "test" || env.branch =~ /(^f_.*_dev$)|(^fix_.*_test$)/ }
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
sshPublisher(
|
||||||
|
continueOnError: false,
|
||||||
|
failOnError: true,
|
||||||
|
publishers: [
|
||||||
|
sshPublisherDesc(
|
||||||
|
configName: env.branchConfig,
|
||||||
|
transfers: [
|
||||||
|
sshTransfer(
|
||||||
|
cleanRemote: false,
|
||||||
|
excludes: '',
|
||||||
|
execCommand: [
|
||||||
|
"echo 定位到项目位置",
|
||||||
|
"cd /usr/local/fengchaoit/WorkSpace/$projectName",
|
||||||
|
"echo 授予启动脚本权限",
|
||||||
|
"chmod +x ./apprun.sh",
|
||||||
|
"echo 停止正在运行服务",
|
||||||
|
"./apprun.sh remove",
|
||||||
|
"echo 启动新构建服务",
|
||||||
|
"./apprun.sh restart"
|
||||||
|
].join('\n'),
|
||||||
|
execTimeout: 120000,
|
||||||
|
flatten: false,
|
||||||
|
makeEmptyDirs: false,
|
||||||
|
noDefaultExcludes: false,
|
||||||
|
patternSeparator: '[, ]+',
|
||||||
|
remoteDirectory: "/usr/local/fengchaoit/WorkSpace/$projectName",
|
||||||
|
remoteDirectorySDF: false,
|
||||||
|
removePrefix: 'project',
|
||||||
|
sourceFiles: 'project/*'
|
||||||
|
)
|
||||||
|
],
|
||||||
|
usePromotionTimestamp: false,
|
||||||
|
useWorkspaceInPromotion: false,
|
||||||
|
verbose: true
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('清理') {
|
||||||
|
steps {
|
||||||
|
sh "echo '删除原始镜像'"
|
||||||
|
sh "docker rmi $projectName:$targetVersion"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
success {
|
||||||
|
script {
|
||||||
|
if(env.branch == "main"){
|
||||||
|
sh "curl -X POST -H \"Content-Type: application/json\" -d '{\"msg_type\":\"interactive\",\"card\":{\"config\":{},\"i18n_elements\":{\"zh_cn\":[{\"tag\":\"column_set\",\"flex_mode\":\"none\",\"horizontal_spacing\":\"default\",\"background_style\":\"default\",\"columns\":[{\"tag\":\"column\",\"elements\":[{\"tag\":\"div\",\"text\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目${targetVersion}版本打包成功,请及时获取备份\",\"text_size\":\"normal\",\"text_align\":\"left\",\"text_color\":\"default\"},\"icon\":{\"tag\":\"standard_icon\",\"token\":\"announce_filled\",\"color\":\"grey\"}}],\"width\":\"weighted\",\"weight\":1}]}]},\"i18n_header\":{\"zh_cn\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"版本构建成功通知\"},\"subtitle\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目打包成功\"},\"template\":\"green\",\"ud_icon\":{\"tag\":\"standard_icon\",\"token\":\"msgcard-rectangle_outlined\"}}}}}}' https://open.feishu.cn/open-apis/bot/v2/hook/ecad4435-131f-4d49-ab2b-d12e95c9b245"
|
||||||
|
} else if(env.branch == "dev" || env.branch == "test" || env.branch =~ /(^f_.*_dev$)|(^fix_.*_test$)/) {
|
||||||
|
sh "curl -X POST -H \"Content-Type: application/json\" -d '{\"msg_type\":\"interactive\",\"card\":{\"config\":{},\"i18n_elements\":{\"zh_cn\":[{\"tag\":\"column_set\",\"flex_mode\":\"none\",\"horizontal_spacing\":\"default\",\"background_style\":\"default\",\"columns\":[{\"tag\":\"column\",\"elements\":[{\"tag\":\"div\",\"text\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目${targetVersion}版本打包成功且已部署到${env.branchConfig}环境,请及时查看验证\",\"text_size\":\"normal\",\"text_align\":\"left\",\"text_color\":\"default\"},\"icon\":{\"tag\":\"standard_icon\",\"token\":\"announce_filled\",\"color\":\"grey\"}}],\"width\":\"weighted\",\"weight\":1}]}]},\"i18n_header\":{\"zh_cn\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"版本构建成功通知\"},\"subtitle\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目部署成功\"},\"template\":\"green\",\"ud_icon\":{\"tag\":\"standard_icon\",\"token\":\"msgcard-rectangle_outlined\"}}}}}}' https://open.feishu.cn/open-apis/bot/v2/hook/ecad4435-131f-4d49-ab2b-d12e95c9b245"
|
||||||
|
} else {
|
||||||
|
sh "curl -X POST -H \"Content-Type: application/json\" -d '{\"msg_type\":\"interactive\",\"card\":{\"config\":{},\"i18n_elements\":{\"zh_cn\":[{\"tag\":\"column_set\",\"flex_mode\":\"none\",\"horizontal_spacing\":\"default\",\"background_style\":\"default\",\"columns\":[{\"tag\":\"column\",\"elements\":[{\"tag\":\"div\",\"text\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目${env.branch}分支构建成功,非有效分支无法进行远程部署,请及时检查\",\"text_size\":\"normal\",\"text_align\":\"left\",\"text_color\":\"default\"},\"icon\":{\"tag\":\"standard_icon\",\"token\":\"announce_filled\",\"color\":\"grey\"}}],\"width\":\"weighted\",\"weight\":1}]}]},\"i18n_header\":{\"zh_cn\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"版本构建成功通知\"},\"subtitle\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目构建成功\"},\"template\":\"green\",\"ud_icon\":{\"tag\":\"standard_icon\",\"token\":\"msgcard-rectangle_outlined\"}}}}}}' https://open.feishu.cn/open-apis/bot/v2/hook/ecad4435-131f-4d49-ab2b-d12e95c9b245"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
sh "curl -X POST -H \"Content-Type: application/json\" -d '{\"msg_type\":\"interactive\",\"card\":{\"config\":{},\"i18n_elements\":{\"zh_cn\":[{\"tag\":\"column_set\",\"flex_mode\":\"none\",\"horizontal_spacing\":\"default\",\"background_style\":\"default\",\"columns\":[{\"tag\":\"column\",\"elements\":[{\"tag\":\"div\",\"text\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目${env.branch}分支构建失败,请及时查看失败原因并修复\",\"text_size\":\"normal\",\"text_align\":\"left\",\"text_color\":\"default\"},\"icon\":{\"tag\":\"standard_icon\",\"token\":\"announce_filled\",\"color\":\"grey\"}}],\"width\":\"weighted\",\"weight\":1}]}]},\"i18n_header\":{\"zh_cn\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"项目构建失败通知\"},\"subtitle\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目构建失败\"},\"template\":\"red\",\"ud_icon\":{\"tag\":\"standard_icon\",\"token\":\"close_outlined\"}}}}}}' https://open.feishu.cn/open-apis/bot/v2/hook/ecad4435-131f-4d49-ab2b-d12e95c9b245"
|
||||||
|
}
|
||||||
|
aborted {
|
||||||
|
sh "curl -X POST -H \"Content-Type: application/json\" -d '{\"msg_type\":\"interactive\",\"card\":{\"config\":{},\"i18n_elements\":{\"zh_cn\":[{\"tag\":\"column_set\",\"flex_mode\":\"none\",\"horizontal_spacing\":\"default\",\"background_style\":\"default\",\"columns\":[{\"tag\":\"column\",\"elements\":[{\"tag\":\"div\",\"text\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目${env.branch}分支构建未成功,请及时查看失败原因并修复\",\"text_size\":\"normal\",\"text_align\":\"left\",\"text_color\":\"default\"},\"icon\":{\"tag\":\"standard_icon\",\"token\":\"announce_filled\",\"color\":\"grey\"}}],\"width\":\"weighted\",\"weight\":1}]}]},\"i18n_header\":{\"zh_cn\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"项目构建未成功通知\"},\"subtitle\":{\"tag\":\"plain_text\",\"content\":\"${projectName}项目构建未成功\"},\"template\":\"orange\",\"ud_icon\":{\"tag\":\"standard_icon\",\"token\":\"warning_outlined\"}}}}}}' https://open.feishu.cn/open-apis/bot/v2/hook/ecad4435-131f-4d49-ab2b-d12e95c9b245"
|
||||||
|
}
|
||||||
|
cleanup {
|
||||||
|
sh "echo '清理无用空镜像'"
|
||||||
|
sh "docker image prune -f"
|
||||||
|
sh "var=\$(docker ps -a -q --filter \"status=exited\");if [ -n \"\$var\" ];then docker rm \$var; fi"
|
||||||
|
cleanWs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
47
README.md
47
README.md
@@ -1,3 +1,46 @@
|
|||||||
# mversion-ui
|
# 前端基座版本
|
||||||
|
|
||||||
|
智视界前端管理系统-基座版本
|
||||||
|
|
||||||
|
|
||||||
|
## 项目结构目录
|
||||||
|
1. modules 为内置插件模块
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 项目技术方案
|
||||||
|
项目技术类型:Vue3.5+Typescript+Vite 技术方案
|
||||||
|
微前端工具:micro-app
|
||||||
|
状态管理工具:Pina
|
||||||
|
测试工具:jest
|
||||||
|
国际化: i18n
|
||||||
|
工作流-工作节点工具:vueflow
|
||||||
|
|
||||||
|
|
||||||
|
## 基础样式组件介绍
|
||||||
|
1. 颜色-样式 统一色调值
|
||||||
|
2. 尺寸-样式 统一样式大小
|
||||||
|
3. 布局-样式 Layout普通管理系统布局
|
||||||
|
|
||||||
|
|
||||||
|
## 表单组件封装样式
|
||||||
|
1. 表单组件统一封装 表单按照label-position="top"方式
|
||||||
|
2. 弹窗组件统一风格样式
|
||||||
|
3. 抽屉组件统一风格样式
|
||||||
|
4. 表单编辑统一使用 双击表单即为编辑 失去焦点即为保存 这种机制
|
||||||
|
5. 表单字段涉及到下方权限功能
|
||||||
|
|
||||||
|
|
||||||
|
## 后续迭代方向
|
||||||
|
1. 插件功能抽离为单独的组件模块 通过url方式进行引入调试 实现跟主应用解耦的操作
|
||||||
|
|
||||||
|
|
||||||
|
## 项目中使用到的插件库实现情况
|
||||||
|
[x] i18n注入 语言全局化实现
|
||||||
|
[x] 状态管理工具Pinia实现
|
||||||
|
[x] ElementPlus 组件引入 并且实现i18n
|
||||||
|
[] 引入unocss 样式
|
||||||
|
[] 路由由后端控制实现动态路由
|
||||||
|
|
||||||
|
|
||||||
名匠前端项目-基座版本
|
|
||||||
84
auto-imports.d.ts
vendored
Normal file
84
auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/* 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
|
||||||
|
}
|
||||||
22
components.d.ts
vendored
Normal file
22
components.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
// oxlint-disable
|
||||||
|
// ------
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
|
||||||
|
export {}
|
||||||
|
|
||||||
|
/* prettier-ignore */
|
||||||
|
declare module 'vue' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
|
ElDatePick: typeof import('element-plus/es')['ElDatePick']
|
||||||
|
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||||
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
|
PageForm: typeof import('./src/components/pageForm/index.vue')['default']
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
}
|
||||||
|
}
|
||||||
3
config.js
Normal file
3
config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const VITE_PROJECT_PREFIX = '/';
|
||||||
|
export const VITE_PUBLIC_PATH = './';
|
||||||
|
export const VITE_APP_BASE_API = 'http://api.test.com';
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>智视界</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2779
package-lock.json
generated
Normal file
2779
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
package.json
Normal file
32
package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "vite-project",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@micro-zoe/micro-app": "^1.0.0-rc.28",
|
||||||
|
"element-plus": "^2.13.0",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"vue": "^3.5.24",
|
||||||
|
"vue-i18n": "^11.2.7",
|
||||||
|
"vue-router": "^4.6.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
"@vue/tsconfig": "^0.8.1",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
|
"sass": "^1.97.1",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"unplugin-auto-import": "^20.3.0",
|
||||||
|
"unplugin-vue-components": "^30.0.0",
|
||||||
|
"vite": "^7.2.4",
|
||||||
|
"vue-tsc": "^3.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
1975
pnpm-lock.yaml
generated
Normal file
1975
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
public/vite.svg
Normal file
1
public/vite.svg
Normal 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="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
21
src/App.vue
Normal file
21
src/App.vue
Normal 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
6
src/api/index.ts
Normal 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
1
src/assets/vue.svg
Normal 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
90
src/auto-imports.d.ts
vendored
Normal 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')
|
||||||
|
}
|
||||||
314
src/components/pageForm/index.vue
Normal file
314
src/components/pageForm/index.vue
Normal 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>
|
||||||
32
src/hooks/usePermission.ts
Normal file
32
src/hooks/usePermission.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
90
src/hooks/useTokenRefresh.ts
Normal file
90
src/hooks/useTokenRefresh.ts
Normal 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
7
src/locales/en.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default {
|
||||||
|
message: {
|
||||||
|
title: 'Hello World',
|
||||||
|
hello: 'Hello',
|
||||||
|
welcome: 'Welcome',
|
||||||
|
},
|
||||||
|
}
|
||||||
7
src/locales/zh.ts
Normal file
7
src/locales/zh.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default {
|
||||||
|
message: {
|
||||||
|
title: '你好世界',
|
||||||
|
hello: '你好',
|
||||||
|
welcome: '欢迎使用',
|
||||||
|
},
|
||||||
|
}
|
||||||
57
src/main.ts
Normal file
57
src/main.ts
Normal 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
13
src/pages/Home/index.vue
Normal 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
12
src/pages/Login/index.vue
Normal 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
258
src/request/index.ts
Normal 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
16
src/router/index.ts
Normal 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
3
src/store/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import useUserStore from './modules/user';
|
||||||
|
|
||||||
|
export { useUserStore };
|
||||||
12
src/store/modules/user.ts
Normal file
12
src/store/modules/user.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
|
const useUserStore = defineStore("user", {
|
||||||
|
state: () => {
|
||||||
|
return {
|
||||||
|
name: "user",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default useUserStore;
|
||||||
1
src/styles/element/index.scss
Normal file
1
src/styles/element/index.scss
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
36
src/utils/directives.ts
Normal file
36
src/utils/directives.ts
Normal 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
20
src/utils/lang.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
16
tsconfig.app.json
Normal file
16
tsconfig.app.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
26
tsconfig.node.json
Normal file
26
tsconfig.node.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
85
vite.config.ts
Normal file
85
vite.config.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { defineConfig, loadEnv } from "vite";
|
||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import AutoImport from "unplugin-auto-import/vite";
|
||||||
|
import Components from "unplugin-vue-components/vite";
|
||||||
|
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
|
||||||
|
import { resolve } from "path";
|
||||||
|
import { VITE_APP_BASE_API, VITE_PUBLIC_PATH } from './config.js';
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig(({ mode }) => {
|
||||||
|
// 加载环境变量
|
||||||
|
const env = loadEnv(mode, process.cwd(), "");
|
||||||
|
return {
|
||||||
|
plugins: [
|
||||||
|
AutoImport({
|
||||||
|
imports: ["vue", "vue-router", "pinia"],
|
||||||
|
// 自动导入自定义目录下的导出
|
||||||
|
dirs: [],
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
dts: resolve(__dirname, "src/auto-imports.d.ts"), // 生成自动导入的声明文件
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
}),
|
||||||
|
vue(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": resolve(__dirname, "src"),
|
||||||
|
"@assets": resolve(__dirname, "src/assets"),
|
||||||
|
"@components": resolve(__dirname, "src/components"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
base: VITE_PUBLIC_PATH,
|
||||||
|
// 服务配置
|
||||||
|
server: {
|
||||||
|
host: "0.0.0.0",
|
||||||
|
port: 3000,
|
||||||
|
open: true, // 自动打开浏览器
|
||||||
|
cors: true, // 允许跨域
|
||||||
|
strictPort: false, // 端口被占用时是否尝试使用下一个可用端口
|
||||||
|
proxy: {
|
||||||
|
// 代理配置
|
||||||
|
"/api": {
|
||||||
|
target: VITE_APP_BASE_API,
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ""),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 构建配置
|
||||||
|
build: {
|
||||||
|
outDir: "dist",
|
||||||
|
assetsDir: "static",
|
||||||
|
sourcemap: false,
|
||||||
|
minify: "terser",
|
||||||
|
terserOptions: {
|
||||||
|
compress: {
|
||||||
|
drop_console: false,
|
||||||
|
drop_debugger: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
chunkFileNames: "static/js/[name]-[hash].js",
|
||||||
|
entryFileNames: "static/js/[name]-[hash].js",
|
||||||
|
assetFileNames: "static/[ext]/[name]-[hash].[ext]",
|
||||||
|
manualChunks: {
|
||||||
|
vendor: ["vue", "vue-router"],
|
||||||
|
utils: ["lodash", "axios"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// CSS 配置
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
additionalData: `@use "@/styles/element/index.scss" as *;`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user