# toutiao **Repository Path**: training-demo/toutiao ## Basic Information - **Project Name**: toutiao - **Description**: 通过 @faker-js/faker 和 vite-plugin-mock 模拟数据和请求,掌握 Vue 和 TS 的综合应用。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-03-11 - **Last Updated**: 2026-03-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 1. 创建项目 ```bash npm create vite@latest ``` # 2. 静态结构 main.ts ```ts import "./styles/index.css"; ``` styles/index.css ```css body { margin: 0; padding: 0; } *, *:before, *:after { box-sizing: inherit; } li { list-style: none; } dl, dd, dt, ul, li { margin: 0; padding: 0; } .no-padding { padding: 0px !important; } .padding-content { padding: 4px 0; } a:focus, a:active { outline: none; } a, a:focus, a:hover { cursor: pointer; color: inherit; text-decoration: none; } b { font-weight: normal; } div:focus { outline: none; } .fr { float: right; } .fl { float: left; } .pr-5 { padding-right: 5px; } .pl-5 { padding-left: 5px; } .block { display: block; } .pointer { cursor: pointer; } .inlineBlock { display: block; } .catagtory { display: flex; overflow: hidden; overflow-x: scroll; background-color: #f4f5f6; width: 100%; position: fixed; top: 0; left: 0; z-index: 999; } .catagtory li { padding: 0 15px; text-align: center; line-height: 40px; color: #505050; cursor: pointer; z-index: 99; white-space: nowrap; } .catagtory li.select { color: #f85959; } .list { margin-top: 60px; } .article_item { padding: 0 10px; } .article_item .img_box { display: flex; justify-content: space-between; } .article_item .img_box .w33 { width: 33%; height: 90px; display: inline-block; } .article_item .img_box .w100 { width: 100%; height: 180px; display: inline-block; } .article_item h3 { font-weight: normal; line-height: 2; } .article_item .info_box { color: #999; line-height: 2; position: relative; font-size: 12px; } .article_item .info_box span { padding-right: 10px; } .article_item .info_box span.close { border: 1px solid #ddd; border-radius: 2px; line-height: 15px; height: 12px; width: 16px; text-align: center; padding-right: 0; font-size: 8px; position: absolute; right: 0; top: 7px; } ``` components/channel.vue ```html 开发者资讯 ios c++ android css 数据库 区块链 go ``` components/channelList.vue ```html python数据预处理 :数据标准化 13552285417 0评论 2018-11-29T17:02:09 ``` App.vue ```html ``` # 3. 数据模拟 ```bash npm i @faker-js/faker vite-plugin-mock -D ``` vite.config.ts ```ts import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; import { viteMockServe } from "vite-plugin-mock"; export default defineConfig({ plugins: [ vue(), viteMockServe({ mockPath: "mock", enable: true, }), ], }); ``` mock/channel.ts ```ts import { faker } from "@faker-js/faker"; import { ChannelItem, ChannelDetail } from "../src/types/data"; const createChannels = () => { const channels: ChannelItem[] = []; for (let i = 0; i < 10; i++) { channels.push({ id: `${i + 1}`, name: faker.music.songName(), }); } return channels; }; const createChannelDetailById = (channelId: string) => { const infoList: { channel: string; data: ChannelDetail[] }[] = []; for (let i = 0; i < 10; i++) { const result: ChannelDetail[] = []; for (let j = 0; j < 50; j++) { result.push({ id: faker.string.uuid(), username: faker.internet.username(), email: faker.internet.email(), avatar: faker.image.avatar(), password: faker.internet.password(), birthdate: faker.date.birthdate(), registeredAt: faker.date.past(), }); } infoList.push({ channel: i + "", data: result, }); } return infoList.find((item) => item.channel === channelId)?.data; }; export default [ { url: "/api/channels", method: "get", response: () => { return { data: createChannels(), message: "ok" }; }, }, { url: "/api/channel/:id", method: "get", response: (params: { query: { id: string } }) => { const id = params.query.id || Math.floor(Math.random() * 10) + ""; return { data: createChannelDetailById(id), message: "ok" }; }, }, ]; ``` # 4. Axios 和 TypeScript utils/request.ts ```ts import axios, { type Method } from "axios"; import type { ApiResponse } from "../types/data"; const instance = axios.create({ baseURL: "/api", timeout: 5000, }); instance.interceptors.request.use( (config) => { return config; }, (error) => { return Promise.reject(error); }, ); instance.interceptors.response.use( (response) => { return response.data; }, (error) => { return Promise.reject(error); }, ); const request = ( url: string, method: Method = "get", submitData?: object, ) => { return instance.request>({ url, method, [method.toLowerCase() === "get" ? "params" : "data"]: submitData, }); }; export default request; ``` App.vue 中测试如下,types/data.d.ts ```ts export type ApiResponse = { data: T; message: string; }; export type ChannelItem = { id: string; name: string; }; export type ChannelDetail = { id: string; username: string; email: string; avatar: string; password: string; birthdate: Date; registeredAt: Date; }; export type ChannelList = ChannelDetail[]; ``` src\components\channel.vue ```html ``` # 5. 频道渲染 components/channel.vue ```html {{ channel.name }} ``` # 6. 导航切换 App.vue ```html ``` components/channel.vue ```html {{ channel.name }} ``` # 7. 列表渲染 App.vue ```html ``` components/channelList.vue ```html {{channel.username}} {{channel.email}} {{channel.password}} {{channel.birthdate}} ```