init project
This commit is contained in:
5
.editorconfig
Normal file
5
.editorconfig
Normal 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
2
.env.development
Normal file
@ -0,0 +1,2 @@
|
||||
# 接口
|
||||
VITE_API=https://api.imyeyu.dev
|
||||
15
.eslintignore
Normal file
15
.eslintignore
Normal 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
82
.eslintrc.cjs
Normal 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 }
|
||||
]
|
||||
}
|
||||
};
|
||||
158
.gitignore
vendored
158
.gitignore
vendored
@ -1,149 +1,31 @@
|
||||
# ---> Vue
|
||||
# gitignore template for Vue.js projects
|
||||
#
|
||||
# Recommended template: Node.gitignore
|
||||
|
||||
# TODO: where does this rule come from?
|
||||
docs/_book
|
||||
|
||||
# TODO: where does this rule come from?
|
||||
test/
|
||||
|
||||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
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
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.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.*
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
/.env.production
|
||||
/.eslintrc-auto-import.json
|
||||
/.claude
|
||||
/CLAUDE.md
|
||||
/components.d.ts
|
||||
/src/auto-imports.d.ts
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
# temp-file-web
|
||||
# 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.
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||
|
||||
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>文件中转站</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
44
package.json
Normal file
44
package.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "temp-file-web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.24",
|
||||
"less": "^4.3.0",
|
||||
"pinia": "^3.0.4",
|
||||
"timi-web": "link:..\\timi-web",
|
||||
"timi-tdesign-pc": "link:..\\timi-tdesign-pc",
|
||||
"tdesign-vue-next": "1.17.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.1",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.2.4",
|
||||
"vue-tsc": "^3.1.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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
9702
pnpm-lock.yaml
generated
Normal file
9702
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
24
src/Root.vue
Normal file
24
src/Root.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<root-layout class="root diselect" author="夜雨" icp="粤ICP备2025368555号-1" domain="imyeyu.com" text>
|
||||
<home />
|
||||
<t-back-top class="to-top" size="small">
|
||||
<icon name="arrow_1_n" :scale="2" />
|
||||
</t-back-top>
|
||||
</root-layout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Home from "@/views/Home.vue";
|
||||
import { RootLayout } from "../../timi-tdesign-pc";
|
||||
import { Icon } from "../../timi-web";
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.root {
|
||||
|
||||
.to-top {
|
||||
padding: 0;
|
||||
box-shadow: var(--tui-shadow);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
62
src/api/TempFileAPI.ts
Normal file
62
src/api/TempFileAPI.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { axios } from "timi-web";
|
||||
import type { TempFileResponse } from "@/type/TempFile.ts";
|
||||
|
||||
/** 上传进度事件 */
|
||||
interface UploadProgressEvent {
|
||||
loaded: number;
|
||||
total?: number;
|
||||
lengthComputable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传临时文件
|
||||
*
|
||||
* @param files 文件列表
|
||||
* @param onProgress 上传进度回调
|
||||
* @param ttl 文件缓存时长(毫秒)
|
||||
* @returns 临时文件信息列表
|
||||
*/
|
||||
async function upload(files: File[], onProgress?: (progressEvent: UploadProgressEvent) => void, ttl?: number): Promise<TempFileResponse[]> {
|
||||
const formData = new FormData();
|
||||
files.forEach((file) => {
|
||||
formData.append("file", file);
|
||||
});
|
||||
return axios.post("/temp/file/upload", formData, {
|
||||
params: { ttl },
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
},
|
||||
onUploadProgress: onProgress
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取临时文件下载地址
|
||||
*
|
||||
* @param fileId 临时文件 ID
|
||||
* @returns 下载地址
|
||||
*/
|
||||
function getDownloadUrl(fileId: string): string {
|
||||
return `${axios.defaults.baseURL}/temp/file/download/${fileId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载临时文件
|
||||
*
|
||||
* @param fileId 临时文件 ID
|
||||
*/
|
||||
function download(fileId: string): void {
|
||||
const link = document.createElement("a");
|
||||
link.href = getDownloadUrl(fileId);
|
||||
link.download = "";
|
||||
link.style.display = "none";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
export default {
|
||||
upload,
|
||||
getDownloadUrl,
|
||||
download
|
||||
};
|
||||
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
61
src/components/FileDownload.vue
Normal file
61
src/components/FileDownload.vue
Normal file
@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="file-download">
|
||||
<h4>文件下载</h4>
|
||||
<div class="input-group">
|
||||
<t-input
|
||||
class="input"
|
||||
v-model="downloadFileId"
|
||||
placeholder="请输入临时文件 ID"
|
||||
clearable
|
||||
@keyup.enter="download"
|
||||
/>
|
||||
<t-button theme="primary" @click="download">
|
||||
<div class="download">
|
||||
<icon class="icon" name="download" fill="#FFF" />
|
||||
<span>下载文件</span>
|
||||
</div>
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { MessagePlugin } from "tdesign-vue-next";
|
||||
import TempFileAPI from "@/api/TempFileAPI.ts";
|
||||
import { Icon } from "timi-web";
|
||||
|
||||
const downloadFileId = ref("");
|
||||
function download() {
|
||||
const fileId = downloadFileId.value.trim();
|
||||
if (!fileId) {
|
||||
MessagePlugin.warning("请输入文件 ID");
|
||||
return;
|
||||
}
|
||||
TempFileAPI.download(fileId);
|
||||
MessagePlugin.success("开始下载");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.file-download {
|
||||
padding: 0 1rem;
|
||||
|
||||
.input-group {
|
||||
gap: .625rem;
|
||||
display: flex;
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.download {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
margin: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
154
src/components/FileHistoryList.vue
Normal file
154
src/components/FileHistoryList.vue
Normal file
@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div class="file-history-list">
|
||||
<h4 class="gray">已上传文件</h4>
|
||||
<div v-if="fileHistory.length === 0" class="empty gray">暂无上传记录</div>
|
||||
<div v-else class="list">
|
||||
<div
|
||||
v-for="file in fileHistory"
|
||||
:key="file.id"
|
||||
class="item"
|
||||
>
|
||||
<div class="header">
|
||||
<div class="name-expiry">
|
||||
<div class="name bold selectable clip-text" v-text="file.name"></div>
|
||||
<span class="size gray" v-text="IOSize.format(file.size)"></span>
|
||||
</div>
|
||||
<div class="id gray">
|
||||
<span>ID: </span>
|
||||
<span class="selectable" v-text="file.id"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div class="expiry gray" v-text="getTimeRemaining(file.expiryTime)"></div>
|
||||
<div class="actions">
|
||||
<t-button size="small" theme="default" variant="outline" @click="copyFileId(file.id)">复制 ID</t-button>
|
||||
<t-button class="download" size="small" theme="primary" @click="download(file.id)">下载</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { MessagePlugin } from "tdesign-vue-next";
|
||||
import TempFileAPI from "@/api/TempFileAPI.ts";
|
||||
import { useFileHistoryStore } from "@/store/fileHistory";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { IOSize, Time } from "../../../timi-web";
|
||||
|
||||
const fileHistoryStore = useFileHistoryStore();
|
||||
const { fileHistory } = storeToRefs(fileHistoryStore);
|
||||
|
||||
/** 下载文件 */
|
||||
function download(fileId: string) {
|
||||
TempFileAPI.download(fileId);
|
||||
MessagePlugin.success("开始下载");
|
||||
}
|
||||
|
||||
/** 复制文件 ID */
|
||||
async function copyFileId(fileId: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(fileId);
|
||||
await MessagePlugin.success("已复制到剪贴板");
|
||||
} catch (error) {
|
||||
await MessagePlugin.error("复制失败");
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取剩余时间 */
|
||||
function getTimeRemaining(expiryTime: string): string {
|
||||
const now = Time.now();
|
||||
const expiry = new Date(expiryTime).getTime();
|
||||
const diff = expiry - now;
|
||||
if (diff <= 0) {
|
||||
return "已过期";
|
||||
}
|
||||
const days = Math.floor(diff / Time.D);
|
||||
const hours = Math.floor((diff % Time.D) / Time.H);
|
||||
const minutes = Math.floor((diff % Time.H) / Time.M);
|
||||
|
||||
if (0 < days) {
|
||||
return `剩余 ${days} 天 ${hours} 小时`;
|
||||
}
|
||||
if (0 < hours) {
|
||||
return `剩余 ${hours} 小时 ${minutes} 分钟`;
|
||||
}
|
||||
return `剩余 ${minutes} 分钟`;
|
||||
}
|
||||
|
||||
// 初始化时加载历史记录
|
||||
onMounted(fileHistoryStore.loadHistory);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.file-history-list {
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
|
||||
.empty {
|
||||
font-size: .85rem;
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.list {
|
||||
|
||||
.item {
|
||||
border: .1rem solid #E2E8F0;
|
||||
display: flex;
|
||||
padding: .5rem 1rem;
|
||||
background: #f7fafc;
|
||||
margin-bottom: .5rem;
|
||||
flex-direction: column;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--tui-shadow);
|
||||
background: #EDF2F7;
|
||||
border-color: #CBD5E0;
|
||||
}
|
||||
|
||||
.header {
|
||||
|
||||
.name-expiry {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.name {
|
||||
flex: 1;
|
||||
color: #2d3748;
|
||||
margin-bottom: .375rem;
|
||||
}
|
||||
|
||||
.size {
|
||||
font-size: .75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.id {
|
||||
font-size: .85rem;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: flex;
|
||||
margin-top: .5rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.expiry {
|
||||
font-size: .75rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
|
||||
.download {
|
||||
width: 5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
335
src/components/FileUpload.vue
Normal file
335
src/components/FileUpload.vue
Normal file
@ -0,0 +1,335 @@
|
||||
<template>
|
||||
<div class="file-upload">
|
||||
<div class="header">
|
||||
<h4 class="title">文件上传</h4>
|
||||
<t-button
|
||||
v-if="0 < uploadList.length"
|
||||
theme="default"
|
||||
variant="outline"
|
||||
@click="uploadList = []"
|
||||
>
|
||||
<template #icon>
|
||||
<icon name="trash" />
|
||||
</template>
|
||||
<span class="word-space">清空列表</span>
|
||||
</t-button>
|
||||
</div>
|
||||
<div
|
||||
class="upload-area cur-pointer"
|
||||
:class="{ dragover: isDragging }"
|
||||
@dragover.prevent="isDragging = true"
|
||||
@dragleave.prevent="isDragging = false"
|
||||
@drop.prevent="drop"
|
||||
@click="triggerFileInput"
|
||||
>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
multiple
|
||||
accept="*/*"
|
||||
@change="fileSelect"
|
||||
/>
|
||||
<icon class="icon" name="upload" :scale="3" />
|
||||
<p>点击选择文件或拖拽文件到此处</p>
|
||||
<span class="hint">支持多文件上传</span>
|
||||
<div class="ttl-selector" @click.stop>
|
||||
<span class="label">缓存时长:</span>
|
||||
<t-radio-group v-model="ttl" variant="primary-filled" size="small">
|
||||
<t-radio-button :value="Time.H * 3">3 小时</t-radio-button>
|
||||
<t-radio-button :value="Time.H * 6">6 小时</t-radio-button>
|
||||
<t-radio-button :value="Time.D">1 天</t-radio-button>
|
||||
<t-radio-button :value="Time.D * 3">3 天</t-radio-button>
|
||||
</t-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="0 < uploadList.length" class="upload-list">
|
||||
<div
|
||||
v-for="item in uploadList"
|
||||
:key="item.id"
|
||||
class="item"
|
||||
:class="item.status"
|
||||
>
|
||||
<div class="progress-status">
|
||||
<t-progress
|
||||
class="progress"
|
||||
:percentage="item.progress"
|
||||
:status="item.status === Status.SUCCESS ? 'success' : item.status === Status.ERROR ? 'warning' : 'active'"
|
||||
theme="plump"
|
||||
/>
|
||||
<span class="status" :class="item.status">
|
||||
<template v-if="item.status === Status.UPLOADING">上传中...</template>
|
||||
<template v-else-if="item.status === Status.SUCCESS">上传成功</template>
|
||||
<template v-else v-text="item.message || '上传失败'"></template>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="item.status === Status.UPLOADING" class="info">
|
||||
<span class="size">
|
||||
<span v-text="IOSize.format(item.loaded)"></span>
|
||||
<span v-text="' / '"></span>
|
||||
<span v-text="IOSize.format(item.totalSize)"></span>
|
||||
</span>
|
||||
<span v-text="`${IOSize.format(item.speed)} / s`"></span>
|
||||
</div>
|
||||
<div class="files selectable">
|
||||
<p class="name clip-text" v-for="(file, index) in item.files" :key="index" v-text="file.name"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { MessagePlugin } from "tdesign-vue-next";
|
||||
import TempFileAPI from "@/api/TempFileAPI.ts";
|
||||
import { useFileHistoryStore } from "@/store/fileHistory";
|
||||
import { type Item, Status } from "@/type/TempFile.ts";
|
||||
import { Icon, IOSize, Time } from "timi-web";
|
||||
|
||||
const fileHistoryStore = useFileHistoryStore();
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
const isDragging = ref(false);
|
||||
const uploadList = ref<Item[]>([]);
|
||||
const ttl = ref(Time.H * 6); // 默认 6 小时
|
||||
|
||||
/** 文件选择 */
|
||||
function triggerFileInput() {
|
||||
fileInput.value?.click();
|
||||
}
|
||||
|
||||
/** 文件选择 */
|
||||
function fileSelect(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const files = Array.from(target.files || []);
|
||||
if (files.length > 0) {
|
||||
upload(files);
|
||||
}
|
||||
target.value = "";
|
||||
}
|
||||
|
||||
/** 文件拖放 */
|
||||
function drop(e: DragEvent) {
|
||||
isDragging.value = false;
|
||||
const files = Array.from(e.dataTransfer?.files || []);
|
||||
if (files.length > 0) {
|
||||
upload(files);
|
||||
}
|
||||
}
|
||||
|
||||
/** 上传文件 */
|
||||
async function upload(files: File[]) {
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
// 创建上传任务
|
||||
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
|
||||
const task: Item = {
|
||||
files,
|
||||
id: `${Time.now()}_${Math.random()}`,
|
||||
progress: 0,
|
||||
speed: 0,
|
||||
loaded: 0,
|
||||
totalSize,
|
||||
status: Status.UPLOADING
|
||||
};
|
||||
// 添加到上传列表开头
|
||||
uploadList.value.unshift(task);
|
||||
let startTime = Time.now();
|
||||
let uploadedSize = 0;
|
||||
try {
|
||||
// 上传文件
|
||||
const responses = await TempFileAPI.upload(files, (progressEvent) => {
|
||||
if (progressEvent.lengthComputable && progressEvent.total) {
|
||||
const totalPercent = Math.round((progressEvent.loaded / progressEvent.total) * 100);
|
||||
|
||||
// 计算速度
|
||||
const currentTime = Time.now();
|
||||
const timeElapsed = (currentTime - startTime) / 1000;
|
||||
const bytesUploaded = progressEvent.loaded - uploadedSize;
|
||||
const speed = 0 < timeElapsed ? bytesUploaded / timeElapsed : 0;
|
||||
|
||||
// 更新任务进度
|
||||
const uploadTask = uploadList.value.find(u => u.id === task.id);
|
||||
if (uploadTask) {
|
||||
uploadTask.progress = totalPercent;
|
||||
uploadTask.speed = speed;
|
||||
uploadTask.loaded = progressEvent.loaded;
|
||||
}
|
||||
uploadedSize = progressEvent.loaded;
|
||||
startTime = currentTime;
|
||||
}
|
||||
}, ttl.value);
|
||||
// 更新成功状态
|
||||
const uploadTask = uploadList.value.find(u => u.id === task.id);
|
||||
if (uploadTask) {
|
||||
uploadTask.status = Status.SUCCESS;
|
||||
uploadTask.progress = 100;
|
||||
uploadTask.loaded = totalSize;
|
||||
uploadTask.fileIds = responses.map(r => r.id);
|
||||
}
|
||||
// 保存所有文件到历史记录
|
||||
responses.forEach((response, index) => {
|
||||
if (index < files.length) {
|
||||
fileHistoryStore.addFile({
|
||||
id: response.id,
|
||||
name: files[index]!.name,
|
||||
size: files[index]!.size,
|
||||
uploadTime: new Date().toISOString(),
|
||||
expiryTime: response.expireAt
|
||||
});
|
||||
}
|
||||
});
|
||||
await MessagePlugin.success(`成功上传 ${responses.length} 个文件`);
|
||||
} catch (error: any) {
|
||||
// 更新失败状态
|
||||
const uploadTask = uploadList.value.find(u => u.id === task.id);
|
||||
if (uploadTask) {
|
||||
uploadTask.status = Status.ERROR;
|
||||
uploadTask.message = error.message || "上传失败";
|
||||
}
|
||||
await MessagePlugin.error(`上传失败: ${error.message || "未知错误"}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.file-upload {
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
border: .125rem dashed #CBD5E0;
|
||||
padding: 1rem 2.5rem;
|
||||
text-align: center;
|
||||
transition: all .3s ease;
|
||||
background: #F7FAFC;
|
||||
|
||||
&:hover {
|
||||
background: #EDF2F7;
|
||||
border-color: #667EEA;
|
||||
}
|
||||
|
||||
&.dragover {
|
||||
transform: scale(1.02);
|
||||
background: #E6FFFA;
|
||||
border-color: #667EEA;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #2d3748;
|
||||
font-size: 1rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #718096;
|
||||
font-size: .75rem;
|
||||
}
|
||||
|
||||
.ttl-selector {
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.label {
|
||||
color: #4a5568;
|
||||
font-size: .875rem;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-list {
|
||||
margin-top: 1.25rem;
|
||||
|
||||
.item {
|
||||
border: .0625rem solid #e2e8f0;
|
||||
padding: .9375rem;
|
||||
transition: all 300ms ease;
|
||||
background: #F7FAFC;
|
||||
margin-bottom: .625rem;
|
||||
|
||||
&.success {
|
||||
background: #F0FFF4;
|
||||
border-color: #48BB78;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: #F56565;
|
||||
background: #FFF5F5;
|
||||
}
|
||||
|
||||
.progress-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.progress {
|
||||
flex: 1;
|
||||
margin-right: 1rem;
|
||||
|
||||
:deep(.t-progress__inner),
|
||||
:deep(.t-progress__bar) {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: .875rem;
|
||||
font-weight: 500;
|
||||
|
||||
&.UPLOADING {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
&.SUCCESS {
|
||||
color: #48BB78;
|
||||
}
|
||||
|
||||
&.ERROR {
|
||||
color: #f56565;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
color: #718096;
|
||||
display: flex;
|
||||
font-size: .8125rem;
|
||||
margin-top: .5rem;
|
||||
justify-content: space-between;
|
||||
|
||||
.size {
|
||||
color: #718096;
|
||||
font-size: .875rem;
|
||||
margin-right: .625rem;
|
||||
}
|
||||
}
|
||||
|
||||
.files {
|
||||
margin-top: .5rem;
|
||||
|
||||
.name {
|
||||
margin: 0;
|
||||
font-size: .85rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
29
src/main.ts
Normal file
29
src/main.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
import { axios, Network, VDraggable, VPopup } from "timi-web";
|
||||
|
||||
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);
|
||||
// ---------- Vue ----------
|
||||
const pinia = createPinia();
|
||||
const app = createApp(Root);
|
||||
app.use(pinia);
|
||||
app.directive("draggable", VDraggable as any);
|
||||
app.directive("popup", VPopup as any);
|
||||
app.mount("#root");
|
||||
66
src/store/fileHistory.ts
Normal file
66
src/store/fileHistory.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { defineStore } from "pinia";
|
||||
import type { History } from "@/type/TempFile.ts";
|
||||
import { Storage } from "../../../timi-web";
|
||||
|
||||
const FILE_EXPIRY_HOURS = 6;
|
||||
const HISTORY_STORAGE_KEY = "uploadHistory";
|
||||
const MAX_HISTORY_COUNT = 50;
|
||||
|
||||
export const useFileHistoryStore = defineStore("fileHistory", () => {
|
||||
|
||||
// 状态
|
||||
const fileHistory = ref<History[]>([]);
|
||||
|
||||
/** 添加文件到历史记录 */
|
||||
function addFile(file: History) {
|
||||
fileHistory.value.unshift(file);
|
||||
// 限制数量
|
||||
if (MAX_HISTORY_COUNT < fileHistory.value.length) {
|
||||
fileHistory.value = fileHistory.value.slice(0, MAX_HISTORY_COUNT);
|
||||
}
|
||||
// 保存到 localStorage
|
||||
saveToLocalStorage();
|
||||
}
|
||||
|
||||
/** 加载历史记录 */
|
||||
function loadHistory() {
|
||||
if (Storage.has(HISTORY_STORAGE_KEY)) {
|
||||
try {
|
||||
fileHistory.value = Storage.getJSON(HISTORY_STORAGE_KEY);
|
||||
} catch (error) {
|
||||
console.error("加载历史记录失败:", error);
|
||||
fileHistory.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 清理过期文件 */
|
||||
function cleanExpiredFiles() {
|
||||
const now = new Date();
|
||||
const originalLength = fileHistory.value.length;
|
||||
|
||||
fileHistory.value = fileHistory.value.filter(file => {
|
||||
if (!file.expiryTime) {
|
||||
const uploadTime = file.uploadTime ? new Date(file.uploadTime) : now;
|
||||
file.expiryTime = new Date(uploadTime.getTime() + FILE_EXPIRY_HOURS * 60 * 60 * 1000).toISOString();
|
||||
}
|
||||
return new Date(file.expiryTime) > now;
|
||||
});
|
||||
|
||||
if (fileHistory.value.length !== originalLength) {
|
||||
saveToLocalStorage();
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存到 localStorage */
|
||||
function saveToLocalStorage() {
|
||||
Storage.setJSON(HISTORY_STORAGE_KEY, fileHistory.value);
|
||||
}
|
||||
|
||||
return {
|
||||
fileHistory,
|
||||
addFile,
|
||||
loadHistory,
|
||||
cleanExpiredFiles
|
||||
};
|
||||
});
|
||||
34
src/type/TempFile.ts
Normal file
34
src/type/TempFile.ts
Normal file
@ -0,0 +1,34 @@
|
||||
/** 临时文件响应 */
|
||||
export type TempFileResponse = {
|
||||
id: string;
|
||||
expireAt: string;
|
||||
}
|
||||
|
||||
/** 上传状态 */
|
||||
export const Status = {
|
||||
UPLOADING: "UPLOADING",
|
||||
SUCCESS: "SUCCESS",
|
||||
ERROR: "ERROR"
|
||||
} as const;
|
||||
|
||||
/** 上传任务项 */
|
||||
export type Item = {
|
||||
files: File[];
|
||||
id: string;
|
||||
progress: number;
|
||||
speed: number;
|
||||
loaded: number;
|
||||
totalSize: number;
|
||||
status: typeof Status[keyof typeof Status];
|
||||
message?: string;
|
||||
fileIds?: string[];
|
||||
}
|
||||
|
||||
/** 文件历史记录 */
|
||||
export type History = {
|
||||
id: string;
|
||||
name: string;
|
||||
size: number;
|
||||
uploadTime: string;
|
||||
expiryTime: string;
|
||||
}
|
||||
87
src/views/Home.vue
Normal file
87
src/views/Home.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<h1 class="title">文件中转站</h1>
|
||||
<t-alert theme="info" class="tips">
|
||||
<template #message>
|
||||
<h3 class="title">使用须知</h3>
|
||||
<ul class="list">
|
||||
<li v-text="'已上传文件仅自己可见,分享 ID 可直接下载'"></li>
|
||||
<li>
|
||||
<span>每个 IP 限制缓存</span>
|
||||
<strong class="word-space pink">10 GB</strong>
|
||||
<span>容量</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</t-alert>
|
||||
<div class="content">
|
||||
<div class="panel left">
|
||||
<file-upload />
|
||||
</div>
|
||||
<div class="panel right">
|
||||
<file-download />
|
||||
<file-history-list />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import FileUpload from "@/components/FileUpload.vue";
|
||||
import FileDownload from "@/components/FileDownload.vue";
|
||||
import FileHistoryList from "@/components/FileHistoryList.vue";
|
||||
import { useFileHistoryStore } from "@/store/fileHistory.ts";
|
||||
import { Time } from "../../../timi-web";
|
||||
|
||||
const fileHistoryStore = useFileHistoryStore();
|
||||
onMounted(() => setInterval(fileHistoryStore.cleanExpiredFiles, Time.S));
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.home {
|
||||
width: 100%;
|
||||
|
||||
> .title {
|
||||
text-align: center;
|
||||
margin-bottom: 1.875rem;
|
||||
}
|
||||
|
||||
.tips {
|
||||
margin: 0 2rem 1.25rem 2rem;
|
||||
|
||||
.title {
|
||||
margin: 0 0 .5rem 0;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
border-top: var(--tui-border);
|
||||
border-bottom: var(--tui-border);
|
||||
|
||||
.panel {
|
||||
flex: 1;
|
||||
|
||||
&:first-child {
|
||||
border-right: var(--tui-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 45rem) {
|
||||
.home .content {
|
||||
flex-direction: column;
|
||||
|
||||
.panel:first-child {
|
||||
border-right: none;
|
||||
border-bottom: var(--tui-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
13
src/vite-env.d.ts
vendored
Normal file
13
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
28
tsconfig.app.json
Normal file
28
tsconfig.app.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"types": [
|
||||
"vite/client"
|
||||
],
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"node_modules/tdesign-vue-next/global.d.ts"
|
||||
]
|
||||
}
|
||||
11
tsconfig.json
Normal file
11
tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
30
tsconfig.node.json
Normal file
30
tsconfig.node.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": [
|
||||
"ES2023"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
87
vite.config.ts
Normal file
87
vite.config.ts
Normal file
@ -0,0 +1,87 @@
|
||||
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";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
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"]
|
||||
},
|
||||
build: {
|
||||
sourcemap: false,
|
||||
minify: "terser",
|
||||
terserOptions: {
|
||||
compress: {
|
||||
// eslint-disable-next-line camelcase
|
||||
drop_console: true,
|
||||
// eslint-disable-next-line camelcase
|
||||
drop_debugger: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user