Initial project

This commit is contained in:
Timi
2025-07-16 16:35:28 +08:00
parent 1e8213575b
commit 1c1f2f6594
36 changed files with 11610 additions and 129 deletions

5
.editorconfig Normal file
View File

@ -0,0 +1,5 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = tab
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true

2
.env.development Normal file
View File

@ -0,0 +1,2 @@
# 接口
VITE_API=https://api.imyeyu.dev

15
.eslintignore Normal file
View File

@ -0,0 +1,15 @@
*.sh
node_modules
*.md
*.woff
*.ttf
.vscode
.idea
dist
/public
/docs
.husky
.local
.eslintrc.cjs
dist
pnpm-lock.yaml

82
.eslintrc.cjs Normal file
View File

@ -0,0 +1,82 @@
module.exports = {
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:vue/vue3-essential",
"./.eslintrc-auto-import.json"
],
"overrides": [
{
"env": {
"node": true
},
"files": [
".eslintrc.{js}"
],
"parserOptions": {
"sourceType": "script"
}
}
],
"parserOptions": {
"ecmaVersion": "latest",
"parser": "@typescript-eslint/parser",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint",
"vue"
],
"rules": { // 注释是解释使用该设置的效果,而不是设置属性本身
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
// 其他
"camelcase": 2, // 变量驼峰式命名
"quotes": ["error", "double"], // 强制双引字符串
"eqeqeq": ["error", "always"], // 强制全等比较
"semi": ["error", "always"], // 强制语句分号结束
"max-len": [
"error",
{
"code": 180
}
],
// 逗号
"comma-style": [2, "last"], // 逗号出现在行末 [first, last]
"comma-dangle": [2, "never"], // 数组或对象不可带最后一个逗号 [never, always, always-multiline]
// 空格
"no-trailing-spaces": "error", // 禁止行末存在空格
"comma-spacing": [2, { "before": false, "after": true }], // 逗号后需要空格
"semi-spacing": ["error", { "before": false, "after": true }], // 分号后需要空格
"computed-property-spacing": [2, "never"], // 以方括号取对象属性时,[ 后面和 ] 前面需要空格, [never, always]
"space-before-function-paren": ["error", { // 函数括号前空格
"anonymous": "always", // 针对匿名函数表达式,比如 function () {}
"named": "never", // 针对命名函数表达式,比如 function foo() {}
"asyncArrow": "always" // 针对异步的箭头函数表达式,比如 async () => {}
}],
// 缩进
"no-mixed-spaces-and-tabs": "off", // 允许混合缩进
"no-tabs": ["error", { allowIndentationTabs: true }], // 使用 Tab 缩进
"indent": ["error", "tab", { // Tab 缩进相关
SwitchCase: 1 // Switch Case 缩进一级
}],
// 框架
"@typescript-eslint/ban-types": "off", // TS 允许空对象
"@typescript-eslint/no-empty-function": "off", // TS 允许空函数
"@typescript-eslint/no-explicit-any": "off", // TS 允许 any 类型
"@typescript-eslint/explicit-module-boundary-types": "off", // TS 允许显式模块边界类型?
"vue/no-multiple-template-root": "off", // Vue3 支持多个根节点
"@typescript-eslint/no-this-alias": "off", // 允许 this 变量本地化
"vue/no-v-model-argument": "off", // 允许 v-model 支持参数
"vue/multi-word-component-names": "off", // 允许单个词语的组件名
"@typescript-eslint/no-unused-expressions": [ // 允许逻辑与(&&)、逻辑或(||)短路
"error",
{ "allowShortCircuit": true }
]
}
};

145
.gitignore vendored
View File

@ -1,138 +1,29 @@
# ---> Node
# Logs # Logs
logs logs
*.log *.log
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html) node_modules
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist dist
dist-ssr
*.local
# Gatsby files # Editor directories and files
.cache/ .vscode/*
# Comment in the public line in if your project uses Gatsby and not Next.js !.vscode/extensions.json
# https://nextjs.org/blog/next-9-1#public-directory-support .idea
# public .DS_Store
*.suo
# vuepress build output *.ntvs*
.vuepress/dist *.njsproj
*.sln
# vuepress v2.x temp and cache directory *.sw?
.temp
.cache
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
/.env.production
/.eslintrc-auto-import.json
/components.d.ts
/src/auto-imports.d.ts

View File

@ -1,3 +1,18 @@
# blog # Vue 3 + TypeScript + Vite
博客前端 This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support For `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="icon" href="public/favicon.ico" type="image/x-icon">
<title>夜雨的技术生活</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

43
package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "blog",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.13",
"vue-router": "4",
"less": "^4.3.0",
"timi-web": "link:..\\timi-web",
"timi-tdesign-pc": "link:..\\timi-tdesign-pc",
"tdesign-vue-next": "1.12.0"
},
"devDependencies": {
"@types/node": "^22.15.18",
"@vitejs/plugin-vue": "^5.2.4",
"@typescript-eslint/eslint-plugin": "^8.25.0",
"@typescript-eslint/parser": "^8.25.0",
"@vue/cli-plugin-babel": "^5.0.8",
"@vue/compiler-sfc": "^3.5.13",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.4.0",
"typescript": "^5.8.3",
"vite": "6.3.4",
"vue-tsc": "^2.2.10",
"eslint": "^8.56.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-promise": "^7.2.1",
"eslint-plugin-vue": "^9.32.0",
"less": "^4.3.0",
"prettier": "^3.5.2",
"unplugin-auto-import": "^19.2.0",
"unplugin-vue-components": "^28.5.0",
"vite-plugin-dts": "^4.5.3",
"vite-plugin-vue-setup-extend": "^0.4.0"
}
}

9610
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

2
public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Sitemap: ./sitemap.xml

26
src/Root.vue Normal file
View File

@ -0,0 +1,26 @@
<template>
<b-e-flower-fall class="background" />
<root-layout v-if="isReady" class="diselect" icp="粤ICP备2025368555号-1" domain="imyeyu.com" author="夜雨">
<router-view />
</root-layout>
<popup />
<user-profile-popup />
</template>
<script lang="ts" setup>
import { BEFlowerFall, Popup, SettingMapper } from "timi-web";
import { RootLayout, UserProfilePopup } from "timi-tdesign-pc";
const isReady = ref(false);
onMounted(async () => {
await SettingMapper.loadSetting();
isReady.value = true;
});
</script>
<style lang="less">
.background {
background: url("@/assets/img/bg/main.png") fixed right bottom;
}
</style>

37
src/api/ArticleAPI.ts Normal file
View File

@ -0,0 +1,37 @@
import { Article, ArticleRanking, ArticleView } from "@/types/Article";
import { axios, Page, PageResult } from "timi-web";
async function page(page: Page): Promise<PageResult<Article<any>>> {
return axios.post("/article/list", page);
}
/**
* 获取文章
*
* @param id 文章 ID
* @returns 文章数据
*/
async function view(id: number): Promise<ArticleView<any>> {
return axios.get(`/article/${id}`);
}
/**
* 喜欢文章后端有限调用1240ms 一次
*
* @param id 文章 ID
* @returns 最新喜欢数量
*/
async function like(id: number): Promise<number> {
return axios.get(`/article/like/${id}`);
}
async function listRanking(): Promise<ArticleRanking[]> {
return axios.get("/article/list/ranking");
}
export default {
view,
like,
page,
listRanking
};

11
src/api/BlogAPI.ts Normal file
View File

@ -0,0 +1,11 @@
import { Friend } from "@/types/Friend";
import { axios } from "timi-web";
async function friends(): Promise<Friend[]> {
return axios.get("/friend");
}
export default {
friends
};

BIN
src/assets/img/bg/main.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
src/assets/img/disk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
src/assets/img/nagiasu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

View File

@ -0,0 +1,92 @@
<template>
<div class="login-menu">
<t-dropdown
v-if="userStore.isLogged() && userStore.loginUser.user"
class="logged"
trigger="click"
v-popup:config="popupUserStore.config"
@mouseenter="popupUserStore.user.value = userStore.loginUser.user"
>
<t-button class="button" variant="text">
<template #icon>
<t-avatar
class="icon"
:class="(<any>ImageType)[userStore.loginUser.user.profile.avatarType]"
:image="UserAPI.getAvatarURL(userStore.loginUser.user.profile)"
size="small"
shape="round"
/>
</template>
<span v-text="userStore.loginUser.user.name"></span>
<template #suffix>
<icon name="ARROW_1_S" />
</template>
</t-button>
<t-dropdown-menu>
<t-dropdown-item @click="toUserSpace">
<template #prefixIcon>
<icon name="USER" />
</template>
<span>个人中心</span>
</t-dropdown-item>
<t-dropdown-item @click="userStore.logout()">
<template #prefixIcon>
<icon />
</template>
<span>退出登录</span>
</t-dropdown-item>
</t-dropdown-menu>
</t-dropdown>
<t-button
v-else
variant="text"
@click="visitableAuth = true"
>
<template #icon>
<icon class="icon" name="USER" />
</template>
<span>登录</span>
</t-button>
</div>
<t-dialog
v-model:visible="visitableAuth"
attach="body"
:close-btn="false"
:header="false"
:footer="false"
>
<authorize-form
v-if="visitableAuth"
@login-success="visitableAuth = false"
@register-success="visitableAuth = false"
/>
</t-dialog>
</template>
<script lang="ts" setup>
import { Icon, ImageType, SettingKey, SettingMapper, UserAPI, userStore } from "timi-web";
import { AuthorizeForm, popupUserStore } from "timi-tdesign-pc";
const visitableAuth = ref<boolean>(false);
const toUserSpace = () => {
const loginUserId = userStore.loginUser.token?.id;
if (loginUserId) {
window.open(`${SettingMapper.getDomainLink(SettingKey.DOMAIN_SPACE)}/${loginUserId}`, "_blank");
}
};
</script>
<style lang="less" scoped>
.login-menu {
.icon {
margin-right: .5rem;
}
.logged {
&.button {
padding: 0;
}
}
}
</style>

View File

@ -0,0 +1,59 @@
<template>
<div class="article-list">
<article
class="article"
v-for="item in list"
:key="item.id"
>
<header class="header">
<h4 class="title" >
<router-link class="link black" :to="`/aid${item.id}.html`">
<span v-text="item.title"></span>
</router-link>
</h4>
</header>
<section class="digest gray" v-text="item.digest"></section>
</article>
</div>
</template>
<script lang="ts" setup>
import { Article } from "@/types/Article";
const props = withDefaults(defineProps<{
list: Article<any>[]
}>(), {
list: () => []
});
const { list } = toRefs(props);
</script>
<style lang="less" scoped>
.article-list {
padding: 1rem;
.article {
margin-bottom: 1rem;
&:hover {
background: #EEE;
}
.header {
.title {
margin: 0;
padding: .25rem 1rem;
border-left: 3px solid var(--td-brand-color);
}
}
.digest {
height: 3rem;
font-size: 13px;
margin-top: .25rem;
padding-left: calc(1rem + 3px);
}
}
}
</style>

View File

@ -0,0 +1,164 @@
<template>
<div class="article-about">
<div v-if="article">
<markdown-view class="content" :content="article.data" />
<p
class="update-at gray"
v-text="'最后编辑时间:' + Time.toPassedDateTime(article.updatedAt || article.createdAt)"
></p>
</div>
<div class="spectrum" @click="toggleBGM" v-popup:text="'点击暂停/播放'">
<canvas ref="canvas" :width="canvasWidth" height="80"></canvas>
</div>
<audio ref="player" autoplay>
<source src="@/assets/media/fragile.mp3" type="audio/mpeg" />
</audio>
<p class="survival-time light-gray" v-text="survivalTime"></p>
<comment
v-if="SettingMapper.is(SettingKey.ENABLE_COMMENT) && article && article.showComment"
:bizType="CommentBizType.ARTICLE"
:bizId="1"
:titleStickyOffset="49"
:canComment="article.canComment"
/>
</div>
</template>
<script lang="ts" setup>
import { CommentBizType, MarkdownView, SettingKey, SettingMapper, Time } from "timi-web";
import ArticleAPI from "@/api/ArticleAPI";
import { ArticleView } from "@/types/Article";
import { Comment } from "timi-tdesign-pc";
// 文章
const article = ref<ArticleView<any>>();
onMounted(async () => article.value = await ArticleAPI.view(1));
// 运行时间
const survivalTime = ref<string>("网站已运行 ---- 年 --- 天 -- 小时 -- 分钟 -- 秒");
const survivalTimer = ref();
onMounted(async () => {
const begin = new Date("2017/10/9 22:32:52");
clearInterval(survivalTimer.value);
survivalTimer.value = setInterval(() => {
const r = Time.between(begin);
survivalTime.value = `网站已运行 ${r.y}${r.d}${r.h} 小时 ${r.m.toString().padStart(2, "0")} 分钟 ${r.s.toString().padStart(2, "0")}`;
}, 1000);
});
onBeforeUnmount(() => clearInterval(survivalTimer.value));
// 音乐
const player = ref<HTMLAudioElement>();
const canvas = ref<HTMLCanvasElement>();
const canvasWidth = ref(480);
const spectrumRender = ref<number>();
const toggleBGM = () => {
if (player.value) {
if (player.value.paused) {
player.value.play();
} else {
player.value.pause();
}
}
};
const drawSpectrum = () => {
if (!canvas.value || !player.value) {
return;
}
let ctx: any = new AudioContext();
const analyser = ctx.createAnalyser();
const audioSrc = ctx.createMediaElementSource(<HTMLMediaElement>player.value);
audioSrc.connect(analyser);
analyser.connect(ctx.destination);
const cWidth = canvas.value.width,
meterSize = 7,
cHeight = canvas.value.height - 2,
meterWidth = 6,
capHeight = 2,
meterNum = 2400 / meterSize,
capYPositionArray = [];
ctx = canvas.value.getContext("2d");
// 渐变
const gradient = ctx.createLinearGradient(0, 0, 0, 100);
gradient.addColorStop(1, "#A67D7B");
gradient.addColorStop(0.5, "#A67D7B");
gradient.addColorStop(0, "#A67D7B");
const capStyle = "#A67D7B";
// 动画
(function renderFrame() {
const array = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(array);
const step = Math.round(array.length / meterNum);
ctx.clearRect(0, 0, cWidth, cHeight);
for (let i = 0; i < meterNum; i++) {
const value = array[i * step];
if (capYPositionArray.length < Math.round(meterNum)) {
capYPositionArray.push(value);
}
ctx.fillStyle = capStyle;
if (value < capYPositionArray[i]) {
ctx.fillRect(4 + i * meterSize, cHeight - (--capYPositionArray[i] / 3.2), meterWidth, capHeight);
} else {
ctx.fillRect(4 + i * meterSize, cHeight - value / 3.2, meterWidth, capHeight);
capYPositionArray[i] = value;
}
ctx.fillStyle = gradient;
ctx.fillRect(4 + i * meterSize, cHeight - value / 3.2 + capHeight, meterWidth, cHeight);
}
spectrumRender.value = requestAnimationFrame(renderFrame);
})();
};
onMounted(async () => {
if (player.value) {
player.value.volume = .2;
player.value.addEventListener("loadeddata", () => {
if (player.value && 2 <= player.value.readyState) {
// TODO: 成功 play 才允许绘制
drawSpectrum();
}
});
}
});
onBeforeUnmount(() => {
if (spectrumRender.value) {
cancelAnimationFrame(spectrumRender.value);
}
});
</script>
<style lang="less" scoped>
.article-about {
.content {
width: calc(100% - 2rem);
padding: .5rem 1rem;
}
.update-at {
text-align: right;
padding-right: 1rem;
}
.spectrum {
width: 20rem;
height: 320px;
margin: 0 auto;
display: flex;
background: url("@/assets/img/nagiasu.png") bottom no-repeat;
flex-direction: column;
justify-content: end;
background-size: 100%;
}
.survival-time {
height: 16px;
font-size: 13px;
text-align: center;
}
}
</style>

View File

@ -0,0 +1,346 @@
<template>
<div v-if="article" class="article-music">
<p class="time" v-text="articleTime" @click="isCreatedAt = !isCreatedAt"></p>
<!-- 光盘 -->
<div class="disk-box" v-if="article.extendData">
<div class="cover">
<img :src="coverUrl" alt="专辑封面" />
</div>
<!-- SVG 绘制扇形文字 -->
<div class="disk">
<svg viewBox="0 0 100 100">
<path d="M 0,50 a 50,50 0 1,1 0,1 z" id="circle" />
<text fill="#F4F4F4">
<textPath xlink:href="#circle">{{ article.extendData.title }}</textPath>
</text>
</svg>
</div>
</div>
<markdown-view class="description" :content="article.data" />
<div class="content selectable" v-if="article.extendData">
<section class="list">
<h4 class="title">曲目</h4>
<ul class="items">
<li class="item" v-for="(item, i) in article.extendData.list" :key="i" :data-i="`${i + 1}.`">
<div class="value">
<span v-text="item.title"></span>
<icon v-if="item.audio" class="icon cur-pointer" name="MUSIC" @click="doActiveMedia(i)" />
<icon v-if="item.video" class="icon cur-pointer" name="VIDEO" @click="doActiveMedia(i)" />
</div>
<div v-if="activeMediaIndex === i" class="media">
<audio v-if="item.audio" :src="Toolkit.toResURL(item.audio)" controls></audio>
<video v-if="item.video" :src="Toolkit.toResURL(item.video)" controls></video>
</div>
</li>
</ul>
</section>
<section class="info">
<h4 class="title">信息</h4>
<ul class="items">
<li class="item" v-for="(item, i) in article.extendData.info" :key="i">
<span class="label diselect" v-text="`${item.key}`"></span>
<span class="gray" v-text="item.value"></span>
</li>
</ul>
</section>
</div>
</div>
<comment
v-if="SettingMapper.is(SettingKey.ENABLE_COMMENT) && article && article.showComment"
:bizType="CommentBizType.ARTICLE"
:bizId="article.id!"
:titleStickyOffset="80"
:canComment="article.canComment"
/>
</template>
<script lang="ts" setup>
import { ArticleAttachType, ArticleMusicExtendData, ArticleView } from "@/types/Article.ts";
import { CommentBizType, CommonAPI, Icon, MarkdownView, SettingKey, SettingMapper, Time, Toolkit } from "timi-web";
import { Comment } from "timi-tdesign-pc";
const props = defineProps<{
article?: ArticleView<ArticleMusicExtendData>,
}>();
const { article } = toRefs(props);
const isCreatedAt = ref<boolean>(false);
const articleTime = computed(() => {
if (article.value) {
if (isCreatedAt.value || !article.value.updatedAt) {
return "发布于 " + Time.toPassedDateTime(article.value.createdAt);
} else {
return "编辑于 " + Time.toPassedDateTime(article.value.updatedAt);
}
}
});
// 专辑封面
const coverUrl = computed(() => {
if (article.value && article.value.attachmentList) {
for (let i = 0; i < article.value.attachmentList.length; i++) {
const item = article.value.attachmentList[i];
const attachType = (<any>ArticleAttachType)[item.attachType!];
if (attachType === ArticleAttachType.COVER) {
return CommonAPI.getAttachmentReadAPI(item.mongoId);
}
}
}
return undefined;
})
// 展开可播放媒体
const activeMediaIndex = ref();
function doActiveMedia(index: number) {
if (index === activeMediaIndex.value) {
activeMediaIndex.value = undefined;
} else {
activeMediaIndex.value = index;
}
}
</script>
<style lang="less" scoped>
.article-music {
.time {
text-align: center;
}
.disk-box {
width: 628px;
height: 430px;
margin: 12px auto;
display: flex;
position: relative;
align-items: center;
.cover {
left: 3rem;
width: 456px;
height: 428px;
z-index: 2;
padding: 2px 0 0 2px;
position: absolute;
transition: .3s;
/* 蒙板 */
&::after {
content: '';
top: 0;
left: 0;
width: 458px;
height: 430px;
z-index: 3;
position: absolute;
background: url('~@/assets/img/disk.png') no-repeat;
}
img {
width: 100%;
height: 100%;
border: var(--tui-border);
box-shadow: var(--tui-shadow);
}
}
.disk {
top: 50%;
right: 3rem;
color: #F4F4F4;
width: 16rem;
height: 16rem;
border: 3px solid #888;
padding: 40px;
z-index: 1;
position: absolute;
font-size: .5rem;
transform: rotateZ(50deg);
transition: .3s;
margin-top: calc(-43px - 8rem);
background: #212121;
border-radius: 50%;
&::after {
content: '';
top: 50%;
left: 50%;
width: 30px;
height: 30px;
margin: -23px 0 0 -23px;
border: 8px solid #BBB;
position: absolute;
background: #F4F4F4;
border-radius: 50%;
}
svg {
display: block;
overflow: visible;
path {
fill: none;
}
}
}
&:hover,
&:active {
.cover {
left: 0;
transition: 520ms var(--tui-bezier);
}
.disk {
right: 0;
transform: rotateZ(120deg);
transition: 520ms var(--tui-bezier);
}
}
}
.description {
padding: 1rem;
}
.content {
display: flex;
padding: 12px 20px;
flex-wrap: wrap;
justify-content: space-between;
.list {
flex: 10;
border: 1px solid #CDDEF0;
min-width: 440px;
background: #FFF;
margin-right: 1.5rem;
.title {
margin: 0;
padding: .5rem 1rem;
background: #CDDEF0;
}
.items {
margin: 0;
position: relative;
list-style: none;
padding-left: 0;
.item {
padding: 4px 0;
display: flex;
line-height: 1.5;
flex-direction: column;
&::before {
content: attr(data-i);
color: #525870;
width: 32px;
height: 32px;
position: absolute;
text-align: right;
font-weight: bold;
}
&:nth-child(2n) {
background: #F4F4F4;
}
.value {
display: flex;
align-items: center;
text-indent: 2.5em;
.icon {
margin-left: .5rem;
&:hover {
fill: #FF7A9B;
}
}
}
.media {
display: flex;
justify-content: center;
audio,
video {
width: calc(100% - 5em);
margin: 4px auto;
border: var(--tui-border);
box-shadow: var(--tui-shadow);
}
audio {
height: 2.5rem;
background: #F1F3F4;
}
}
}
}
}
.info {
color: #666;
flex: 2;
min-width: 220px;
text-align: left;
line-height: 1.6;
margin-bottom: 24px;
.title {
margin: .5rem 0 0 0;
}
.items {
padding: 4px;
font-size: 12px;
margin-top: 0;
.item {
padding: 6px 0;
line-height: 1.5;
border-bottom: 1px dashed #777;
.label {
color: #525870;
font-weight: bold;
}
}
}
}
}
}
@media screen and (max-width: 680px) {
.article-music {
.disk-box {
width: calc(628px * .5);
height: calc(430px * .5);
.cover {
left: 3rem;
width: calc(456px * .5);
height: calc(428px * .5);
&::after {
width: calc(458px * .5);
height: calc(430px * .5);
}
}
.disk {
width: calc(16rem * .4);
height: calc(16rem * .4);
margin-top: calc(-43px - 8rem * .4);
}
}
}
}
</style>

View File

@ -0,0 +1,52 @@
<template>
<div v-if="article" class="article-public">
<div class="time" v-text="articleTime" @click="isCreatedAt = !isCreatedAt"></div>
<markdown-view class="content" :content="article.data" />
<comment
v-if="SettingMapper.is(SettingKey.ENABLE_COMMENT) && article.showComment"
:bizType="CommentBizType.ARTICLE"
:bizId="article.id!"
:titleStickyOffset="80"
:canComment="article.canComment"
/>
</div>
</template>
<script lang="ts" setup>
import { CommentBizType, MarkdownView, SettingKey, SettingMapper, Time } from "timi-web";
import { ArticleView } from "@/types/Article";
import { Comment } from "timi-tdesign-pc";
const props = defineProps<{
article?: ArticleView<any>,
}>();
const { article } = toRefs(props);
const isCreatedAt = ref<boolean>(false);
const articleTime = computed(() => {
if (article.value) {
if (isCreatedAt.value || !article.value.updatedAt) {
return "发布于 " + Time.toPassedDateTime(article.value.createdAt);
} else {
return "编辑于 " + Time.toPassedDateTime(article.value.updatedAt);
}
}
});
</script>
<style lang="less" scoped>
.article-public {
.time {
text-align: center;
margin: 1rem 0 2rem 0;
}
.content {
width: calc(100% - 2rem);
padding: .5rem 1rem;
}
}
</style>

View File

@ -0,0 +1,168 @@
<template>
<div v-if="article" class="article-software">
<p class="time" v-text="articleTime" @click="isCreatedAt = !isCreatedAt"></p>
<div class="header">
<img class="logo" :src="coverUrl" alt="程序封面" />
<div v-if="article.extendData" class="info">
<div>
<span>官网</span>
<a :href="article.extendData.url" v-text="article.extendData.url"></a>
</div>
<div>
<span>版本</span>
<span class="selectable" v-text="article.extendData.version"></span>
</div>
<div>
<span>文件格式</span>
<span class="selectable" v-text="article.extendData.format"></span>
</div>
<div class="runtimes">
<span>运行环境</span>
<span
class="runtime"
v-for="item in article.extendData.runtime"
v-text="`${item} `"
:key="item"
></span>
</div>
<div>
<span>大小</span>
<span v-text="IOSize.format(article.extendData.size)"></span>
</div>
<div class="downloads">
<span>下载</span>
<a
class="download"
v-for="item in article.extendData.downloads"
:href="Toolkit.toResURL(item.value)"
:key="item.value"
v-text="item.type"
target="_blank"
></a>
</div>
<div>
<span>解压密码</span>
<span class="selectable" v-text="article.extendData.password"></span>
</div>
</div>
</div>
<markdown-view class="content" :content="article.data" />
</div>
<comment
v-if="SettingMapper.is(SettingKey.ENABLE_COMMENT) && article && article.showComment"
:bizType="CommentBizType.ARTICLE"
:bizId="article.id!"
:titleStickyOffset="80"
:canComment="article.canComment"
/>
</template>
<script lang="ts" setup>
import { ArticleAttachType, ArticleSoftwareExtendData, ArticleView } from "@/types/Article.ts";
import { CommentBizType, CommonAPI, IOSize, MarkdownView, SettingKey, SettingMapper, Time, Toolkit } from "timi-web";
import { Comment } from "timi-tdesign-pc";
const props = defineProps<{
article?: ArticleView<ArticleSoftwareExtendData>,
}>();
const { article } = toRefs(props);
const isCreatedAt = ref<boolean>(false);
const articleTime = computed(() => {
if (article.value) {
if (isCreatedAt.value || !article.value.updatedAt) {
return "发布于 " + Time.toPassedDateTime(article.value.createdAt);
} else {
return "编辑于 " + Time.toPassedDateTime(article.value.updatedAt);
}
}
});
const coverUrl = computed(() => {
if (article.value && article.value.attachmentList) {
for (let i = 0; i < article.value.attachmentList.length; i++) {
const item = article.value.attachmentList[i];
const attachType = (<any>ArticleAttachType)[item.attachType!];
if (attachType === ArticleAttachType.COVER) {
return CommonAPI.getAttachmentReadAPI(item.mongoId);
}
}
}
return undefined;
})
</script>
<style lang="less" scoped>
.article-software {
.time {
text-align: center;
}
.header {
display: flex;
padding: 1rem;
margin-bottom: 2rem;
.logo {
width: 140px;
height: 140px;
transition: 520ms var(--tui-bezier);
}
.info {
font-size: 13px;
border-left: 2px solid var(--tui-light-gray);
margin-left: 1rem;
padding-left: 1rem;
> div {
margin-bottom: 4px;
> span:first-child {
color: #666;
padding: 2px 0;
}
}
.downloads {
.download {
border: 1px solid #FF7A9B;
padding: 1px 4px;
background: transparent;
margin-right: 6px;
&:hover {
color: #FFF;
background: #FF7A9B;
}
&:active {
background: #FFAAC0;
}
}
}
}
}
.content {
width: calc(100% - 2rem);
padding: 1rem;
}
}
@media screen and (max-width: 650px) {
.article-software {
.header {
.logo {
width: 70px;
height: 70px;
}
}
}
}
</style>

