Commit 55b357aa authored by tyyin lan's avatar tyyin lan

build: project initialization

parents
root = true
[*]
indent_style = space
charset = utf-8
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
insert_final_newline = false
VITE_APP_ENV = 'DEV'
VITE_APP_THEME_COLOR = '#00a2ea'
VITE_PORT = 8848
VITE_PUBLIC_PATH = '/'
VITE_ROUTER_MODE = 'h5'
VITE_VITEST = true
VITE_HIDE_HOME = false
VITE_APP_ENV = 'PROD'
VITE_APP_THEME_COLOR = '#00a2ea'
VITE_PUBLIC_PATH = '/'
VITE_ROUTER_MODE = 'h5'
# 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/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
auto-imports.d.ts
components.d.ts
package-lock.json
pnpm dlx commitlint --edit $1
npm run lint:lint-staged
\ No newline at end of file
node_modules
build
dist
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"EditorConfig.EditorConfig",
"bradlc.vscode-tailwindcss",
"stylelint.vscode-stylelint",
"redhat.vscode-yaml",
],
"unwantedRecommendations": [
"bradlc.vscode-tailwindcss"
]
}
\ No newline at end of file
{
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.tabSize": 2,
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"tailwindCSS.experimental.configFile": "src/styles/app.css",
"files.associations": {
"*.css": "tailwindcss"
},
"editor.quickSuggestions": {
"strings": "on"
},
"extensions.disabledRecommendations": [
"antfu.unocss"
]
}
\ No newline at end of file
FROM nginx:stable-alpin
RUN rm -rf /usr/share/nginx/html
COPY dist/ /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]
/** 处理环境变量 */
export const wrapperEnv = (envConf: Recordable): ViteEnv => {
const ret: ViteEnv = {
VITE_APP_ENV: 'DEV',
VITE_PORT: 8848,
VITE_PUBLIC_PATH: '/',
VITE_ROUTER_MODE: 'hash',
VITE_VITEST: true,
VITE_HIDE_HOME: false,
}
for (const envName of Object.keys(envConf)) {
let realName = envConf[envName].replace(/\\n/g, '\n')
realName = realName === 'true' ? true : realName === 'false' ? false : realName
if (envName === 'VITE_PORT') {
realName = Number(realName)
}
;(ret as any)[envName] = realName
if (typeof realName === 'string') {
process.env[envName] = realName
} else if (typeof realName === 'object') {
process.env[envName] = JSON.stringify(realName)
}
}
return ret
}
import { type PluginOption } from 'vite'
import vue from '@vitejs/plugin-vue'
import checker from 'vite-plugin-checker'
import { visualizer } from 'rollup-plugin-visualizer'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import tailwindcss from '@tailwindcss/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import appConfig from '../src/config/app-config.json'
function htmlPlugin() {
return {
name: 'html-transform',
transformIndexHtml(html: string) {
// 2. 将html中的占位符替换为配置的值
return html.replace(/%APP_NAME%/g, appConfig.appTitle)
},
}
}
export function setupPlugins(
isBuild: boolean,
envConf: ViteEnv,
_pathResolve: (dir: string) => string,
): PluginOption[] {
const lifecycle = process.env.npm_lifecycle_event
const plugins: PluginOption = [
vue(),
// VueI18nPlugin({
// include: [pathResolve('./src/locales/langs/**')],
// }),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
htmlPlugin(),
tailwindcss(),
]
if (envConf.VITE_VITEST && !isBuild) {
plugins.push(checker({ vueTsc: true }))
}
if (lifecycle === 'report') {
plugins.push(visualizer())
}
return plugins
}
import type { UserConfig } from '@commitlint/types'
const configuration: UserConfig = {
extends: ['@commitlint/config-conventional'],
rules: {
'body-leading-blank': [2, 'always'],
'footer-leading-blank': [1, 'always'],
'subject-empty': [2, 'never'],
'type-empty': [2, 'never'],
'type-enum': [
2,
'always',
[
'feat',
'fix',
'perf',
'style',
'docs',
'test',
'refactor',
'build',
'ci',
'chore',
'revert',
'types',
'release',
],
],
},
}
export default configuration
import globals from 'globals'
import pluginJs from '@eslint/js'
import tseslint from 'typescript-eslint'
import pluginVue from 'eslint-plugin-vue'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import vueParser from 'vue-eslint-parser'
import tsParser from '@typescript-eslint/parser'
export default [
pluginJs.configs.recommended,
...tseslint.configs.recommended,
...pluginVue.configs['flat/recommended'],
{
languageOptions: {
globals: {
...globals.browser,
NodeJS: 'readonly',
Recordable: 'readonly',
ViteEnv: 'readonly',
AnyObject: 'readonly',
ConversationMessageItem: 'readonly',
ConversationMessageItemInfo: 'readonly',
},
parser: vueParser,
parserOptions: {
parser: tsParser,
sourceType: 'module',
ecmaVersion: 'latest',
ecmaFeatures: {
jsx: true,
},
},
},
rules: {
'no-console': 'warn',
'vue/attribute-hyphenation': 'error',
'vue/multi-word-component-names': 'off',
'vue/component-name-in-template-casing': [
'error',
'PascalCase',
{
registeredComponentsOnly: true,
ignores: [],
},
],
'vue/v-on-event-hyphenation': [
'error',
'always',
{
autofix: true,
ignore: [],
},
],
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/array-type': 'error',
},
},
eslintPluginPrettierRecommended,
{
ignores: ['dist/', 'public/'],
},
]
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="//at.alicdn.com/t/c/font_4939750_9nzhie7u3y.css" />
<title>%APP_NAME%</title>
</head>
<body>
<div id="app">
<style>
.loading-wrapper {
width: 100%;
margin-top: 40vh;
display: flex;
justify-content: center;
}
.pulse {
width: 110px;
height: 60px;
color: #00a2ea;
--c: radial-gradient(farthest-side, currentColor 96%, #0000);
background:
var(--c) 100% 100% /30% 60%,
var(--c) 70% 0 /50% 100%,
var(--c) 0 100% /36% 68%,
var(--c) 27% 18% /26% 40%,
linear-gradient(currentColor 0 0) bottom/67% 58%;
background-repeat: no-repeat;
position: relative;
}
.pulse:after {
content: '';
position: absolute;
inset: 0;
background: inherit;
opacity: 0.4;
animation: pulse-hjvm54 1s infinite;
}
@keyframes pulse-hjvm54 {
to {
transform: scale(1.8);
opacity: 0;
}
}
</style>
<div class="loading-wrapper">
<div class="pulse"></div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
export default {
'*.md': ['prettier --write'],
'package.json': ['prettier --write'],
'!(package).json': ['prettier --write--parser json'],
'*.{css,scss,postcss,less}': ['stylelint --fix', 'prettier --write'],
'*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
'*.vue': ['eslint --fix', 'stylelint --fix', 'prettier --write', 'echo "统一格式化完成🌸"'],
}
{
"name": "hxyj-admin-fe",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "npm run build:uat",
"build:sit": "vue-tsc --noEmit && vite build --mode development",
"build:uat": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"lint:lint-staged": "lint-staged -c ./lint-staged.config.js",
"lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
"lint:stylelint": "stylelint \"./src/**/*.{html,vue,css,scss}\" --fix",
"prepare": "husky",
"preinstall": "npx only-allow pnpm"
},
"dependencies": {
"@icon-park/vue-next": "^1.4.2",
"@tailwindcss/vite": "^4.1.8",
"@vueuse/core": "^13.3.0",
"axios": "^1.9.0",
"element-plus": "^2.9.11",
"nanoid": "^5.1.5",
"nprogress": "^0.2.0",
"pinia": "^3.0.2",
"tailwindcss": "^4.1.8",
"tippy.js": "^6.3.7",
"vue": "^3.5.16",
"vue-i18n": "^11.1.5",
"vue-router": "^4.5.1",
"vue-tippy": "^6.7.1"
},
"devDependencies": {
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"@commitlint/types": "^19.8.1",
"@intlify/unplugin-vue-i18n": "^6.0.8",
"@types/node": "^22.15.29",
"@types/nprogress": "^0.2.3",
"@typescript-eslint/parser": "^8.33.1",
"@vitejs/plugin-vue": "^5.2.4",
"autoprefixer": "^10.4.21",
"eslint": "^9.28.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.4.1",
"eslint-plugin-vue": "^10.1.0",
"globals": "^16.2.0",
"husky": "^9.1.7",
"lint-staged": "^16.1.0",
"postcss-html": "^1.8.0",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.12",
"rollup-plugin-visualizer": "^6.0.1",
"stylelint": "^16.20.0",
"stylelint-config-recess-order": "^6.0.0",
"stylelint-config-recommended-vue": "^1.6.0",
"stylelint-config-standard": "^38.0.0",
"stylelint-order": "^7.0.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.33.1",
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.7.0",
"vite": "^6.3.5",
"vite-plugin-checker": "^0.9.3",
"vue-eslint-parser": "^10.1.3",
"vue-tsc": "^2.2.10"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0",
"pnpm": ">=9"
},
"volta": {
"node": "20.19.1"
}
}
This diff is collapsed.
export default {
plugins: ['prettier-plugin-tailwindcss'],
experimentalTernaries: false,
printWidth: 120,
tabWidth: 2,
useTabs: false,
semi: false,
singleQuote: true,
quoteProps: 'as-needed', // 对象属性引号问题
jsxSingleQuote: true,
trailingComma: 'all', // 尾随逗号
bracketSpacing: true,
bracketSameLine: false,
arrowParens: 'always', // 箭头函数单个参数 提供括号支持
requirePragma: false,
insertPragma: false,
proseWrap: 'preserve',
htmlWhitespaceSensitivity: 'css',
vueIndentScriptAndStyle: false,
endOfLine: 'auto',
singleAttributePerLine: false, // 在 HTML、Vue和JSX中强制每行使用单一属性
}
import { request } from '@/utils/request'
export function fetchLogin<T>(payload: { username: string; password: string }) {
return request.post<T>(`/oauth/auth?username=${payload.username}&password=${payload.password}`, null, {
ignoreErrorCheck: true,
})
}
<script setup lang="ts">
import { ref } from 'vue'
import { useResizeObserver } from '@vueuse/core'
import { useDesignSettingStore } from '@/store/modules/design-setting'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
const designSettingStore = useDesignSettingStore()
const currentLocale = ref({
zhCn,
})
const rootContainer = ref<HTMLDivElement | null>(null)
useResizeObserver(rootContainer, (entries) => {
const entry = entries[0]
const { width } = entry.contentRect
/**
* 0 < width <= 760 隐藏侧边栏
* 760 < width <= 990 折叠侧边栏
* width > 990 展开侧边栏
*/
if (width <= 760) {
designSettingStore.toggleIsMobileLayout(true)
designSettingStore.toggleSidebarDisplayStatus('hidden')
} else if (760 < width && width <= 990) {
designSettingStore.toggleIsMobileLayout(false)
designSettingStore.toggleSidebarDisplayStatus('collapse')
} else {
designSettingStore.toggleIsMobileLayout(false)
designSettingStore.toggleSidebarDisplayStatus('expand')
}
})
</script>
<template>
<div ref="rootContainer" class="h-full w-full">
<el-config-provider :locale="currentLocale">
<RouterView v-slot="{ Component }">
<Component :is="Component" />
</RouterView>
</el-config-provider>
</div>
</template>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { useTippy } from 'vue-tippy'
import type { Placement } from 'tippy.js'
// 1. 定义 Props
interface Props {
// 控制最大显示行数
maxLines?: number
// Tippy 弹窗的位置
placement?: Placement
}
const { maxLines = 1, placement = 'right' } = defineProps<Props>()
// 2. 获取 DOM 元素的引用
const textContainerRef = ref<HTMLElement | null>(null)
// 3. 使用 useTippy 创建 Tippy 实例
// 我们将目标元素(textContainerRef)和配置项传入
const tippy = useTippy(textContainerRef, {
// 将 props 中的 placement 响应式地传递给 tippy
placement: placement,
// 添加一个动画效果,使其过渡更平滑
// animation: 'scale',
animation: 'scale-subtle',
// Tippy 的内容将是元素的完整文本
// 我们在启用它时再设置 content,以确保获取到最新内容
content: () => textContainerRef.value?.textContent || '',
})
// 4. 核心逻辑:检测文本是否溢出
const checkTruncation = () => {
const element = textContainerRef.value
if (!element) return
// 关键判断:当元素的滚动高度大于其可见高度时,说明内容被截断了
const isTruncated = element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth
if (isTruncated) {
// 如果文本被截断,启用 tippy
tippy.enable()
} else {
// 否则,禁用 tippy
tippy.disable()
}
}
// 5. 使用 ResizeObserver 监听元素尺寸变化
// 这是最健壮的方式,当父容器宽度变化导致溢出状态改变时,能自动更新
let resizeObserver: ResizeObserver | null = null
onMounted(() => {
const element = textContainerRef.value
if (element) {
// 首次挂载时检查一次
checkTruncation()
// 创建 ResizeObserver 实例
resizeObserver = new ResizeObserver(() => {
// 当元素尺寸变化时,重新检查
checkTruncation()
})
// 开始监听元素
resizeObserver.observe(element)
}
})
// 6. 组件卸载时清理 Observer
onUnmounted(() => {
if (resizeObserver && textContainerRef.value) {
resizeObserver.unobserve(textContainerRef.value)
}
resizeObserver = null
})
// 7. 监听 maxLines 变化,以重新应用样式并检查
// 虽然 CSS v-bind 是响应式的,但 DOM 高度变化需要重新检查
watch(
() => maxLines,
() => {
// 使用 nextTick 确保 DOM 更新后再检查
// 在实践中,因为 CSS 变化会自动触发 ResizeObserver,这一步通常是可选的,
// 但显式添加可以增加代码的健壮性。
checkTruncation()
},
)
</script>
<template>
<div ref="textContainerRef" class="ellipsis-container">
<slot></slot>
</div>
</template>
<style lang="css" scoped>
.ellipsis-container {
/* 关键的 CSS,用于实现多行文本溢出省略 */
display: -webkit-box;
-webkit-box-orient: vertical;
/* 要求2:最大宽度随父元素决定 */
width: 100%;
max-width: 100%;
/* 使用 v-bind 将 Vue 的 props 绑定到 CSS 变量 */
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: v-bind('maxLines');
}
</style>
{
"appTitle": "航行印记管理后台"
}
export const BASE_URLS: Record<'DEV' | 'PROD', string> = {
DEV: '',
PROD: '',
}
import { GlobalThemeOverrides } from 'naive-ui'
export const themeOverrides: GlobalThemeOverrides = {
common: {
primaryColor: '#00a2ea',
primaryColorHover: '#0CABF0',
primaryColorPressed: '#038FCB',
primaryColorSuppl: '#00a2ea',
},
LoadingBar: {
colorLoading: '#00a2ea',
},
}
<script setup lang="ts">
import { useDesignSettingStore } from '@/store/modules/design-setting'
import { useUserStore } from '@/store/modules/user'
import { readonly } from 'vue'
import { useRouter } from 'vue-router'
import { ExpandRight, ExpandLeft } from '@icon-park/vue-next'
const defaultAvatar = 'https://mkp-dev.oss-cn-shenzhen.aliyuncs.com/game-template/20221018/1666079174947.png'
const designSettingStore = useDesignSettingStore()
const userStore = useUserStore()
const router = useRouter()
// const currentRoute = useRoute()
const menuOptions = readonly([
{
label: '退出登录',
key: 1,
},
])
function handleSidebarDisplayStatusChange(status: 'collapse' | 'expand') {
if (designSettingStore.sidebarDisplayStatus === 'hidden') {
designSettingStore.changeShowSidebarDrawer(true)
return
}
designSettingStore.toggleSidebarDisplayStatus(status)
}
function onDropdownSelect(key: number) {
if (key === 1) {
userStore.logout().then(() => {
router.push({ name: 'Login' })
})
}
}
// function handleRefreshPage() {
// router.replace({ path: currentRoute.fullPath })
// }
</script>
<template>
<div class="flex justify-between bg-white shadow-[0_1px_4px_rgba(0,21,41,0.08)] select-none">
<div class="flex">
<div class="flex items-center px-3">
<ExpandLeft
v-show="designSettingStore.sidebarDisplayStatus !== 'expand'"
class="cursor-pointer"
theme="outline"
size="21"
fill="#333639"
:stroke-width="3"
@click="handleSidebarDisplayStatusChange('expand')"
/>
<ExpandRight
v-show="designSettingStore.sidebarDisplayStatus === 'expand'"
class="cursor-pointer"
theme="outline"
size="21"
fill="#333639"
:stroke-width="3"
@click="handleSidebarDisplayStatusChange('collapse')"
/>
</div>
</div>
<div class="mr-5 flex sm:mr-6">
<div class="flex cursor-pointer items-center px-2">
<el-dropdown placement="bottom" @command="onDropdownSelect">
<div class="flex h-full items-center outline-none">
<el-avatar :size="30" fit="cover" :src="userStore.userInfo.avatar || defaultAvatar" shape="circle" />
<div class="ml-2 max-w-24 truncate text-base">{{ userStore.userInfo.userName }}</div>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="option in menuOptions" :key="option.key" :command="option.key">
{{ option.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useDesignSettingStore } from '@/store/modules/design-setting'
import appConfig from '@/config/app-config.json'
const designSettingStore = useDesignSettingStore()
</script>
<template>
<div class="flex h-[64px] cursor-pointer items-center justify-center px-2">
<div
class="h-[32px] w-[32px] shrink-0 bg-[url('https://gsst-poe-sit.gz.bcebos.com/data/20240911/1726041369632.webp')] bg-cover"
></div>
<div
class="truncate pl-[8px] text-lg font-semibold transition"
:class="designSettingStore.isMenuCollapse ? 'w-0 !pl-0' : ''"
>
{{ appConfig.appTitle }}
</div>
</div>
</template>
<script setup lang="ts">
import { sidebarMenus } from '@/router/index'
import { useDesignSettingStore } from '@/store/modules/design-setting'
import SidebarLogo from '../sidebar-logo/sidebar-logo.vue'
// import { type MenuOption } from '@/router/utils'
import { useRoute, useRouter } from 'vue-router'
import { ref, watch } from 'vue'
import EllipsisTooltipText from '@/components/ellipsis-tooltip-text.vue'
const designSettingStore = useDesignSettingStore()
const currentRoute = useRoute()
const router = useRouter()
const menuValue = ref(currentRoute.meta.key)
watch(
() => currentRoute.fullPath,
() => {
menuValue.value = currentRoute.meta.key
},
)
// function handleUpdateValue(_key: string, menuItemOption: MenuOption) {
// router.push({ name: menuItemOption.routeName })
// // menuValue.value = key
// }
function handleLogoClick() {
router.push({ name: 'Root' })
}
function handleMenuItemClick(routeName: any) {
console.log('💥💥💥💥💥💥jtest💥💥💥💥💥💥')
console.log(routeName)
// router.push({ name: routeName })
}
</script>
<template>
<div class="sidebar-container flex h-full flex-col">
<SidebarLogo @click="handleLogoClick" />
<div class="flex-1">
<el-scrollbar>
<el-menu
:popper-offset="3"
:default-active="menuValue"
:collapse="designSettingStore.isMenuCollapse"
class="!border-none"
>
<template v-for="menuInfo in sidebarMenus">
<template v-if="!menuInfo.children">
<el-menu-item
:key="menuInfo.key"
:index="menuInfo.key"
class="relative hover:!bg-[unset]"
@click="handleMenuItemClick(menuInfo.routeName)"
>
<span
class="absolute inset-x-[10px] inset-y-[4px] rounded-[6px] bg-[#e5f6fd] opacity-0 transition"
:class="
menuValue === menuInfo.key ? 'opacity-100' : 'group-hover:bg-[#f3f3f5] group-hover:opacity-100'
"
></span>
<i
class="iconfont z-1 pr-[8px] pl-[4px] !text-[16px]"
:class="{
[menuInfo.icon]: true,
'!text-(--el-menu-active-color)': menuValue === menuInfo.key,
'!text-(--el-text-color-primary)': menuValue !== menuInfo.key,
}"
></i>
<template #title>
<EllipsisTooltipText
class="z-1"
:class="
menuValue === menuInfo.key ? '!text-(--el-menu-active-color)' : 'text-(--el-text-color-primary)'
"
>
{{ menuInfo.label }}
</EllipsisTooltipText>
<!-- <el-text class="z-1 !text-(--el-menu-active-color)" truncated></el-text> -->
</template>
</el-menu-item>
</template>
<template v-else>
<el-sub-menu :key="menuInfo.key" :index="menuInfo.key" class="relative hover:!bg-[unset]">
<template #title>
<span
class="menu-bg-panel absolute inset-x-[10px] inset-y-[4px] rounded-[6px] bg-[#e5f6fd] opacity-0 transition"
:class="menuValue === menuInfo.key ? 'opacity-100' : ''"
></span>
<i
class="iconfont z-1 pr-[8px] pl-[4px] !text-[16px]"
:class="{
[menuInfo.icon]: true,
'!text-(--el-menu-active-color)': menuValue === menuInfo.key,
'!text-(--el-text-color-primary)': menuValue !== menuInfo.key,
}"
></i>
<span class="z-1">{{ menuInfo.label }}</span>
</template>
<template v-for="menuChildInfo in menuInfo.children" :key="menuChildInfo.key">
<el-menu-item
:index="menuChildInfo.key"
class="group relative hover:!bg-[unset]"
@click="handleMenuItemClick(menuChildInfo.routeName)"
>
<span
class="absolute inset-x-[10px] inset-y-[4px] rounded-[6px] bg-[#e5f6fd] opacity-0 transition"
:class="
menuValue === menuChildInfo.key
? 'opacity-100'
: 'group-hover:bg-[#f3f3f5] group-hover:opacity-100'
"
></span>
<i
class="iconfont z-1 pr-[8px] pl-[4px] !text-[16px]"
:class="{
[menuChildInfo.icon]: true,
'!text-(--el-menu-active-color)': menuValue === menuChildInfo.key,
'!text-(--el-text-color-primary)': menuValue !== menuChildInfo.key,
}"
></i>
<template #title>
<EllipsisTooltipText
class="z-1"
:class="
menuValue === menuChildInfo.key
? '!text-(--el-menu-active-color)'
: 'text-(--el-text-color-primary)'
"
>
{{ menuChildInfo.label }}
</EllipsisTooltipText>
</template>
</el-menu-item>
</template>
</el-sub-menu>
</template>
</template>
</el-menu>
</el-scrollbar>
</div>
</div>
</template>
<style lang="css" scoped>
.sidebar-container {
/* --el-menu-base-level-padding: 12px; */
:deep(.el-menu .el-sub-menu__title) {
position: relative;
&:hover {
background-color: unset;
.menu-bg-panel {
background-color: #f3f3f5;
opacity: 1;
}
}
}
}
</style>
<script setup lang="ts">
import Sidebar from './components/sidebar/sidebar.vue'
import NavBar from './components/navbar/navbar.vue'
import { useDesignSettingStore } from '@/store/modules/design-setting'
import { ref, watchEffect } from 'vue'
const designSettingStore = useDesignSettingStore()
const layoutSideWidth = ref(210)
watchEffect(() => {
if (designSettingStore.isMenuCollapse) {
layoutSideWidth.value = 64
} else {
layoutSideWidth.value = 210
}
})
</script>
<template>
<el-container class="h-full select-none">
<el-aside
v-show="designSettingStore.sidebarDisplayStatus !== 'hidden'"
class="transition-duration-300 shadow-[2px_0_8px_0_rgba(29,35,41,0.05)] transition-[width] ease-in-out"
:width="`${layoutSideWidth}px`"
>
<Sidebar />
</el-aside>
<el-drawer
v-model="designSettingStore.showSidebarDrawer"
body-class="drawer-body-container"
:with-header="false"
direction="ltr"
:size="210"
>
<Sidebar />
</el-drawer>
<el-container>
<el-header class="!px-0" height="64px">
<NavBar class="h-full" />
</el-header>
<el-main class="main-content-wrapper">
<RouterView v-slot="{ Component }">
<Transition appear name="fade-slide" mode="out-in">
<div>
<Component :is="Component" />
</div>
</Transition>
</RouterView>
</el-main>
</el-container>
</el-container>
</template>
<style lang="css" scoped>
:deep(.el-drawer .drawer-body-container) {
padding: 0;
}
:deep(.layout-content) {
display: flex;
flex-direction: column;
background-color: #f5f7f9;
}
:deep(.main-content-wrapper) {
background-color: #f5f7f9;
}
.fade-slide-leave-active,
.fade-slide-enter-active {
transition: all 0.3s;
}
.fade-slide-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.fade-slide-leave-to {
opacity: 0;
transform: translateX(30px);
}
</style>
import { type App } from 'vue'
import { createI18n } from 'vue-i18n'
import messages from './messages'
const i18n = createI18n({
legacy: false,
locale: 'zh-HK',
fallbackLocale: 'zh-CN',
messages,
})
export function setupI18n(app: App) {
app.use(i18n)
}
buttons:
btnLogin: '登录'
buttons:
btnLogin: '登錄'
import zhHK from './langs/zh-hk.yaml'
import zhCN from './langs/zh-cn.yaml'
const messages: Record<I18n.LangType, I18n.Schema> = {
'zh-HK': zhHK,
'zh-CN': zhCN,
}
export default messages
import { createApp } from 'vue'
import { setupStore } from './store/'
import { setupRouter } from './router'
// import { setupI18n } from './locales'
import App from './app.vue'
import '@/styles/app.css'
import '@/styles/nprogress.css'
// 引入 Tippy.js 的核心、主题和动画CSS
import 'tippy.js/dist/tippy.css'
import 'tippy.js/themes/light-border.css' // 一个漂亮的主题,可选用
import 'tippy.js/animations/scale-subtle.css' // 一个平滑的动画,可选用
async function bootstrap() {
const app = createApp(App)
setupStore(app)
// setupI18n(app)
await setupRouter(app)
app.mount('#app')
}
bootstrap()
import type { Router } from 'vue-router'
import { useUserStore } from '@/store/modules/user'
import NProgress from 'nprogress'
import appConfig from '@/config/app-config.json'
NProgress.configure({ showSpinner: false })
/** 路由白名单 */
const whitePathList = ['/login']
export function createRouterGuards(router: Router) {
router.beforeEach((to) => {
NProgress.start()
const userStore = useUserStore()
if (userStore.isLogin && to.fullPath === '/login') {
return false
}
// 白名单直接跳过
if (whitePathList.includes(to.path)) {
return true
}
// if (!userStore.isLogin && !whitePathList.includes(to.fullPath)) {
// return { path: '/login', query: { redirect: encodeURIComponent(to.fullPath) } }
// }
return true
})
router.afterEach((to) => {
if (to.meta.hiddenTitle) {
document.title = appConfig.appTitle
} else {
document.title = (to.meta.title as string) || appConfig.appTitle
}
NProgress.done()
})
router.onError((error) => {
console.log(error, '--路由错误--')
})
}
import { type App } from 'vue'
import { createRouter, type RouteRecordRaw } from 'vue-router'
import { createRouterGuards } from './guards'
import baseRoutes from './modules/base'
import { getHistoryMode, menuFilterSort } from './utils'
/** 原始静态路由(未做任何处理) */
const routes: RouteRecordRaw[] = []
/* 自动导入全部静态路由,无需再手动引入! */
const modules: Record<string, any> = import.meta.glob(['./modules/**/*.ts', '!./modules/**/base.ts'], {
eager: true,
})
;(function () {
const routesDraft: RouteRecordRaw[] = []
Object.keys(modules).forEach((key) => {
routesDraft.push(modules[key].default)
})
routesDraft.forEach((routesItem) => {
if (Array.isArray(routesItem)) {
routes.push(...routesItem)
} else {
routes.push(routesItem)
}
})
})()
/** 导出处理后的静态路由(三级及以上的路由全部拍成二级) */
// export const constantRoutes: RouteRecordRaw[] = formatTwoStageRoutes(
// formatFlatteningRoutes(ascending(routes.flat(Infinity))),
// )
/** 用于渲染菜单,保持原始层级 */
// export const constantMenus: RouteRecordRaw[] = ascending(routes.flat(Infinity)).concat(...baseRoutes)
export const sidebarMenus = menuFilterSort([...routes])
const router = createRouter({
history: getHistoryMode(import.meta.env.VITE_ROUTER_MODE),
routes: [...routes, ...baseRoutes],
strict: true,
scrollBehavior(_to, from, savedPosition) {
return new Promise((resolve) => {
if (savedPosition) {
return savedPosition
} else {
if (from.meta.saveScrollTop) {
const top: number = document.documentElement.scrollTop || document.body.scrollTop
resolve({ left: 0, top })
}
}
})
},
})
export async function setupRouter(app: App) {
app.use(router)
// 创建路由守卫
createRouterGuards(router)
await router.isReady()
}
import { type RouteRecordRaw } from 'vue-router'
export default [
{
path: '/login',
name: 'Login',
meta: {
rank: 1001,
title: '登录',
},
component: () => import('@/views/login/login.vue'),
},
// {
// path: '/404',
// name: 'NotFound',
// meta: {
// title: '未找到该页面',
// },
// component: () => import('@/views/exception/404.vue'),
// },
{
path: '/500',
name: 'ServerError',
meta: {
rank: 1001,
title: '服务器错误',
},
component: () => import('@/views/exception/500.vue'),
},
{
path: '/:pathMatch(.*)*',
name: 'Universal',
meta: {
rank: 1001,
title: '未找到该页面',
},
component: () => import('@/views/exception/404.vue'),
},
] as RouteRecordRaw[]
import { type RouteRecordRaw } from 'vue-router'
import Layout from '@/layout/index.vue'
import Home from '@/views/home/home.vue'
export default [
{
path: '/',
name: 'Root',
meta: {
rank: 1001,
title: '',
},
component: Layout,
redirect: '/home',
children: [
{
path: '/home',
name: 'Home',
meta: {
rank: 1001,
title: '首页',
icon: 'icon-shouye-zhihui',
},
component: Home,
},
],
},
] as RouteRecordRaw[]
import { nanoid } from 'nanoid/non-secure'
// import { h, type VNode } from 'vue'
import { createWebHashHistory, createWebHistory, type RouteRecordRaw, type RouterHistory } from 'vue-router'
export interface MenuOption {
// label: (() => VNode) | string
label: string
key: string
routeName: string
// icon: (() => VNode) | null
icon: string
children?: MenuOption[]
}
export function getHistoryMode(modeString: ViteEnv['VITE_ROUTER_MODE']): RouterHistory {
if (modeString === 'h5') {
return createWebHistory()
}
return createWebHashHistory()
}
function menuSort(routes: RouteRecordRaw[]) {
routes.sort((a, b) => {
if (!a.meta || !a.meta.rank) {
return 1
}
if (!b.meta || !b.meta.rank) {
return -1
}
return (a.meta.rank as number) - (b.meta.rank as number)
})
return routes
}
export function menuFilterSort(routes: RouteRecordRaw[]) {
function createRouteKey(routes: RouteRecordRaw[]) {
routes.forEach((route, index) => {
if (route.meta) {
route.meta.key = nanoid()
} else {
route.meta = { key: nanoid(), title: '', rank: 1001 + index }
}
if (route.children) {
createRouteKey(route.children)
}
})
}
createRouteKey(routes)
function menuChildrenSort(routes: RouteRecordRaw[]) {
routes.forEach((routeItem) => {
if (routeItem.children) {
const newRouteChildren = menuSort(routeItem.children)
routeItem.children = newRouteChildren
menuChildrenSort(routeItem.children)
}
})
}
menuChildrenSort(routes)
menuSort(routes)
const rootRouteIndex = routes.findIndex((route) => route.name === 'Root')
if (rootRouteIndex >= 0) {
const rootRouteChildren = routes[rootRouteIndex].children || []
routes.splice(rootRouteIndex, 1)
routes.unshift(...rootRouteChildren)
}
function createMenuOptions(routes: RouteRecordRaw[]) {
const menuOptions: MenuOption[] = []
routes.forEach((route) => {
// 菜单Item隐藏判断
if (route.meta?.hideSideMenItem) {
return
}
const menuOption: MenuOption = {
// label: route.meta?.title
// ? () => h(RouterLink, { to: { name: route.name || 'Root' } }, { default: () => route.meta?.title })
// : '-',
label: (route.meta?.title as string) || '-',
key: route.meta?.key as string,
routeName: route.name as string,
// icon: route.meta?.icon ? () => h('i', { class: `iconfont ${route?.meta?.icon as string}` }) : null,
icon: route.meta?.icon || '',
}
if (route.children) {
menuOption.children = createMenuOptions(route.children)
}
menuOptions.push(menuOption)
})
return menuOptions
}
return createMenuOptions(routes)
}
import type { App } from 'vue'
import { createPinia } from 'pinia'
const pinia = createPinia()
export function setupStore(app: App) {
app.use(pinia)
}
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app-store', {
state: () => ({
layoutDevice: 'desktop',
isMenuCollapse: false,
}),
actions: {
toggleLayoutDevice(layoutDevice: 'desktop' | 'mobile') {
if (this.layoutDevice !== layoutDevice) {
this.layoutDevice = layoutDevice
if (layoutDevice === 'mobile') {
this.isMenuCollapse = true
}
}
},
toggleMenuCollapse(isCollapse: boolean) {
if (this.isMenuCollapse !== isCollapse) {
this.isMenuCollapse = isCollapse
}
},
},
})
import { defineStore } from 'pinia'
export const useDesignSettingStore = defineStore('design-setting-store', {
state: () => ({
sidebarDisplayStatus: 'expand', // status: 'expand' | 'collapse' | 'hidden'
isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),
isMobileLayout: false,
showSidebarDrawer: false,
}),
getters: {
isMenuCollapse: (state) => {
return state.sidebarDisplayStatus === 'collapse'
},
},
actions: {
toggleSidebarDisplayStatus(status: 'expand' | 'collapse' | 'hidden') {
this.sidebarDisplayStatus = status
},
toggleIsMobileLayout(result: boolean) {
this.isMobileLayout = result
},
changeShowSidebarDrawer(result: boolean) {
this.showSidebarDrawer = result
},
},
})
import { defineStore } from 'pinia'
import { ss } from '@/utils/storage'
import { STORAGE_KEYS } from '@/utils/storage-key'
import type { UserState, UserInfo } from '../types/user'
function getDefaultUserInfo(): UserInfo {
return {
account: '',
userAccount: '',
userId: null,
userName: '用户',
createdTime: '',
avatar: '',
}
}
export const useUserStore = defineStore('user-store', {
state: (): UserState => ({
isLogin: ss.get(STORAGE_KEYS.IS_LOGIN),
token: ss.get(STORAGE_KEYS.TOKEN) || '',
userInfo: ss.get(STORAGE_KEYS.USER_INFO) || getDefaultUserInfo(),
}),
actions: {
async logout() {
this.isLogin = false
this.token = ''
this.userInfo = getDefaultUserInfo()
ss.remove(STORAGE_KEYS.IS_LOGIN)
ss.remove(STORAGE_KEYS.TOKEN)
ss.remove(STORAGE_KEYS.USER_INFO)
},
updateIsLogin(status: boolean) {
this.isLogin = status
ss.set(STORAGE_KEYS.IS_LOGIN, status)
},
updateToken(token: string) {
this.token = token
ss.set(STORAGE_KEYS.TOKEN, token)
},
updateUserInfo(userInfo: UserInfo) {
this.userInfo = userInfo
ss.set(STORAGE_KEYS.USER_INFO, userInfo)
},
},
})
export interface UserInfo {
account: string
userAccount: string
userId: null | number
userName: string
createdTime: string
avatar: string
}
export interface UserState {
isLogin: boolean
token: string
userInfo: UserInfo
}
@import 'tailwindcss';
@import './theme.css';
@import './base.css';
@import './utilities.css';
@import './reset.css';
/* @import './element-theme.css'; */
/* @layer base {
} */
:root {
--el-color-primary: green;
}
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
position: fixed;
top: 0;
left: 0;
z-index: 1031;
width: 100%;
height: 2px;
background: var(--el-color-primary);
}
/* Fancy blur effect */
#nprogress .peg {
position: absolute;
right: 0;
display: block;
width: 100px;
height: 100%;
box-shadow:
0 0 10px #29d,
0 0 5px #29d;
opacity: 1;
transform: rotate(3deg) translate(0, -4px);
}
/* Remove these to get rid of the spinner */
#nprogress .spinner {
position: fixed;
top: 15px;
right: 15px;
z-index: 1031;
display: block;
}
#nprogress .spinner-icon {
box-sizing: border-box;
width: 18px;
height: 18px;
border: solid 2px transparent;
border-top-color: #29d;
border-left-color: #29d;
border-radius: 50%;
animation: nprogress-spinner 400ms linear infinite;
}
.nprogress-custom-parent {
position: relative;
overflow: hidden;
}
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
@keyframes nprogress-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes nprogress-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
html {
box-sizing: border-box;
width: 100%;
height: 100%;
}
body {
width: 100%;
height: 100%;
margin: 0;
}
#app {
width: 100%;
height: 100%;
}
@theme {
--color-theme-color: #00a2ea;
}
@utility flex-center {
@apply flex items-center justify-center;
}
export function isAllEmpty(value: string | number | any[] | AnyObject) {
if (!value && typeof value !== 'number') {
return false
}
switch (typeof value) {
case 'object': {
if (Array.isArray(value)) {
return value.length === 0
} else {
return Object.keys(value).length === 0
}
}
default:
return true
}
}
import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios'
import { BASE_URLS } from '@/config/base-url'
import { useUserStore } from '@/store/modules/user'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
interface PagingInfoParams {
pageNo: number
pageSize: number
}
export interface Response<T> {
data: T
message: string | null
code: number
pagingInfo?: PagingInfoParams & { totalPages: number; totalRows: number }
}
const ENV = import.meta.env.VITE_APP_ENV
function handleLogout() {
const router = useRouter()
const currentRoute = router.currentRoute.value
router.replace({ name: 'Login', query: { redirect: encodeURIComponent(currentRoute.fullPath) } })
ElMessage.warning('身份已过期,请重新登录')
}
const service = axios.create({
baseURL: `${BASE_URLS[ENV]}/api`,
timeout: 7000,
headers: {
'Content-Type': 'application/json',
},
})
service.interceptors.request.use(
(config) => {
const token = useUserStore().token
if (token) config.headers['X-Request-Token'] = token
return config
},
(error) => {
return Promise.reject(error.response)
},
)
service.interceptors.response.use(
(response: AxiosResponse) => {
if (response.status === 200) {
return response.data
}
throw new Error(response.status.toString())
},
(error) => {
const response = error.response
// 处理响应错误
if (response && [500, 502].includes(response.status) && window.location.hash === '#/500')
useRouter().push({ name: 'ServerError' })
return Promise.reject(error)
},
)
type RequestOptions = AxiosRequestConfig & { ignoreErrorCheck?: boolean }
function http<T>(
method: 'GET' | 'POST' = 'POST',
url: string,
payload?: { pagingInfo?: PagingInfoParams; [key: string]: any } | null,
options?: RequestOptions,
) {
/* 请求成功处理函数 */
const successHandler = (res: Response<T>): Promise<Response<T>> | Response<T> => {
if (res.code === 0 || options?.ignoreErrorCheck) return res
if (res.code === -10) {
handleLogout()
}
if (res.message) {
const msg = res.message.match(/\[(?<msg>[\s\S]+)\]/)?.groups?.msg
ElMessage.error(msg || res.message)
}
return Promise.reject(res)
}
/* 请求失败处理函数 */
const failHandler = (error: Error) => {
throw new Error(error?.message || 'Error')
}
// 分页参数携带
if (payload?.pagingInfo) {
const { pageNo, pageSize } = payload.pagingInfo
delete payload.pagingInfo
if (pageNo && pageSize) {
if (options) {
if (options.params) {
options.params = { ...options.params, ...{ pageNo, pageSize } }
} else {
options.params = { pageNo, pageSize }
}
} else {
options = { params: { pageNo, pageSize } }
}
}
}
return method === 'GET'
? service.get<any, Response<T>>(url, Object.assign(options || {}, payload || {})).then(successHandler, failHandler)
: service.post<any, Response<T>>(url, payload || null, options || {}).then(successHandler, failHandler)
}
export const request = {
get<T>(url: string, params?: { pagingInfo?: PagingInfoParams; [key: string]: any } | null, options?: RequestOptions) {
return http<T>('GET', url, params, options)
},
post<T>(
url: string,
payload?: { pagingInfo?: PagingInfoParams; [key: string]: any } | null,
options?: RequestOptions,
) {
return http<T>('POST', url, payload, options)
},
}
export enum STORAGE_KEYS {
TOKEN = 'TOKEN',
IS_LOGIN = 'IS_LOGIN',
USER_INFO = 'USER_INFO',
}
interface StorageData<T = any> {
data: T
expire: number | null
}
export function createLocalStorage(options?: { expire?: number | null }) {
const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7
const { expire } = Object.assign({ expire: DEFAULT_CACHE_TIME }, options)
function set<T = any>(key: string, data: T) {
const storageData: StorageData<T> = {
data,
expire: expire !== null ? new Date().getTime() + expire * 1000 : null,
}
const json = JSON.stringify(storageData)
window.localStorage.setItem(key, json)
}
function get(key: string) {
const json = window.localStorage.getItem(key)
if (json) {
let storageData: StorageData | null = null
try {
storageData = JSON.parse(json)
} catch {
// Prevent failure
}
if (storageData) {
const { data, expire } = storageData
if (expire === null || expire >= Date.now()) return data
}
remove(key)
return null
}
}
function remove(key: string) {
window.localStorage.removeItem(key)
}
function clear() {
window.localStorage.clear()
}
return { set, get, remove, clear }
}
export const ls = createLocalStorage()
export const ss = createLocalStorage({ expire: null })
import type { App } from 'vue'
import VueTippy from 'vue-tippy'
export function setupStore(app: App) {
app.use(VueTippy, {
defaultProps: { placement: 'right' },
})
}
/**
* @description 创建层级关系
* @param tree 树
* @param pathList 每一项的id组成的数组
* @returns 创建层级关系后的树
*/
export const buildHierarchyTree = (tree: any[], pathList = []): any => {
if (!Array.isArray(tree)) {
console.warn('tree must be an array')
return []
}
if (!tree || tree.length === 0) return []
for (const [key, node] of tree.entries()) {
node.id = key
node.parentId = pathList.length ? pathList[pathList.length - 1] : null
node.pathList = [...pathList, node.id]
const hasChildren = node.children && node.children.length > 0
if (hasChildren) {
buildHierarchyTree(node.children, node.pathList)
}
}
return tree
}
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
function handleToHomePage() {
router.replace({ name: 'Root' })
}
</script>
<template>
<div class="h-screen overflow-hidden">
<div class="absolute top-10 left-10">
<el-button type="primary" @click="handleToHomePage">返回首页</el-button>
</div>
<div class="flex h-full items-center justify-center">
<img
class="w-10/12 transition-[width] duration-200 ease-linear sm:w-5/12"
src="@/assets/svgs/404.svg"
alt="404"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
function handleToHomePage() {
router.replace({ name: 'Root' })
}
</script>
<template>
<div class="h-screen overflow-hidden">
<div class="absolute top-10 left-10">
<el-button type="primary" @click="handleToHomePage">返回首页</el-button>
</div>
<div class="flex h-full items-center justify-center">
<img
class="w-10/12 transition-[width] duration-200 ease-linear sm:w-5/12"
src="@/assets/svgs/500.svg"
alt="500"
/>
</div>
</div>
</template>
<script setup lang="ts"></script>
<template>
<div>首页内容区</div>
</template>
<script setup lang="ts"></script>
<template>
<div>登陆页面</div>
</template>
export default {
extends: ['stylelint-config-standard', 'stylelint-config-recess-order', 'stylelint-config-recommended-vue'],
plugins: ['stylelint-order'],
rules: {
'color-function-notation': 'legacy',
'font-family-name-quotes': null,
'font-family-no-missing-generic-family-keyword': null,
'alpha-value-notation': 'number',
'selector-class-pattern': null,
'at-rule-no-unknown': [
true,
{
ignoreAtRules: ['tailwind', 'apply', 'variants', 'utility', 'reference', 'theme'],
},
],
'import-notation': 'string',
'at-rule-no-deprecated': [
true,
{
ignoreAtRules: ['apply'],
},
],
'custom-property-pattern': null,
},
}
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@build/*": ["build/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "types/**/*.d.ts"]
}
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
]
}
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": true
},
"include": ["types/**/*.d.ts", "build/**/*.ts", "vite.config.ts"]
}
// declare interface Window {}
type Recordable<T = any> = Record<string, T>
declare type AnyObject = { [k: string]: any }
declare namespace I18n {
type LangType = 'zh-HK' | 'zh-CN'
type Schema = {
buttons: {
btnLogin: string
}
}
}
declare module '*.yaml' {
const value: I18n.Schema
export default value
}
import 'vue-router'
export {}
declare module 'vue-router' {
interface RouteMeta {
rank: number
title: string
icon?: string
}
}
/// <reference types="vite/client" />
declare interface ViteEnv {
readonly VITE_APP_ENV: 'DEV' | 'PROD'
readonly VITE_PORT: number
readonly VITE_PUBLIC_PATH: string
readonly VITE_ROUTER_MODE: 'hash' | 'h5'
readonly VITE_VITEST: boolean
readonly VITE_HIDE_HOME: boolean
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface ImportMetaEnv extends ViteEnv {}
interface ImportMeta {
readonly env: ImportMetaEnv
}
import { defineConfig, loadEnv } from 'vite'
import { resolve } from 'node:path'
import { wrapperEnv } from './build'
import { setupPlugins } from './build/plugins'
/** 当前执行node命令时文件夹的地址(工作目录) */
const root: string = process.cwd()
/** 路径查找 */
const pathResolve = (dir: string): string => {
return resolve(__dirname, '.', dir)
}
export default defineConfig(({ command, mode }) => {
const isBuild = command === 'build'
const envConf = wrapperEnv(loadEnv(mode, root))
return {
base: envConf.VITE_PUBLIC_PATH,
resolve: {
alias: {
'@': pathResolve('src'),
'@build': pathResolve('build'),
},
},
plugins: setupPlugins(isBuild, envConf, pathResolve),
server: {
host: true,
port: envConf.VITE_PORT,
},
}
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment