修改文件结构

This commit is contained in:
编码猿
2025-09-16 22:46:14 +08:00
parent 715d3ff0bb
commit c3d4b7e1e4
34 changed files with 55 additions and 6 deletions

View File

@@ -0,0 +1,84 @@
## 问 & 答
问: 为什么vue项目要抛弃SFC写法而尝试jsx/tsx方式来编码
https://juejin.cn/post/6911175470255964174
问:不会 vue3 jsx 语法,怎么办?
https://juejin.cn/post/7141674726434439176
vue3 jsx defineComponent 语法,实际项目中 setup 代码太长,怎么办?
答:使用组合式函数 (Composables) - 最推荐
案例:
```jsx
// composables/useUserData.js
import { ref, onMounted } from 'vue'
import api from '@/api'
export function useUserData(userId) {
const user = ref(null)
const loading = ref(false)
const error = ref(null)
const fetchUser = async () => {
loading.value = true
try {
user.value = await api.getUser(userId)
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
onMounted(fetchUser)
return {
user,
loading,
error,
refetch: fetchUser
}
}
```
主组件:
```jsx
// UserProfile.jsx
import { defineComponent } from 'vue'
import { useUserData } from '@/composables/useUserData'
export default defineComponent({
name: 'UserProfile',
props: {
userId: {
type: String,
required: true
}
},
setup(props) {
const { user, loading, error, refetch } = useUserData(props.userId)
return () => (
<div>
{loading.value && <div>Loading...</div>}
{error.value && <div>Error: {error.value}</div>}
{user.value && (
<div>
<h2>{user.value.name}</h2>
<p>{user.value.email}</p>
</div>
)}
</div>
)
}
})
```

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/app/main.ts"></script>
</body>
</html>

2903
front-end/main-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,35 @@
{
"name": "vue-project",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build"
},
"dependencies": {
"axios": "^1.12.2",
"pinia": "^3.0.3",
"vue": "^3.5.18",
"vue-router": "^4.0.6"
},
"devDependencies": {
"@originjs/vite-plugin-federation": "^1.4.1",
"@tsconfig/node22": "^22.0.2",
"@types/node": "^22.16.5",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^5.0.1",
"@vue/tsconfig": "^0.7.0",
"npm-run-all2": "^8.0.4",
"typescript": "~5.8.0",
"vite": "^7.0.6",
"vite-plugin-vue-devtools": "^8.0.0",
"vue-tsc": "^3.0.4"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,17 @@
import { defineComponent } from 'vue'
import { RouterLink, RouterView } from "vue-router";
export default defineComponent({
name: 'App',
setup() {
return () => (
<>
<div id="nav">
<RouterLink to="/"></RouterLink> |
<RouterLink to="/about"></RouterLink>
</div>
<RouterView></RouterView>
</>
)
}
})

View File

@@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

View File

@@ -0,0 +1,54 @@
import { defineComponent, defineEmits, onMounted} from 'vue'
import type { JSX } from "vue/jsx-runtime";
export default defineComponent({
name: "Header",
props: {
text: {
type: Array<{ id: number, title: string }>,
default: () => []
}
},
emits: {
sendMessage: (message: string) => typeof message === 'string'
},
setup(props, { attrs, slots, emit, expose }) {
onMounted(() => {
console.log("onMounted")
})
const handleClick = () => {
console.log("handleClick")
emit('sendMessage', '子给父的数据')
}
const button = (): JSX.Element => {
return <button onClick={handleClick}>emit点我</button>
}
return () => (
<div>
<h1></h1>
{ button() }
{ slots.default?.() }
{ slots.header?.() }
<div class="ScopedSlots">
<p></p>
<ul>
{
props.text.map(item => {
return slots.scoped?.(item)
})
}
</ul>
</div>
</div>
)
}
})

View File

@@ -0,0 +1,13 @@
// import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App'
import router from './router'
createApp(App)
.use(createPinia())
.use(router)
.mount('#app')

View File

@@ -0,0 +1,24 @@
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: () => import('../views/Home'),
},
{
path: '/about',
name: 'about',
component: () => import('../views/About'),
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('../views/NotFound')
}
],
})
export default router

View File

@@ -0,0 +1,12 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

View File

@@ -0,0 +1,12 @@
import { defineComponent } from 'vue'
export default defineComponent({
name: "About",
setup() {
return () => (
<div class="about">
<h1></h1>
</div>
)
}
})

View File

@@ -0,0 +1,60 @@
import { defineComponent, reactive, onMounted } from 'vue'
import Header from '@/components/Header'
import { HttpConfig } from '@shared/config/index'
import { UrlCheckUtils } from '@shared/utils'
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
interface stateType {
list: ListItemType[]
}
interface ListItemType {
id: number
title: string
}
export default defineComponent({
name: "Home",
setup() {
const counterStore = useCounterStore()
const { increment } = counterStore
const { count, doubleCount } = storeToRefs(counterStore)
const state = reactive<stateType>({
list: [
{ id: 1, title: '新闻1' },
{ id: 2, title: '新闻2' },
{ id: 3, title: '新闻3' },
]
})
onMounted(() => {
// console.log("onMounted: ", HttpConfig.DevBaseUrl);
console.log("check: ", UrlCheckUtils.check())
})
// 定义处理子组件事件的函数
const handleChildEvent = (message: string) => {
console.log('收到子组件事件:', message)
}
return () => (
<div class="home">
<Header text={state.list} onSendMessage={handleChildEvent}>
{{
scoped: (item: ListItemType) => <li key={item.id}>{item.title}</li>,
header: () => <div class="header"> </div>,
default: () => <div class="default"> default </div>
}}
</Header>
<h1></h1>
<div class="count" onClick={increment}>+1{count.value}</div>
</div>
)
}
})

View File

@@ -0,0 +1,11 @@
import { defineComponent } from 'vue'
export default defineComponent({
setup() {
return () => (
<div class="404">
<h1>404</h1>
</div>
)
}
})

View File

@@ -0,0 +1,5 @@
export default class HttpConfig {
public static DevBaseUrl: string = "http://127.0.0.1:8080"
public static prodBaseUrl: string = "http://127.0.0.1:9090/api"
public static apiList: string = ""
}

View File

@@ -0,0 +1,4 @@
export default class ThemeConfig {
public static primaryColor: string = '#1890ff'; // 主色调
public static layoutType: 'side' | 'top' = 'side'; // 侧边栏布局
}

View File

@@ -0,0 +1,4 @@
import HttpConfig from './HttpConfig';
import ThemeConfig from './ThemeConfig';
export { HttpConfig, ThemeConfig };

View File

@@ -0,0 +1,93 @@
import axios, { type AxiosInstance, AxiosError, type AxiosRequestConfig, type InternalAxiosRequestConfig } from "axios";
import { UrlCheckUtils } from '@shared/utils/index'
class AxiosInfra {
public instance: AxiosInstance;
public constructor() {
this.instance = axios.create({
baseURL: UrlCheckUtils.check(),
timeout: 10000,
headers: {
"Content-Type": "application/json"
}
});
this.ResponseInterceptor();
this.RequestInterceptor();
}
public async get(params: AxiosRequestConfig): Promise<any> {
return await this.instance.get(<string>params.url, {
params: params.data,
headers: Object.assign({ Authorization: localStorage.getItem("token") }, params.headers)
});
}
public async post(params: AxiosRequestConfig): Promise<any> {
return await this.instance.post(<string>params.url, params.data, {
headers: Object.assign({ Authorization: localStorage.getItem("token") }, params.headers)
})
}
/**
* Put 请求
* 请求参数请参考接口RequestParams
* @param params
*/
public async put(params: AxiosRequestConfig): Promise<any> {
return await this.instance.put(<string>params.url, params.data)
}
/**
* delete 请求
* 请求参数请参考接口RequestParams
* @param params
*/
public async delete(params: AxiosRequestConfig): Promise<any> {
return await this.instance.delete(<string>params.url, { params: params.data })
}
/**
* 响应拦截器
* @constructor
*/
public async ResponseInterceptor(): Promise<any> {
this.instance.interceptors.response.use(
(response: any) => {
return response.data.result;
},
(error: AxiosError) => {
console.log(`
❌ 请求错误 ❌
1、基地址: ${error.config?.baseURL}
2、短地址: ${error.config?.url}
3、请求 方式: ${error.config?.method}
4、请求 参数: ${JSON.stringify(error.config?.params)}
5、请求 头 : ${JSON.stringify(error.config?.headers)}
------------------------------------------------------------
6、错误信息为: ${error.message}
7、错误代码为: ${error.stack}
`);
return Promise.reject(error)
})
}
/**
* 添加请求拦截器
* @constructor
*/
public async RequestInterceptor(): Promise<any> {
this.instance.interceptors.request.use((config: any) => {
return config
}, function (error: AxiosError) {
return Promise.reject(error)
})
}
}
export default new AxiosInfra();

View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,4 @@
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}

View File

@@ -0,0 +1,12 @@
import { HttpConfig } from '@shared/config/index'
export default class UrlCheckUtils {
public static check() {
if (import.meta.env.MODE === 'production') {
return HttpConfig.prodBaseUrl;
} else {
return HttpConfig.DevBaseUrl;
}
}
}

View File

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

View File

@@ -0,0 +1,27 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": [
"src/shared/types/*.d.ts",
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*",
"src/**/*.tsx",
"src/**/*.vue"
],
"exclude": [
"src/**/__tests__/*"
],
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "vue",
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": {
"@/*": [
"./src/app/*"
],
"@shared/*": [
"./src/shared/*"
]
}
}
}

View File

@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

View File

@@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

View File

@@ -0,0 +1,21 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src/app/', import.meta.url)),
'@shared': fileURLToPath(new URL('./src/shared/', import.meta.url))
},
},
})