107
src/layout/IndexLayout.vue Normal file
View File

@ -0,0 +1,107 @@
<template>
<div class="index-layout">
<t-head-menu class="menu" v-model="menuValue">
<template #logo>
<div
v-if="owner"
class="owner"
v-popup:config="popupUserStore.config"
@mouseenter="popupUserStore.user.value = owner"
>
<img
class="avatar ir-pixelated"
:class="(<any>ImageType)[owner.profile.avatarType]"
:src="UserAPI.getAvatarURL(owner.profile)"
alt="avatar"
/>
<div class="name">夜雨</div>
</div>
</template>
<t-menu-item to="/?p=0" :value="Menu.INDEX">主页</t-menu-item>
<t-menu-item
:href="SettingMapper.getDomainLink(SettingKey.DOMAIN_GIT)"
:value="Menu.GIT" target="_blank"
>开源项目</t-menu-item>
<t-menu-item to="/about" :value="Menu.ABOUT">关于</t-menu-item>
<template #operations v-if="SettingMapper.is(SettingKey.ENABLE_LOGIN)">
<login-menu v-if="showUser" />
</template>
</t-head-menu>
<router-view :key="route.name!" />
</div>
</template>
<script lang="ts" setup>
import { deviceStore, ImageType, SettingKey, SettingMapper, UserAPI, UserView } from "timi-web";
import LoginMenu from "@/components/LoginMenu.vue";
import { popupUserStore } from "timi-tdesign-pc";
// 个人信息
const owner = ref<UserView>();
onMounted(async () => owner.value = await UserAPI.view(1));
// 路由
enum Menu {
INDEX = "INDEX",
GIT = "GIT",
ABOUT = "ABOUT"
}
const regexMenu: {
[key: string]: Menu
} = {
"ArticleIndex|ArticleDetail": Menu.INDEX,
"About": Menu.ABOUT
};
const route = useRoute();
const menuValue = ref();
const syncMenuRouter = (routerName?: string) => {
if (routerName) {
for (const item in regexMenu) {
if (new RegExp(item).test(routerName as string)) {
menuValue.value = regexMenu[item];
break;
}
}
}
};
watch(() => route.name, (routerName) => syncMenuRouter(routerName as string));
onMounted(() => syncMenuRouter(route.name as string));
// 登录
const showUser = computed(() => !deviceStore.isPhone.value);
</script>
<style lang="less" scoped>
.index-layout {
.menu {
top: 0;
z-index: 12;
position: sticky;
background: rgba(255, 255, 255, .8);
border-bottom: var(--tui-border);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
.owner {
height: calc(100% - .5rem);
margin: 0 1rem 0 0;
padding: .25rem;
display: flex;
align-items: center;
.avatar {
height: 100%;
}
.name {
font-size: 1rem;
margin-left: 1rem;
}
}
}
}
</style>

57
src/main.ts Normal file
View File

@ -0,0 +1,57 @@
import { createApp } from "vue";
import router from "@/router";
import { NavigationGuardNext, RouteLocationNormalized } from "vue-router";
import { axios, Network, userStore, VDraggable, VPopup } from "timi-web";
import { DialogPlugin } from "tdesign-vue-next";
import "tdesign-vue-next/dist/tdesign.css";
import "timi-tdesign-pc/style.css";
import "timi-web/style.css";
import Root from "@/Root.vue";
console.log(`
______ __ _ _
/ __\\ \\ \\ \\ \\
/ . . \\ ' \\ \\ \\ \\
( ) imyeyu.com ) ) ) )
'\\ ___ /' / / / /
====='===='=====================/_/_/_/
`);
// ---------- 网络 ----------
axios.defaults.baseURL = import.meta.env.VITE_API;
axios.interceptors.request.use(Network.userTokenInterceptors);
// ---------- 路由 ----------
router.beforeEach(async (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext): Promise<void> => {
await userStore.login4Storage();
if (to.matched.length !== 0) {
next();
return;
}
if (from.name) {
next({name: from.name});
return;
}
// 找不到页面
const alert = DialogPlugin.alert({
header: "错误404",
body: "找不到该页面",
confirmBtn: {
content: "关闭",
variant: "base",
theme: "default"
},
onConfirm: () => {
alert.destroy();
next("/");
}
});
});
// ---------- Vue ----------
const app = createApp(Root);
app.use(router);
app.directive("draggable", VDraggable as any);
app.directive("popup", VPopup as any);
app.mount("#root");

37
src/router/index.ts Normal file
View File

@ -0,0 +1,37 @@
import IndexLayout from "@/layout/IndexLayout.vue";
import ArticleDetail from "@/views/ArticleDetail.vue";
import ArticleIndex from "@/views/ArticleIndex.vue";
import { createRouter, createWebHistory } from "vue-router";
export default createRouter({
history: createWebHistory("/"),
routes: [
{
path: "/",
name: "Index",
component: IndexLayout,
children: [
{
path: "/",
name: "ArticleIndex",
component: ArticleIndex
},
{
path: "/aid:id.html",
name: "ArticleDetail",
component: ArticleDetail
},
{
path: "/about",
name: "About",
component: ArticleDetail
},
{
// 兼容性保留
path: "/article/aid:id.html",
redirect: to => `/aid${to.params.id}.html`
}
]
}
]
});

115
src/types/Article.ts Normal file
View File

@ -0,0 +1,115 @@
import { AttachmentView, Model } from "timi-web";
// 文章
export type Article<E extends ArticleMusicExtendData | ArticleSoftwareExtendData> = {
title?: string;
type: ArticleType;
classId: number;
digest?: string;
data?: string;
extendData?: E;
reads: number;
likes: number;
showComment: boolean;
canComment: boolean;
canRanking: boolean;
} & Model;
export type ArticleView<E extends ArticleMusicExtendData | ArticleSoftwareExtendData> = {
comments?: number;
clazz: ArticleClass;
labels: ArticleLabel[];
attachmentList: AttachmentView[];
} & Article<E>;
export enum ArticleType {
/** 关于 */
ABOUT,
/** 公版 */
PUBLIC,
/** 音乐 */
MUSIC,
/** 软件 */
SOFTWARE
}
export enum ArticleAttachType {
COVER,
}
// 文章分类
export type ArticleClass = {
name?: string;
isOther?: boolean;
order?: number;
count?: number;
} & Model;
// 文章标签
export type ArticleLabel = {
name?: string;
count?: number;
} & Model;
// 每周排行
export type ArticleRanking = {
title?: string;
count?: number;
recentAt?: number;
} & Model;
export type ArticleMusicExtendData = {
title: string;
list: ArticleMusicItem[];
info: {
key: string;
value: string;
}[];
}
export type ArticleSoftwareExtendData = {
url?: string;
downloads: ArticleSoftwareDownload[];
format: string;
runtime: ArticleSoftwareRuntime[];
size: number;
version: string;
password?: string;
}
export enum ArticleSoftwareDownloadType {
TIMI_MONGO,
TIMI_COS,
URL
}
export enum ArticleSoftwareRuntime {
JVM,
WINDOWS,
LINUX,
MAC_OS
}
export type ArticleSoftwareDownload = {
type: ArticleSoftwareDownloadType;
value: string;
}
export type ArticleMusicItem = {
title: string;
audio?: string;
video?: string;
}

8
src/types/Friend.ts Normal file
View File

@ -0,0 +1,8 @@
import { Model } from "timi-web";
/** 友链 */
export type Friend = {
icon?: string;
name: string;
link: string;
} & Model

126
src/views/ArticleDetail.vue Normal file
View File

@ -0,0 +1,126 @@
<template>
<article class="article-detail">
<header v-if="!isAbout" class="header">
<div class="relative">
<t-button
class="back"
theme="default"
variant="text"
content="返回"
@click="router.back()"
>
<template #icon>
<icon class="icon" name="ARROW_1_W" />
</template>
</t-button>
<div class="title">
<h4
v-if="article"
class="value selectable clip-text"
v-text="article.title"
></h4>
</div>
<t-button
class="comment"
theme="default"
variant="text"
content="评论"
>
<template #icon>
<icon class="icon" name="MESSAGE" />
</template>
</t-button>
</div>
</header>
<component :is="template" :article="article" />
</article>
</template>
<script lang="ts" setup>
import { Icon } from "timi-web";
import ArticleAPI from "@/api/ArticleAPI";
import ArticleAbout from "@/components/article/template/ArticleAbout.vue";
import ArticleMusic from "@/components/article/template/ArticleMusic.vue";
import ArticlePublic from "@/components/article/template/ArticlePublic.vue";
import ArticleSoftware from "@/components/article/template/ArticleSoftware.vue";
import { ArticleView } from "@/types/Article";
import { type Component } from "vue";
const route = useRoute();
const router = useRouter();
const article = ref<ArticleView<any>>();
const isAbout = ref<boolean>(false);
const template = ref<Component | null>(null);
const templates: Record<string, Component> = {
ABOUT: ArticleAbout,
MUSIC: ArticleMusic,
PUBLIC: ArticlePublic,
SOFTWARE: ArticleSoftware
};
watch(article, () => {
if (article.value) {
template.value = markRaw(templates[article.value.type]);
}
});
onMounted(async () => {
isAbout.value = route.name === "About";
const id = isAbout.value ? 1 : route.params.id as any as number;
article.value = await ArticleAPI.view(id);
});
</script>
<style lang="less" scoped>
.article-detail {
.header {
top: 49px;
z-index: 3;
position: sticky;
background: rgba(255, 255, 255, .8);
border-bottom: 1px solid var(--td-component-border);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
.relative {
position: relative;
.icon {
margin-right: 2px;
}
.back {
padding: 0 9px;
}
.title {
top: 0;
width: 100%;
height: 100%;
z-index: -1;
position: absolute;
text-align: center;
line-height: 2rem;
.value {
margin: 0 auto;
display: block;
max-width: calc(100% - 120px - 10px);
}
}
.comment {
top: 0;
right: 0;
height: 100%;
padding: 0 9px;
position: absolute;
line-height: 2rem;
}
}
}
}
</style>

238
src/views/ArticleIndex.vue Normal file
View File

