Commit 9e872a5d authored by tyyin lan's avatar tyyin lan

build: init project

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_NAME = 'DIGITAL_PERSON_FE'
VITE_APP_THEME_COLOR = '#2468f2'
VITE_PORT = 8848
VITE_PUBLIC_PATH = /
VITE_ROUTER_MODE = 'hash'
VITE_VITEST = true
VITE_HIDE_HOME = false
VITE_APP_ENV = 'PROD'
# 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 exec commitlint --edit $1
npm run lint:lint-staged
node_modules
build
dist
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"EditorConfig.EditorConfig",
"bradlc.vscode-tailwindcss",
"stylelint.vscode-stylelint",
"redhat.vscode-yaml",
"antfu.unocss"
],
"unwantedRecommendations": ["bradlc.vscode-tailwindcss"]
}
{
"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"
},
}
\ No newline at end of file
/** 处理环境变量 */
export const wrapperEnv = (envConf: Recordable): ViteEnv => {
const ret: ViteEnv = {
VITE_APP_ENV: 'DEV',
VITE_APP_NAME: 'DIGITAL_PERSON_FE',
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 { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
import UnoCSS from 'unocss/vite'
export function setupPlugins(isBuild: boolean, envConf: ViteEnv): PluginOption[] {
const lifecycle = process.env.npm_lifecycle_event
const plugins: PluginOption = [
vue(),
AutoImport({
imports: [
{
'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar'],
},
],
}),
Components({
resolvers: [NaiveUiResolver()],
}),
UnoCSS(),
]
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'
import unocss from '@unocss/eslint-config/flat'
export default [
unocss,
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',
'@unocss/order': 'off',
},
},
eslintPluginPrettierRecommended,
{
ignores: ['dist/', 'public/'],
},
]
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- <title>DIGITAL_PERSON-FE</title> -->
<title>DIGITAL_PERSON_FE</title>
</head>
<body>
<div id="app">
<style>
.spinner-wrapper {
display: flex;
justify-content: center;
padding-top: 40vh;
}
.boxes {
--size: 32px;
--duration: 800ms;
height: calc(var(--size) * 2);
width: calc(var(--size) * 3);
position: relative;
transform-style: preserve-3d;
transform-origin: 50% 50%;
margin-top: calc(var(--size) * 1.5 * -1);
transform: rotateX(60deg) rotateZ(45deg) rotateY(0deg) translateZ(0px);
}
.boxes .box {
width: var(--size);
height: var(--size);
top: 0;
left: 0;
position: absolute;
transform-style: preserve-3d;
}
.boxes .box:nth-child(1) {
transform: translate(100%, 0);
-webkit-animation: box1 var(--duration) linear infinite;
animation: box1 var(--duration) linear infinite;
}
.boxes .box:nth-child(2) {
transform: translate(0, 100%);
-webkit-animation: box2 var(--duration) linear infinite;
animation: box2 var(--duration) linear infinite;
}
.boxes .box:nth-child(3) {
transform: translate(100%, 100%);
-webkit-animation: box3 var(--duration) linear infinite;
animation: box3 var(--duration) linear infinite;
}
.boxes .box:nth-child(4) {
transform: translate(200%, 0);
-webkit-animation: box4 var(--duration) linear infinite;
animation: box4 var(--duration) linear infinite;
}
.boxes .box > div {
--background: #5c8df6;
--top: auto;
--right: auto;
--bottom: auto;
--left: auto;
--translateZ: calc(var(--size) / 2);
--rotateY: 0deg;
--rotateX: 0deg;
position: absolute;
width: 100%;
height: 100%;
background: var(--background);
top: var(--top);
right: var(--right);
bottom: var(--bottom);
left: var(--left);
transform: rotateY(var(--rotateY)) rotateX(var(--rotateX)) translateZ(var(--translateZ));
}
.boxes .box > div:nth-child(1) {
--top: 0;
--left: 0;
}
.boxes .box > div:nth-child(2) {
--background: #145af2;
--right: 0;
--rotateY: 90deg;
}
.boxes .box > div:nth-child(3) {
--background: #447cf5;
--rotateX: -90deg;
}
.boxes .box > div:nth-child(4) {
--background: #dbe3f4;
--top: 0;
--left: 0;
--translateZ: calc(var(--size) * 3 * -1);
}
@-webkit-keyframes box1 {
0%,
50% {
transform: translate(100%, 0);
}
100% {
transform: translate(200%, 0);
}
}
@keyframes box1 {
0%,
50% {
transform: translate(100%, 0);
}
100% {
transform: translate(200%, 0);
}
}
@-webkit-keyframes box2 {
0% {
transform: translate(0, 100%);
}
50% {
transform: translate(0, 0);
}
100% {
transform: translate(100%, 0);
}
}
@keyframes box2 {
0% {
transform: translate(0, 100%);
}
50% {
transform: translate(0, 0);
}
100% {
transform: translate(100%, 0);
}
}
@-webkit-keyframes box3 {
0%,
50% {
transform: translate(100%, 100%);
}
100% {
transform: translate(0, 100%);
}
}
@keyframes box3 {
0%,
50% {
transform: translate(100%, 100%);
}
100% {
transform: translate(0, 100%);
}
}
@-webkit-keyframes box4 {
0% {
transform: translate(200%, 0);
}
50% {
transform: translate(200%, 100%);
}
100% {
transform: translate(100%, 100%);
}
}
@keyframes box4 {
0% {
transform: translate(200%, 0);
}
50% {
transform: translate(200%, 100%);
}
100% {
transform: translate(100%, 100%);
}
}
</style>
<div class="spinner-wrapper">
<div class="boxes">
<div class="box">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div class="box">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div class="box">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div class="box">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</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": "digital-person-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": {
"@iconify/vue": "^4.1.2",
"@unocss/reset": "^0.61.9",
"@vueuse/core": "^10.11.1",
"axios": "^1.7.7",
"nanoid": "^5.0.7",
"pinia": "^2.2.2",
"vue": "^3.5.3",
"vue-router": "^4.4.3"
},
"devDependencies": {
"@commitlint/cli": "^19.4.1",
"@commitlint/config-conventional": "^19.4.1",
"@commitlint/types": "^19.0.3",
"@types/node": "^20.16.5",
"@typescript-eslint/parser": "^7.18.0",
"@unocss/eslint-config": "^0.61.9",
"@vitejs/plugin-vue": "^4.6.2",
"autoprefixer": "^10.4.20",
"eslint": "^9.9.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.28.0",
"globals": "^15.9.0",
"husky": "^9.1.5",
"lint-staged": "^15.2.10",
"naive-ui": "^2.39.0",
"postcss": "^8.4.45",
"postcss-html": "^1.7.0",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.78.0",
"stylelint": "^16.9.0",
"stylelint-config-recess-order": "^4.6.0",
"stylelint-config-recommended-scss": "^14.1.0",
"stylelint-config-recommended-vue": "^1.5.0",
"stylelint-config-standard": "^36.0.1",
"stylelint-config-standard-scss": "^13.1.0",
"stylelint-order": "^6.0.4",
"typescript": "^5.5.4",
"typescript-eslint": "^7.18.0",
"unocss": "^0.61.9",
"unplugin-auto-import": "^0.17.8",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.4.3",
"vite-plugin-checker": "^0.7.2",
"vue-eslint-parser": "^9.4.3",
"vue-tsc": "~2.0.29"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0",
"pnpm": ">=9"
}
}
This diff is collapsed.
export default {
plugins: {
autoprefixer: {},
},
}
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 { zhCN, dateZhCN } from 'naive-ui'
import { ref } from 'vue'
import { themeOverrides } from '@/config/theme-config'
import { useResizeObserver } from '@vueuse/core'
import { useDesignSettingStore } from '@/store/modules/design-setting'
// import { NThemeEditor } from 'naive-ui'
const designSettingStore = useDesignSettingStore()
const currentLocale = ref({
zhCN,
})
const currentDateLocale = ref(dateZhCN)
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">
<NConfigProvider
class="h-full w-full"
:locale="currentLocale"
:date-locale="currentDateLocale"
:theme-overrides="themeOverrides"
>
<RouterView v-slot="{ Component }">
<Component :is="Component" />
</RouterView>
</NConfigProvider>
</div>
<!-- <NThemeEditor /> -->
</template>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<script setup lang="ts">
import { Icon } from '@iconify/vue'
defineProps<{
icon: string
}>()
</script>
<template>
<Icon :icon="icon" />
</template>
export const appConfig = {
title: '管理后台',
}
export const BASE_URLS: Record<'DEV' | 'PROD', string> = {
DEV: '',
PROD: '',
}
import { GlobalThemeOverrides } from 'naive-ui'
export const themeOverrides: GlobalThemeOverrides = {
common: {
primaryColor: '#2468f2',
primaryColorHover: '#45a9bb',
primaryColorPressed: '#398c9b',
primaryColorSuppl: '#45a9bb',
},
LoadingBar: {
colorLoading: '#2468f2',
},
}
<script setup lang="ts">
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { useDesignSettingStore } from '@/store/modules/design-setting'
import { useUserStore } from '@/store/modules/user'
import { readonly } from 'vue'
import { useRouter } from 'vue-router'
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 avatarOptions = readonly([
{
label: '退出登录',
key: 1,
},
])
function handleSidebarDisplayStatusChange(status: 'collapse' | 'expand') {
if (designSettingStore.sidebarDisplayStatus === 'hidden') {
designSettingStore.changeShowSidebarDrawer(true)
return
}
designSettingStore.toggleSidebarDisplayStatus(status)
}
function handleDropdownSelect(key: number) {
if (key === 1) {
userStore.logout().then(() => {
router.push({ name: 'Login' })
})
}
}
// function handleRefreshPage() {
// router.replace({ path: currentRoute.fullPath })
// }
</script>
<template>
<div class="flex select-none justify-between bg-white shadow-[0_1px_4px_rgba(0,21,41,0.08)]">
<div class="flex">
<div class="flex items-center px-3">
<CustomIcon
v-show="designSettingStore.sidebarDisplayStatus !== 'expand'"
class="h-5 w-5 cursor-pointer"
icon="ant-design:menu-unfold-outlined"
@click="handleSidebarDisplayStatusChange('expand')"
/>
<CustomIcon
v-show="designSettingStore.sidebarDisplayStatus === 'expand'"
class="h-5 w-5 cursor-pointer"
icon="ant-design:menu-fold-outlined"
@click="handleSidebarDisplayStatusChange('collapse')"
/>
</div>
<!-- <div class="flex items-center px-3">
<CustomIcon icon="mdi:reload" class="h-5 w-5 cursor-pointer" @click="handleRefreshPage" />
</div> -->
</div>
<div class="mr-5 flex sm:mr-6">
<div class="flex cursor-pointer items-center px-2">
<NDropdown trigger="click" :options="avatarOptions" @select="handleDropdownSelect">
<div class="flex h-full items-center">
<NAvatar round :size="30" object-fit="cover" :src="userStore.userInfo.avatar || defaultAvatar" />
<div class="ml-2 max-w-24 truncate text-base">{{ userStore.userInfo.userName }}</div>
</div>
</NDropdown>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useDesignSettingStore } from '@/store/modules/design-setting'
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('@/assets/images/logo.png')] bg-cover"></div>
<Transition name="logo">
<div v-show="designSettingStore.sidebarDisplayStatus !== 'collapse'" class="ml-2 truncate text-lg font-semibold">
后台管理系统
</div>
</Transition>
</div>
</template>
<style lang="scss" scoped>
// .logo-enter-active,
// .logo-leave-active {
// transition: opacity 0.09s ease-in;
// }
// .logo-enter-from,
// .logo-leave-to {
// opacity: 0;
// }
</style>
<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'
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' })
}
</script>
<template>
<SidebarLogo @click="handleLogoClick" />
<NMenu
:options="sidebarMenus"
:collapsed-width="64"
:collapsed-icon-size="20"
:indent="24"
:icon-size="20"
:collapsed="designSettingStore.sidebarDisplayStatus === 'collapse'"
:value="menuValue"
@update:value="handleUpdateValue"
/>
</template>
<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'
const designSettingStore = useDesignSettingStore()
const layoutSideWidth = 210
function handleLayoutSideCollapsedChange(collapsed: boolean) {
designSettingStore.toggleSidebarDisplayStatus(collapsed ? 'collapse' : 'expand')
}
function handleDrawerMackClick() {
designSettingStore.changeShowSidebarDrawer(false)
}
</script>
<template>
<NLayout has-sider position="absolute">
<NLayoutSider
v-show="designSettingStore.sidebarDisplayStatus !== 'hidden'"
class="shadow-[2px_0_8px_0_rgba(29,35,41,0.05)]"
show-trigger="bar"
collapse-mode="width"
:collapsed-width="64"
:collapsed="designSettingStore.sidebarDisplayStatus === 'collapse'"
:width="layoutSideWidth"
:on-update:collapsed="handleLayoutSideCollapsedChange"
>
<Sidebar />
</NLayoutSider>
<NDrawer
v-model:show="designSettingStore.showSidebarDrawer"
:width="layoutSideWidth"
:close-on-esc="false"
placement="left"
display-directive="show"
:on-mask-click="handleDrawerMackClick"
>
<Sidebar />
</NDrawer>
<NLayout content-class="layout-content">
<NLayoutHeader class="box-border h-[64px] flex-none">
<NavBar class="h-full" />
</NLayoutHeader>
<NLayoutContent class="main-content-wrapper flex-1">
<main class="box-border min-h-full p-4">
<RouterView v-slot="{ Component }">
<Transition appear name="fade-slide" mode="out-in">
<Component :is="Component" />
</Transition>
</RouterView>
</main>
<!-- <NLayoutFooter bordered>版权</NLayoutFooter> -->
</NLayoutContent>
</NLayout>
</NLayout>
</template>
<style lang="scss" scoped>
:deep(.layout-content) {
@apply flex flex-col bg-[#f5f7f9];
}
:deep(.main-content-wrapper) {
@apply bg-[#f5f7f9];
// ::-webkit-scrollbar {
// width: 8px;
// height: 8px;
// }
// ::-webkit-scrollbar-track {
// width: 6px;
// background: rgba(#101f1c, 0.1);
// border-radius: 2em;
// }
// ::-webkit-scrollbar-thumb {
// cursor: pointer;
// background-color: rgba(144, 147, 153, 20%);
// background-clip: padding-box;
// // min-height: 28px;
// border-radius: 2em;
// transition: background-color 0.3s;
// }
// ::-webkit-scrollbar-thumb:hover {
// background-color: rgba(144, 147, 153, 30%);
// }
}
.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 { createApp } from 'vue'
import { setupStore } from './store/'
import { setupRouter } from './router'
import { setupDiscrete } from '@/utils/create-discrete'
import App from './app.vue'
import '@/styles/reset.scss'
import 'virtual:uno.css'
import '@unocss/reset/normalize.css'
import '@unocss/reset/tailwind.css'
async function bootstrap() {
const app = createApp(App)
setupDiscrete()
setupStore(app)
await setupRouter(app)
// 解决潜在的样式冲突
const meta = document.createElement('meta')
meta.name = 'naive-ui-style'
document.head.appendChild(meta)
app.mount('#app')
}
bootstrap()
import type { Router } from 'vue-router'
import { useUserStore } from '@/store/modules/user'
/** 路由白名单 */
const whitePathList = ['/login']
export function createRouterGuards(router: Router) {
router.beforeEach((to) => {
window.$loadingBar.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 = ''
} else if (to.meta.title) {
document.title = `${import.meta.env.VITE_APP_NAME}-${to.meta.title}`
} else {
document.title = import.meta.env.VITE_APP_NAME
}
window.$loadingBar.finish()
})
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) => {
Array.isArray(routesItem) ? routes.push(...routesItem) : 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: any[] = 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: 'material-symbols:home-outline',
// },
// component: Home,
// },
// ],
// },
// ] as RouteRecordRaw[]
export default [
{
path: '/',
name: 'Root',
meta: {
rank: 1001,
title: '首页',
},
component: Home,
},
] as RouteRecordRaw[]
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
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
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) => {
route.meta ? (route.meta.key = nanoid()) : (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(CustomIcon, { icon: (route?.meta?.icon as string) || '' }) : null,
}
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,
}),
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 { TOKEN, IS_LOGIN, USER_INFO } 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(IS_LOGIN),
token: ss.get(TOKEN) || '',
userInfo: ss.get(USER_INFO) || getDefaultUserInfo(),
}),
actions: {
async logout() {
this.isLogin = false
this.token = ''
this.userInfo = getDefaultUserInfo()
ss.remove(IS_LOGIN)
ss.remove(TOKEN)
ss.remove(USER_INFO)
},
updateIsLogin(status: boolean) {
this.isLogin = status
ss.set(IS_LOGIN, status)
},
updateToken(token: string) {
this.token = token
ss.set(TOKEN, token)
},
updateUserInfo(userInfo: UserInfo) {
this.userInfo = userInfo
ss.set(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 './variable';
@import './mixin';
html {
box-sizing: border-box;
width: 100%;
height: 100%;
}
body {
width: 100%;
height: 100%;
margin: 0;
}
#app {
width: 100%;
height: 100%;
}
import { createDiscreteApi } from 'naive-ui'
import type { DialogApiInjection, DialogOptions, DialogReactive } from 'naive-ui/es/dialog/src/DialogProvider'
/* 拦截一层添加默认配置 */
function createDialogProvider(dialog: DialogApiInjection): DialogApiInjection {
/* 默认配置 */
const defaultConfig: DialogOptions = {
style: 'margin-top: 180px',
}
return {
destroyAll() {
dialog.destroyAll()
},
create(options: DialogOptions): DialogReactive {
return dialog.create(Object.assign(defaultConfig, options))
},
success(options: DialogOptions): DialogReactive {
return dialog.success(Object.assign(defaultConfig, options))
},
warning(options: DialogOptions): DialogReactive {
return dialog.warning(Object.assign(defaultConfig, options))
},
error(options: DialogOptions): DialogReactive {
return dialog.error(Object.assign(defaultConfig, options))
},
info(options: DialogOptions): DialogReactive {
return dialog.info(Object.assign(defaultConfig, options))
},
}
}
export function setupDiscrete() {
const { message, notification, loadingBar, dialog } = createDiscreteApi(
['message', 'dialog', 'notification', 'loadingBar'],
{
configProviderProps: {
themeOverrides: {
LoadingBar: {
colorLoading: import.meta.env.VITE_APP_THEME_COLOR,
},
},
},
},
)
window.$message = message
window.$notification = notification
window.$loadingBar = loadingBar
window.$dialog = createDialogProvider(dialog)
}
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'
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) } })
window.$message.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
window.$message.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)
options
? options.params
? (options.params = { ...options.params, ...{ pageNo, pageSize } })
: (options.params = { pageNo, pageSize })
: (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 const TOKEN = 'TOKEN'
export const IS_LOGIN = 'IS_LOGIN'
export const 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 })
/**
* @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">
function toHomePage() {}
</script>
<template>
<div class="h-screen overflow-hidden">
<div class="absolute left-10 top-10">
<n-button type="primary" @click="toHomePage">返回首页</n-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">
function toHomePage() {}
</script>
<template>
<div class="h-screen overflow-hidden">
<div class="absolute left-10 top-10">
<n-button type="primary" @click="toHomePage">返回首页</n-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">
import Layout from './layout/index.vue'
</script>
<template>
<Layout />
</template>
<script setup lang="ts"></script>
<template>
<header class="flex h-[56px] justify-between">
<div class="font-600 flex items-center pl-4 text-xl">萃想智能云创</div>
<div class="flex items-center pr-4">
<div class="mx-6">
<n-button class="!rounded-[6px]" color="#ffecd4" text-color="#151b26">
<template #icon><CustomIcon class="text-lg" icon="mingcute:pig-money-line" /></template>
<span class="text-[14px]">充值</span>
</n-button>
</div>
<n-avatar
class="cursor-pointer"
round
size="small"
object-fit="cover"
src="https://mkp-dev.oss-cn-shenzhen.aliyuncs.com/game-template/20230103/1672717001352.png"
/>
</div>
</header>
</template>
<script setup lang="ts">
import HeaderBar from './header-bar.vue'
import SideBar from './side-bar.vue'
import MainContent from './main-content.vue'
</script>
<template>
<div class="h-screen bg-[#f3f4fb]">
<div class="mx-auto h-full max-w-[1440px]">
<n-layout content-class="layout-wrapper-content" class="h-full !bg-transparent">
<n-layout-header class="mb-[16px] !bg-transparent">
<HeaderBar />
</n-layout-header>
<n-layout has-sider class="mb-[16px] flex-1 !bg-transparent">
<n-layout-sider class="!bg-transparent">
<SideBar />
</n-layout-sider>
<n-layout-content class="rounded-[16px]">
<MainContent />
</n-layout-content>
</n-layout>
</n-layout>
</div>
</div>
</template>
<style lang="scss" scoped>
:deep(.layout-wrapper-content) {
@apply flex flex-col;
}
</style>
<script setup lang="ts">
import RecentCreation from '../components/recent-creation.vue'
</script>
<template>
<main class="p-[24px]">
<div>
<RecentCreation />
</div>
</main>
</template>
<script setup lang="ts">
import { shallowReadonly } from 'vue'
import type { MenuOption } from 'naive-ui'
const menuOptions = shallowReadonly<MenuOption[]>([
{
type: 'group',
label: '视频',
key: 'videos',
children: [
{
label: '工作台',
key: 'workbench',
},
],
},
])
</script>
<template>
<section class="h-full pr-[24px]">
<div class="h-full rounded-[16px] bg-white p-[16px]">
<n-menu value="workbench" :options="menuOptions" />
</div>
</section>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { appConfig } from '@/config/app-config'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { FormInst } from 'naive-ui'
// import { fetchLogin } from '@/apis/user'
import { useUserStore } from '@/store/modules/user'
// import type { UserInfo } from '@/store/types/user'
import { useRouter } from 'vue-router'
const userStore = useUserStore()
const router = useRouter()
const loginForm = ref({
username: '',
password: '',
})
const loginBtnLoading = ref(false)
const loginFormRef = ref<FormInst | null>(null)
const loginFormRules = {
username: { required: true, message: '请输入用户名', trigger: 'blur' },
password: { required: true, message: '请输入密码', trigger: 'blur' },
}
function handleLogin(e: MouseEvent) {
e.preventDefault()
loginFormRef.value?.validate((errors) => {
if (!errors) {
// loginBtnLoading.value = true
userStore.updateIsLogin(true)
userStore.updateToken('res.data.token')
const userInfoRes = {
account: 'account',
userAccount: 'userAccount',
userId: 134,
userName: 'userName',
createdTime: 'createdTime',
}
userStore.updateUserInfo({
account: userInfoRes.account,
userAccount: userInfoRes.userAccount,
userId: userInfoRes.userId,
userName: userInfoRes.userName,
createdTime: userInfoRes.createdTime,
avatar: '',
})
const currentRoute = router.currentRoute.value
const redirectUrl = decodeURIComponent((currentRoute.query.redirect as string) || '')
if (redirectUrl) {
router.replace({ path: redirectUrl })
return
}
router.replace({ name: 'Root' })
// fetchLogin<{ token: string; user: UserInfo }>(loginForm.value)
// .then((res) => {
// if (res.code !== null && res.code !== 0) {
// window.$message.error(res.message || '登录失败请重试')
// return
// }
// userStore.updateIsLogin(true)
// userStore.updateToken(res.data.token)
// const userInfoRes = res.data.user || {}
// userStore.updateUserInfo({
// account: userInfoRes.account,
// userAccount: userInfoRes.userAccount,
// userId: userInfoRes.userId,
// userName: userInfoRes.userName,
// createdTime: userInfoRes.createdTime,
// avatar: '',
// })
// const currentRoute = router.currentRoute.value
// const redirectUrl = decodeURIComponent((currentRoute.query.redirect as string) || '')
// if (redirectUrl) {
// router.replace({ path: redirectUrl })
// return
// }
// router.replace({ name: 'Root' })
// })
// .catch((err) => {
// console.log(err)
// })
// .finally(() => {
// loginBtnLoading.value = false
// })
}
})
}
</script>
<template>
<div class="bg-svg-login-bg h-full w-full">
<div class="fixed left-1/2 top-1/3 w-[90%] -translate-x-1/2 -translate-y-1/3 rounded-lg bg-white p-6 sm:w-[410px]">
<div class="mb-6 flex justify-center sm:mb-7">
<div class="bg-px-logo-png h-24 w-36"></div>
</div>
<div class="mb-8 text-center text-2xl font-bold text-[#999] outline-none sm:mb-10 sm:text-2xl">
{{ appConfig.title }}
</div>
<div>
<NForm ref="loginFormRef" label-placement="left" size="large" :model="loginForm" :rules="loginFormRules">
<NFormItem path="username">
<NInput v-model:value="loginForm.username" placeholder="请输入用户名">
<template #prefix>
<CustomIcon class="h-5 w-5 text-[#868686]" icon="material-symbols:person-outline" />
</template>
</NInput>
</NFormItem>
<NFormItem path="password">
<NInput
v-model:value="loginForm.password"
type="password"
show-password-on="click"
placeholder="请输入密码"
>
<template #prefix>
<CustomIcon class="h-5 w-5 text-[#868686]" icon="mdi:lock-outline" />
</template>
</NInput>
</NFormItem>
<NFormItem class="mt-4">
<NButton
type="primary"
size="large"
block
:loading="loginBtnLoading"
:disabled="!loginForm.username || !loginForm.password"
@click="handleLogin"
>
登录
</NButton>
</NFormItem>
</NForm>
</div>
</div>
</div>
</template>
export default {
extends: [
'stylelint-config-standard',
'stylelint-config-recess-order',
'stylelint-config-standard-scss',
'stylelint-config-recommended-vue/scss',
],
plugins: ['stylelint-order'],
rules: {
'color-function-notation': 'legacy',
'scss/at-rule-no-unknown': [
true,
{
ignoreAtRules: [
'tailwind',
'apply',
'variants',
'responsive',
'screen',
'function',
'if',
'each',
'include',
'mixin',
'use',
],
},
],
'font-family-name-quotes': null,
'font-family-no-missing-generic-family-keyword': null,
'scss/at-import-partial-extension': 'always',
'alpha-value-notation': 'number',
},
}
{
"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 {
$loadingBar: import('naive-ui').LoadingBarProviderInst
$dialog: import('naive-ui').DialogProviderInst
$message: import('naive-ui').MessageProviderInst
$notification: import('naive-ui').NotificationProviderInst
}
type Recordable<T = any> = Record<string, T>
declare type AnyObject = { [k: string]: any }
import 'vue-router'
export {}
declare module 'vue-router' {
interface RouteMeta {
rank: number
title: string
}
}
/// <reference types="vite/client" />
declare interface ViteEnv {
readonly VITE_APP_ENV: 'DEV' | 'PROD'
readonly VITE_APP_NAME: string
readonly VITE_PORT: number
readonly VITE_PUBLIC_PATH: string
readonly VITE_ROUTER_MODE: 'hash' | 'h5'
readonly VITE_VITEST: boolean
readonly VITE_HIDE_HOME: boolean
}
interface ImportMetaEnv extends ViteEnv {}
interface ImportMeta {
readonly env: ImportMetaEnv
}
import { defineConfig, transformerDirectives } from 'unocss'
export default defineConfig({
transformers: [transformerDirectives()],
rules: [
[
/^bg-svg-([\w-]+)$/,
([, fname]) => ({ 'background-image': `url(@/assets/svgs/${fname}.svg)`, 'background-size': 'cover' }),
],
[
/^bg-px-([\w-]+)-(png|jpg|gif)$/,
([, fname, suffix]) => ({
'background-image': `url(@/assets/images/${fname}.${suffix})`,
'background-size': 'cover',
}),
],
],
})
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),
server: {
host: true,
port: envConf.VITE_PORT,
},
css: {
preprocessorOptions: {
scss: {
additionalData: '@use "@/styles/index.scss" as *;',
},
},
},
}
})
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