@ -0,0 +1,238 @@
<template>
<div class="article-index">
<div class="list">
<article-list
v-if="pageResult"
:list="pageResult.list"
/>
<t-pagination
class="pages"
v-if="pageResult"
:total="pageResult.total"
:pageSize="16"
:showPageSize="false"
:totalContent="isShowPageTotal"
:current="currentPage"
:onCurrentChange="onPageChangeEvent"
>
<template #totalContent v-if="isShowPageTotal">
<div style="flex: 1" v-text="`共 ${pageResult.total} 个项目`"></div>
</template>
</t-pagination>
</div>
<aside class="aside">
<section class="ranking">
<h4 class="title">
<icon class="icon" name="LIST" />
<span>排行</span>
</h4>
<ul class="items">
<li
class="item"
v-for="item in ranking"
:key="item.id"
>
<a
class="title black clip-text"
:href="`/aid${item.id}.html`"
v-text="item.title"
:title="item.title"
@click.prevent="Toolkit.leftClickCallback($event, () => router.push(`/aid${item.id}.html`))"
></a>
</li>
</ul>
</section>
<section class="friend">
<h4 class="title">
<icon class="icon" name="LINK_0" />
<span>友链</span>
</h4>
<div class="items">
<div
class="item"
v-for="friend in friends"
:key="friend.link"
>
<img class="icon ir-pixelated" :src="friend.icon" :alt="friend.name" />
<a
class="name black"
:href="friend.link"
target="_blank"
v-text="friend.name"
></a>
</div>
</div>
</section>
</aside>
</div>
</template>
<script lang="ts" setup>
import { deviceStore, Icon, Page, PageResult, Toolkit } from "timi-web";
import ArticleAPI from "@/api/ArticleAPI";
import { Article, ArticleRanking } from "@/types/Article";
import ArticleList from "@/components/article/ArticleList.vue";
import { Friend } from "@/types/Friend.ts";
import BlogAPI from "@/api/BlogAPI.ts";
const router = useRouter();
const route = useRoute();
// 页码
const currentPage = computed(() => page.index + 1);
// 分页参数
const page = reactive<Page>({
index: 0,
size: 16
});
// 数据列表
const pageResult = ref<PageResult<Article<any>>>();
// 获取列表
const fetchList = Toolkit.debounce(async () => pageResult.value = await ArticleAPI.page(page));
// 监听路由分页
watch(
() => route.query.p,
async (newP) => {
const pNum = Number(newP);
if (Number.isInteger(pNum) && 0 <= pNum) {
page.index = pNum;
await fetchList();
} else {
// 参数无效时重置为第一页
await router.push({
path: route.path,
query: {
...route.query,
p: 0
}
});
}
},
{ immediate: true }
);
function onPageChangeEvent(current: number) {
const newIndex = current - 1;
router.push({
path: route.path,
query: {
...route.query,
p: newIndex
}
});
}
// 分页总数
const isShowPageTotal = computed(() => !deviceStore.isPhone.value);
// 排行
const ranking = reactive<ArticleRanking[]>([]);
onMounted(async () => {
ranking.push(...await ArticleAPI.listRanking());
});
// 友链
const friends = reactive<Friend[]>([]);
onMounted(async () => {
friends.push(...await BlogAPI.friends());
});
</script>
<style lang="less" scoped>
.article-index {
display: flex;
border-bottom: var(--tui-border);
.list {
border-right: var(--tui-border);
.pages {
bottom: -1px;
padding: var(--tui-page-padding);
position: sticky;
background: rgba(255, 255, 255, .8);
border-top: var(--tui-border);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
}
.aside {
top: 49px;
width: 240px;
height: fit-content;
position: sticky;
min-width: 240px;
h4.title {
margin: 0;
display: flex;
padding: .5rem;
align-items: center;
border-bottom: var(--tui-border);
.icon {
margin-right: .5rem;
}
}
.ranking {
.items {
margin: 0;
padding: .5rem .5rem .5rem 2rem;
.item {
.title {
width: 100%;
display: block;
font-size: 14px;
}
}
}
}
.friend {
margin-top: 2rem;
.items {
display: flex;
padding: .5rem;
flex-direction: column;
.item {
display: flex;
margin-bottom: .5rem;
.icon {
width: 16px;
height: 16px;
margin-right: .25rem;
}
.name {
margin: 0;
font-size: 13px;
}
}
}
}
}
}
@media screen and (max-width: 680px) {
.article-index {
.list {
border-right: none;
}
.aside {
display: none;
}
}
}
</style>

15
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,15 @@
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
declare module "prismjs";
interface ImportMetaEnv {
readonly VITE_API: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

42
tsconfig.json Normal file
View File

@ -0,0 +1,42 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": [
"ESNext",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Node",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
}
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.vue",
"node_modules/tdesign-vue-next/global.d.ts"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

12
tsconfig.node.json Normal file
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": [
"vite.config.ts"
]
}

91
vite.config.ts Normal file
View File

@ -0,0 +1,91 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import dts from "vite-plugin-dts";
import { resolve } from "path";
import VueSetupExtend from "vite-plugin-vue-setup-extend";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { TDesignResolver } from "unplugin-vue-components/resolvers";
export default defineConfig({
base: "/",
plugins: [
vue(),
dts({
include: "./src"
}),
VueSetupExtend(),
AutoImport({
imports: [
"vue",
"vue-router",
"pinia",
{
"axios": [
["default", "axios"]
]
}
],
dts: "src/auto-imports.d.ts",
eslintrc: {
enabled: true,
globalsPropValue: true
},
resolvers: [
TDesignResolver({
library: "vue-next"
})
]
}),
Components({
dirs: [
"src/components",
"src/layout",
"src/views/components"
],
resolvers: [
TDesignResolver({
library: "vue-next"
})
]
})
],
css: {
preprocessorOptions: {
less: {
math: "always",
lessOptions: {
modifyVars: {
"@btn-border-radius": "0"
},
javascriptEnabled: true
}
}
}
},
define: {
"process.env": {}
},
resolve: {
alias: {
"@": resolve(__dirname, "src")
},
extensions: [".js", ".json", ".ts"]
},
server: {
port: 80,
host: "imyeyu.dev"
},
build: {
sourcemap: false,
minify: "terser",
terserOptions: {
compress: {
// eslint-disable-next-line camelcase
drop_console: true,
// eslint-disable-next-line camelcase
drop_debugger: true
}
}
}
});