Initial 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 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
145
.gitignore
vendored
145
.gitignore
vendored
@ -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
|
||||||
|
|||||||
19
README.md
19
README.md
@ -1,3 +1,18 @@
|
|||||||
# space
|
# 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
13
index.html
Normal 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>
|
||||||
44
package.json
Normal file
44
package.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "space",
|
||||||
|
"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",
|
||||||
|
"pinia": "^3.0.1",
|
||||||
|
"timi-web": "link:..\\timi-web",
|
||||||
|
"timi-tdesign-pc": "link:..\\timi-tdesign-pc",
|
||||||
|
"tdesign-vue-next": "1.13.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"
|
||||||
|
}
|
||||||
|
}
|
||||||
9704
pnpm-lock.yaml
generated
Normal file
9704
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Sitemap: ./sitemap.xml
|
||||||
25
src/Root.vue
Normal file
25
src/Root.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<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/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { BEFlowerFall, Popup, SettingKey, SettingMapper } from "timi-web";
|
||||||
|
import { RootLayout } from "timi-tdesign-pc";
|
||||||
|
|
||||||
|
const isReady = ref(false);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await SettingMapper.loadSetting({ key: SettingKey.FMC_MAX_BIND });
|
||||||
|
isReady.value = true;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.background {
|
||||||
|
background: url("@/assets/img/main.png") fixed right bottom;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
11
src/api/DeveloperAPI.ts
Normal file
11
src/api/DeveloperAPI.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { axios, Developer } from "timi-web";
|
||||||
|
|
||||||
|
const BASE_URI = "/git/developer";
|
||||||
|
|
||||||
|
async function update(developer: Developer): Promise<void> {
|
||||||
|
return axios.post(`${BASE_URI}/update`, developer);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
update
|
||||||
|
};
|
||||||
20
src/api/MinecraftAPI.ts
Normal file
20
src/api/MinecraftAPI.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { axios } from "timi-web";
|
||||||
|
import { MinecraftPlayer } from "@/types/MinecraftPlayer.ts";
|
||||||
|
|
||||||
|
async function list(): Promise<MinecraftPlayer[]> {
|
||||||
|
return axios.post("/fmc/player/list");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bind(name: string): Promise<void> {
|
||||||
|
return axios.post("/fmc/player/bind", { name });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unbind(id: number): Promise<void> {
|
||||||
|
return axios.post("/fmc/player/unbind", { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
list,
|
||||||
|
bind,
|
||||||
|
unbind
|
||||||
|
};
|
||||||
105
src/api/UserAPI.ts
Normal file
105
src/api/UserAPI.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { axios, CommentBizType, CommentReplyView, CommentView, PageResult, SettingKey, SettingMapper, Toolkit, userStore } from "timi-web";
|
||||||
|
import { UserCommentPage, UserPrivacy, UserRequest } from "@/types/User.ts";
|
||||||
|
import { CommentReplyPage } from "../../../timi-web/src";
|
||||||
|
|
||||||
|
async function updateProfile(request: UserRequest): Promise<void> {
|
||||||
|
return axios({
|
||||||
|
method: "POST",
|
||||||
|
url: axios.defaults.baseURL + "/user/profile/update",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
"Token": userStore.loginUser?.token?.value
|
||||||
|
},
|
||||||
|
data: Toolkit.toFormData(request)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取隐私控制
|
||||||
|
*
|
||||||
|
* @returns 隐私控制
|
||||||
|
*/
|
||||||
|
async function getPrivacy(): Promise<UserPrivacy> {
|
||||||
|
return axios.post("/user/privacy");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新隐私控制
|
||||||
|
*
|
||||||
|
* @param userPrivacy 隐私控制
|
||||||
|
* @returns true 为更新成功
|
||||||
|
*/
|
||||||
|
async function updatePrivacy(userPrivacy: UserPrivacy): Promise<boolean> {
|
||||||
|
return axios.post("/user/privacy/update", userPrivacy);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCommentTargetUrl(comment: CommentView) {
|
||||||
|
let url: string;
|
||||||
|
switch (comment.bizType) {
|
||||||
|
case CommentBizType.ARTICLE:
|
||||||
|
url = `${SettingMapper.getDomainLink(SettingKey.DOMAIN_BLOG)}/aid${comment.bizId}.html`;
|
||||||
|
break;
|
||||||
|
case CommentBizType.GIT_ISSUE:
|
||||||
|
url = `${SettingMapper.getDomainLink(SettingKey.DOMAIN_GIT)}/${(comment.repository as any).name}/issues/${comment.bizId}`;
|
||||||
|
break;
|
||||||
|
case CommentBizType.GIT_MERGE:
|
||||||
|
url = `${SettingMapper.getDomainLink(SettingKey.DOMAIN_GIT)}/${(comment.repository as any).name}/merges/${comment.bizId}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listComment(page: UserCommentPage): Promise<PageResult<CommentView>> {
|
||||||
|
return axios.post("/user/comment/list", page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除评论
|
||||||
|
*
|
||||||
|
* @param commentId 评论 ID
|
||||||
|
* @returns true 为删除成功
|
||||||
|
*/
|
||||||
|
async function deleteComment(commentId: number): Promise<boolean> {
|
||||||
|
return axios.post("/user/comment/delete", { commentId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取评论下的回复
|
||||||
|
*
|
||||||
|
* @param page 回复分页
|
||||||
|
*/
|
||||||
|
async function listCommentReply(page: CommentReplyPage): Promise<PageResult<CommentReplyView>> {
|
||||||
|
return axios.post("/user/comment/reply/list", page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除回复
|
||||||
|
*
|
||||||
|
* @param replyId 回复 ID
|
||||||
|
* @returns true 为删除成功
|
||||||
|
*/
|
||||||
|
async function deleteCommentReply(replyId: number): Promise<boolean> {
|
||||||
|
return axios.post("/user/comment/reply/delete", { replyId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 忽略被回复
|
||||||
|
*
|
||||||
|
* @param replyId 回复 ID
|
||||||
|
* @returns true 为删除成功
|
||||||
|
*/
|
||||||
|
async function ignoreCommentReply(replyId: number): Promise<boolean> {
|
||||||
|
return axios.post("/user/comment/reply/ignore", { replyId });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
updateProfile,
|
||||||
|
getPrivacy,
|
||||||
|
updatePrivacy,
|
||||||
|
getCommentTargetUrl,
|
||||||
|
listComment,
|
||||||
|
deleteComment,
|
||||||
|
listCommentReply,
|
||||||
|
deleteCommentReply,
|
||||||
|
ignoreCommentReply
|
||||||
|
};
|
||||||
BIN
src/assets/img/main.png
Normal file
BIN
src/assets/img/main.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
611
src/components/AvatarEditor.vue
Normal file
611
src/components/AvatarEditor.vue
Normal file
@ -0,0 +1,611 @@
|
|||||||
|
<template>
|
||||||
|
<div class="avatar-editor no-select">
|
||||||
|
<div class="header">
|
||||||
|
<div class="file">
|
||||||
|
<t-input class="name" type="text" v-model="fileName" placeholder="" readonly />
|
||||||
|
<t-button>选择文件</t-button>
|
||||||
|
<input ref="file" class="mask" type="file" accept="image/png" @change="onSelectedFile($event)" />
|
||||||
|
</div>
|
||||||
|
<t-button
|
||||||
|
class="reset"
|
||||||
|
theme="default"
|
||||||
|
@click="reset()"
|
||||||
|
>还原默认</t-button>
|
||||||
|
<t-button
|
||||||
|
class="cancel"
|
||||||
|
v-show="avatarAttr.isLoad"
|
||||||
|
theme="default"
|
||||||
|
@click="cancel()"
|
||||||
|
>取消</t-button>
|
||||||
|
</div>
|
||||||
|
<div class="content" v-show="avatarAttr.isLoad">
|
||||||
|
<!-- 编辑器 -->
|
||||||
|
<div ref="editor" class="editor">
|
||||||
|
<img ref="avatar" class="img" :class="(<any>ImageType)[rendererType]" :src="avatarAttr.data" />
|
||||||
|
<div ref="selector" class="selector" v-draggable="selectorConfig">
|
||||||
|
<div class="corner lt" v-draggable="selectorConfigLT"></div>
|
||||||
|
<div class="corner rt" v-draggable="selectorConfigRT"></div>
|
||||||
|
<div class="corner lb" v-draggable="selectorConfigLB"></div>
|
||||||
|
<div class="corner rb" v-draggable="selectorConfigRB"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="preview">
|
||||||
|
<!-- 预览 -->
|
||||||
|
<div class="item x128">
|
||||||
|
<img class="img" :class="(<any>ImageType)[rendererType]" :style="previewCSS128" :src="avatarAttr.data" />
|
||||||
|
</div>
|
||||||
|
<div class="item x64">
|
||||||
|
<img class="img" :class="(<any>ImageType)[rendererType]" :style="previewCSS64" :src="avatarAttr.data" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<canvas ref="canvas" class="canvas" width="256" height="256" hidden></canvas>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { CommonAPI, ImageType, IOSize, PublicResources, SettingKey, SettingMapper } from "timi-web";
|
||||||
|
|
||||||
|
// _________________256px___________________
|
||||||
|
// | |<--- 编辑器容器
|
||||||
|
// | |
|
||||||
|
// | Avatar Editor |
|
||||||
|
// |1_______________________________________2|
|
||||||
|
// || ___________________ |<---- 图像
|
||||||
|
// || | | ||
|
||||||
|
// || | |<-------------- 选区
|
||||||
|
// || | | ||
|
||||||
|
// || Image | Selector | || 256px
|
||||||
|
// || | | ||
|
||||||
|
// || | | ||
|
||||||
|
// || |___________________| ||
|
||||||
|
// ||_______________________________________||
|
||||||
|
// |3 4|
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// |_________________________________________|
|
||||||
|
//
|
||||||
|
// 注释:
|
||||||
|
// 1. 相对于编辑器的图像左上端点 minX, minY <-- 相对于编辑器,不是选区限位
|
||||||
|
// 2. 相对于编辑器的图像右上端点 maxX, minY
|
||||||
|
// 3. 相对于编辑器的图像左下端点 minX, maxY
|
||||||
|
// 4. 相对于编辑器的图像右下端点 maxX, maxY
|
||||||
|
//
|
||||||
|
// 头像属性
|
||||||
|
type AvatarAttr = {
|
||||||
|
// 端点位置坐标(相对于编辑器)
|
||||||
|
minX: number;
|
||||||
|
minY: number;
|
||||||
|
maxX: number;
|
||||||
|
maxY: number;
|
||||||
|
|
||||||
|
width: number; // 图像实际宽度
|
||||||
|
height: number; // 图像实际高度
|
||||||
|
scale: number; // 从实际尺寸渲染到编辑器的缩放
|
||||||
|
isHorizontal?: boolean;
|
||||||
|
isVertical?: boolean;
|
||||||
|
isSquare?: boolean; // 是否正方形
|
||||||
|
data?: string;
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
isLoad: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1_________________________________________
|
||||||
|
// | 2,5___________________ | <-- 实际图像
|
||||||
|
// | | | |
|
||||||
|
// | | | |
|
||||||
|
// | Image | |<------ 选区,此时选区最大化选择图像右侧
|
||||||
|
// | | Selector |6 |7
|
||||||
|
// | | | |
|
||||||
|
// | | | |
|
||||||
|
// | 4___________________| |
|
||||||
|
// |_________________________________________|
|
||||||
|
// 3
|
||||||
|
//
|
||||||
|
// 注释:
|
||||||
|
// 1. 相对于编辑器的选区限位左上端点 minX, minY(限第 5 点的实时坐标)
|
||||||
|
// 2. 相对于编辑器的选区限位右上端点 maxX, minY
|
||||||
|
// 3. 相对于编辑器的选区限位左下端点 minX, maxY
|
||||||
|
// 4. 相对于编辑器的选区限位右下端点 maxX, maxY
|
||||||
|
// 5. 选区实时所在坐标 x, y(相对于编辑器)
|
||||||
|
// 6. 选区实时尺寸 size
|
||||||
|
// **. 选区触发坐标尺寸调整触发坐标 xy 为相对于选区位置(5),比如选区最下角就是 [0, 选区高度]
|
||||||
|
//
|
||||||
|
// 选区属性
|
||||||
|
type SelectorAttr = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
size: number;
|
||||||
|
|
||||||
|
minX: number;
|
||||||
|
minY: number;
|
||||||
|
maxX: number;
|
||||||
|
maxY: number;
|
||||||
|
maxSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选区对预览区(包括渲染区 - Canvas 画板)的坐标和缩放参数 */
|
||||||
|
type SelectorScaleAttr = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
scale: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// props
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
rendererType?: ImageType
|
||||||
|
}>(), {
|
||||||
|
rendererType: ImageType.PIXELATED
|
||||||
|
});
|
||||||
|
const { rendererType } = toRefs(props);
|
||||||
|
|
||||||
|
// ref
|
||||||
|
const img = ref<HTMLImageElement>();
|
||||||
|
const file = ref<HTMLInputElement>();
|
||||||
|
const canvas = ref<HTMLCanvasElement>();
|
||||||
|
const editor = ref<HTMLElement>();
|
||||||
|
const avatar = ref<HTMLImageElement>();
|
||||||
|
const selector = ref<HTMLElement>();
|
||||||
|
const fileName = ref<string>("");
|
||||||
|
const avatarAttr = reactive<AvatarAttr>({
|
||||||
|
minX: 0,
|
||||||
|
minY: 0,
|
||||||
|
maxX: 0,
|
||||||
|
maxY: 0,
|
||||||
|
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
scale: 1,
|
||||||
|
isLoad: false
|
||||||
|
});
|
||||||
|
const selectorAttr = reactive<SelectorAttr>({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
size: 0,
|
||||||
|
|
||||||
|
minX: 0,
|
||||||
|
minY: 0,
|
||||||
|
maxX: 0,
|
||||||
|
maxY: 0,
|
||||||
|
maxSize: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// computed
|
||||||
|
const previewCSS64 = computed(() => {
|
||||||
|
const r = getPreviewCSS(64);
|
||||||
|
return `top: ${r.y}px; left: ${r.x}px; transform: scale(${r.scale})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewCSS128 = computed(() => {
|
||||||
|
const r = getPreviewCSS(128);
|
||||||
|
return `top: ${r.y}px; left: ${r.x}px; transform: scale(${r.scale})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectorConfig = computed(() => {
|
||||||
|
return {
|
||||||
|
// 点击选区时忽略本事件
|
||||||
|
interruptWhen: (e: MouseEvent) => (e.target as HTMLElement).classList.contains("corner"),
|
||||||
|
onDragging: (_e: MouseEvent, px: number, py: number) => {
|
||||||
|
// X 轴限位
|
||||||
|
if (px < selectorAttr.minX) {
|
||||||
|
selectorAttr.x = selectorAttr.minX;
|
||||||
|
} else if (selectorAttr.maxX < px) {
|
||||||
|
selectorAttr.x = selectorAttr.maxX;
|
||||||
|
} else {
|
||||||
|
selectorAttr.x = px;
|
||||||
|
}
|
||||||
|
// Y 轴限位
|
||||||
|
if (py < selectorAttr.minY) {
|
||||||
|
selectorAttr.y = selectorAttr.minY;
|
||||||
|
} else if (selectorAttr.maxY < py) {
|
||||||
|
selectorAttr.y = selectorAttr.maxY;
|
||||||
|
} else {
|
||||||
|
selectorAttr.y = py;
|
||||||
|
}
|
||||||
|
if (selector.value) {
|
||||||
|
selector.value.style.top = selectorAttr.y + "px";
|
||||||
|
selector.value.style.left = selectorAttr.x + "px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectorConfigLT = computed(() => {
|
||||||
|
let x, y, width;
|
||||||
|
let oldAttr: SelectorAttr;
|
||||||
|
return {
|
||||||
|
onMouseDown: () => oldAttr = { ...selectorAttr },
|
||||||
|
onDragging: (_e: MouseEvent, _px: number, _py: number, dx: number, dy: number) => {
|
||||||
|
if (!selector.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
width = Math.max(oldAttr.size - dx, oldAttr.size - dy, 24);
|
||||||
|
// 限尺寸
|
||||||
|
if (oldAttr.x + oldAttr.size - width < avatarAttr.minX || oldAttr.y + oldAttr.size - width < avatarAttr.minY) {
|
||||||
|
width = Math.min(oldAttr.x + oldAttr.size - avatarAttr.minX, oldAttr.y + oldAttr.size - avatarAttr.minY);
|
||||||
|
}
|
||||||
|
x = oldAttr.x + oldAttr.size - width;
|
||||||
|
y = oldAttr.y + oldAttr.size - width;
|
||||||
|
selector.value.style.left = x + "px";
|
||||||
|
selector.value.style.top = y + "px";
|
||||||
|
selector.value.style.width = selector.value.style.height = width + "px";
|
||||||
|
// 更新限位
|
||||||
|
selectorAttr.x = x;
|
||||||
|
selectorAttr.y = y;
|
||||||
|
selectorAttr.size = width;
|
||||||
|
selectorAttr.maxX = avatarAttr.maxX - width;
|
||||||
|
selectorAttr.maxY = avatarAttr.maxY - width;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectorConfigRT = computed(() => {
|
||||||
|
let y, size;
|
||||||
|
let oldAttr: SelectorAttr;
|
||||||
|
return {
|
||||||
|
onMouseDown: () => oldAttr = { ...selectorAttr },
|
||||||
|
onDragging: (_e: MouseEvent, _px: number, _py: number, dx: number, dy: number) => {
|
||||||
|
if (!selector.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size = Math.max(oldAttr.size + dx, oldAttr.size - dy, 24);
|
||||||
|
// 限尺寸
|
||||||
|
if (oldAttr.y + oldAttr.size - size < avatarAttr.minY || avatarAttr.maxX < oldAttr.x + size) {
|
||||||
|
size = Math.min(oldAttr.y + oldAttr.size - avatarAttr.minY, avatarAttr.maxX - selector.value.offsetLeft);
|
||||||
|
}
|
||||||
|
y = oldAttr.y + oldAttr.size - size;
|
||||||
|
selector.value.style.top = y + "px";
|
||||||
|
selector.value.style.width = selector.value.style.height = size + "px";
|
||||||
|
// 更新限位
|
||||||
|
selectorAttr.y = y;
|
||||||
|
selectorAttr.size = size;
|
||||||
|
selectorAttr.maxX = avatarAttr.maxX - size;
|
||||||
|
selectorAttr.maxY = avatarAttr.maxY - size;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectorConfigLB = computed(() => {
|
||||||
|
let x, size;
|
||||||
|
let oldAttr: SelectorAttr;
|
||||||
|
return {
|
||||||
|
onMouseDown: () => oldAttr = { ...selectorAttr },
|
||||||
|
onDragging: (_e: MouseEvent, _px: number, _py: number, dx: number, dy: number) => {
|
||||||
|
if (!selector.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size = Math.max(oldAttr.size - dx, oldAttr.size + dy, 24);
|
||||||
|
// 限尺寸
|
||||||
|
if (oldAttr.x + oldAttr.size - size < avatarAttr.minX || avatarAttr.maxY < oldAttr.y + size) {
|
||||||
|
size = Math.min(oldAttr.x + oldAttr.size - avatarAttr.minX, avatarAttr.maxY - selector.value.offsetTop);
|
||||||
|
}
|
||||||
|
x = oldAttr.x + oldAttr.size - size;
|
||||||
|
selector.value.style.left = x + "px";
|
||||||
|
selector.value.style.width = selector.value.style.height = size + "px";
|
||||||
|
// 更新限位
|
||||||
|
selectorAttr.x = x;
|
||||||
|
selectorAttr.size = size;
|
||||||
|
selectorAttr.maxX = avatarAttr.maxX - size;
|
||||||
|
selectorAttr.maxY = avatarAttr.maxY - size;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectorConfigRB = computed(() => {
|
||||||
|
let size;
|
||||||
|
return {
|
||||||
|
onDragging: (_e: MouseEvent, px: number, py: number) => {
|
||||||
|
if (!selector.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
px += 10;
|
||||||
|
py += 10;
|
||||||
|
// 相对选区最大偏移
|
||||||
|
size = Math.max(px, py, 24);
|
||||||
|
// 计算相对编辑器最大偏移
|
||||||
|
const maxX = selector.value.offsetLeft + size;
|
||||||
|
const maxY = selector.value.offsetTop + size;
|
||||||
|
if (avatarAttr.maxX < maxX || avatarAttr.maxY < maxY) {
|
||||||
|
size = Math.min(avatarAttr.maxX - selector.value.offsetLeft, avatarAttr.maxY - selector.value.offsetTop);
|
||||||
|
}
|
||||||
|
selector.value.style.width = selector.value.style.height = size + "px";
|
||||||
|
// 更新限位
|
||||||
|
selectorAttr.size = size;
|
||||||
|
selectorAttr.maxX = avatarAttr.maxX - size;
|
||||||
|
selectorAttr.maxY = avatarAttr.maxY - size;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// method
|
||||||
|
const getPreviewCSS = (previewSize: number): SelectorScaleAttr => {
|
||||||
|
const selectorScale = selectorAttr.maxSize / selectorAttr.size;
|
||||||
|
// 预览区缩放比
|
||||||
|
const previewScale = previewSize / (avatarAttr.isHorizontal ? avatarAttr.height : avatarAttr.width);
|
||||||
|
let x = 0;
|
||||||
|
if (selectorAttr.maxX !== selectorAttr.minX) {
|
||||||
|
// 选区限位最大值和最小值不能相等,因为要相减作为除数计算 X 轴移动百分比(如果相等说明选区 X 轴完全覆盖,不可移动)
|
||||||
|
const movePercent = (selectorAttr.x - selectorAttr.minX) / (selectorAttr.maxX - selectorAttr.minX);
|
||||||
|
x = movePercent * (avatarAttr.width * (previewScale * selectorScale) - previewSize);
|
||||||
|
}
|
||||||
|
let y = 0;
|
||||||
|
if (selectorAttr.maxY !== selectorAttr.minY) {
|
||||||
|
const movePercent = (selectorAttr.y - selectorAttr.minY) / (selectorAttr.maxY - selectorAttr.minY);
|
||||||
|
y = movePercent * (avatarAttr.height * (previewScale * selectorScale) - previewSize);
|
||||||
|
}
|
||||||
|
return { x: -x, y: -y, scale: previewScale * selectorScale };
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelectedFile = (event: any) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
avatarAttr.name = file.name;
|
||||||
|
if (file.type === "image/png") {
|
||||||
|
if (file.size <= IOSize.MB * 16) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.onload = () => {
|
||||||
|
if (reader.readyState === 2) {
|
||||||
|
if (reader.result) {
|
||||||
|
const data = reader.result as string;
|
||||||
|
parseImageData(data);
|
||||||
|
fileName.value = file.name;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析图片到编辑器
|
||||||
|
*
|
||||||
|
* @param data 可以是地址也可以是 Base64 数据
|
||||||
|
*/
|
||||||
|
const parseImageData = (data: string) => {
|
||||||
|
avatarAttr.data = data;
|
||||||
|
|
||||||
|
const newImg = new Image();
|
||||||
|
newImg.crossOrigin = "anonymous";
|
||||||
|
newImg.src = data;
|
||||||
|
newImg.onload = async () => {
|
||||||
|
if (!selector.value || !avatar.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基本属性
|
||||||
|
avatarAttr.width = newImg.width;
|
||||||
|
avatarAttr.height = newImg.height;
|
||||||
|
|
||||||
|
avatarAttr.isHorizontal = newImg.height < newImg.width;
|
||||||
|
avatarAttr.isVertical = newImg.height < newImg.width;
|
||||||
|
avatarAttr.isSquare = !avatarAttr.isHorizontal && !avatarAttr.isVertical;
|
||||||
|
// 选区限位
|
||||||
|
if (avatarAttr.isHorizontal) {
|
||||||
|
avatarAttr.scale = 256 / avatarAttr.width;
|
||||||
|
// 显示图像的端点坐标
|
||||||
|
avatarAttr.minX = 0;
|
||||||
|
avatarAttr.maxX = 256;
|
||||||
|
avatarAttr.minY = 128 - avatarAttr.height * avatarAttr.scale * .5;
|
||||||
|
avatarAttr.maxY = 128 + avatarAttr.height * avatarAttr.scale * .5;
|
||||||
|
// 选区端点
|
||||||
|
selectorAttr.minX = 0;
|
||||||
|
selectorAttr.minY = avatarAttr.minY;
|
||||||
|
selectorAttr.maxX = 256 - avatarAttr.height * avatarAttr.scale;
|
||||||
|
selectorAttr.maxY = avatarAttr.minY;
|
||||||
|
} else {
|
||||||
|
avatarAttr.scale = 256 / avatarAttr.height;
|
||||||
|
// 显示图像的端点坐标
|
||||||
|
avatarAttr.minX = 128 - avatarAttr.width * avatarAttr.scale * .5;
|
||||||
|
avatarAttr.maxX = 128 + avatarAttr.width * avatarAttr.scale * .5;
|
||||||
|
avatarAttr.minY = 0;
|
||||||
|
avatarAttr.maxY = 256;
|
||||||
|
// 选区端点
|
||||||
|
selectorAttr.minX = avatarAttr.minX;
|
||||||
|
selectorAttr.minY = 0;
|
||||||
|
selectorAttr.maxX = avatarAttr.minX;
|
||||||
|
selectorAttr.maxY = 256 - avatarAttr.width * avatarAttr.scale;
|
||||||
|
}
|
||||||
|
avatarAttr.isLoad = true;
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
// 图片尺寸
|
||||||
|
avatar.value.style.width = "auto";
|
||||||
|
avatar.value.style.height = "auto";
|
||||||
|
if ((avatarAttr.isSquare || avatarAttr.isHorizontal) && avatarAttr.width < 256) {
|
||||||
|
avatar.value.style.width = "100%";
|
||||||
|
}
|
||||||
|
if ((avatarAttr.isSquare || avatarAttr.isVertical) && avatarAttr.height < 256) {
|
||||||
|
avatar.value.style.height = "100%";
|
||||||
|
}
|
||||||
|
// 选区属性
|
||||||
|
let size;
|
||||||
|
if (avatarAttr.isHorizontal) {
|
||||||
|
size = avatarAttr.height * avatarAttr.scale;
|
||||||
|
selectorAttr.x = selectorAttr.maxX * .5;
|
||||||
|
selectorAttr.y = selectorAttr.minY;
|
||||||
|
} else {
|
||||||
|
size = avatarAttr.width * avatarAttr.scale;
|
||||||
|
selectorAttr.x = selectorAttr.minX;
|
||||||
|
selectorAttr.y = selectorAttr.maxY * .5;
|
||||||
|
}
|
||||||
|
selectorAttr.maxSize = selectorAttr.size = size;
|
||||||
|
selector.value.style.top = selectorAttr.x + "px";
|
||||||
|
selector.value.style.left = selectorAttr.y + "px";
|
||||||
|
selector.value.style.width = size + "px";
|
||||||
|
selector.value.style.height = size + "px";
|
||||||
|
};
|
||||||
|
img.value = newImg;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 还原默认头像(虽然是获取默认头像,但实际修改的还是专有头像) */
|
||||||
|
const reset = async () => {
|
||||||
|
const res = JSON.parse(SettingMapper.getValue(SettingKey.PUBLIC_RESOURCES) as string) as PublicResources;
|
||||||
|
parseImageData(CommonAPI.getAttachmentReadAPI(res.user.avatar));
|
||||||
|
fileName.value = "default.png";
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 取消编辑 */
|
||||||
|
const cancel = () => {
|
||||||
|
avatarAttr.isLoad = false;
|
||||||
|
fileName.value = "";
|
||||||
|
img.value = undefined;
|
||||||
|
if (file.value) {
|
||||||
|
file.value.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @returns 回调头像编辑结果(Base64 数据),为 null 时表示没有更新头像 */
|
||||||
|
const getValue = (): string | null => {
|
||||||
|
if (img.value && avatarAttr.isLoad && canvas.value) {
|
||||||
|
const r = getPreviewCSS(256);
|
||||||
|
const ctx = canvas.value.getContext("2d");
|
||||||
|
if (ctx) {
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
ctx.drawImage(img.value, r.x, r.y, img.value.width * r.scale, img.value.height * r.scale);
|
||||||
|
return canvas.value.toDataURL();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getValue,
|
||||||
|
cancel
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.avatar-editor {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.file {
|
||||||
|
width: 80%;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.mask {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset,
|
||||||
|
.cancel {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
width: 256px;
|
||||||
|
height: 256px;
|
||||||
|
border: var(--tui-border);
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 256px;
|
||||||
|
max-height: 256px;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
background-size: 30px 30px;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, #E5E5E5 25%, transparent 0, transparent 75%, #E5E5E5 0),
|
||||||
|
linear-gradient(45deg, #E5E5E5 25%, transparent 0, transparent 75%, #E5E5E5 0);
|
||||||
|
background-position: 0 0, 15px 15px;
|
||||||
|
|
||||||
|
.img {
|
||||||
|
position: absolute;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector {
|
||||||
|
cursor: all-scroll;
|
||||||
|
outline: 256px solid rgba(0, 0, 0, .2);
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
width: calc(100% - 2px);
|
||||||
|
height: calc(100% - 2px);
|
||||||
|
border: 1px solid #FFF;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border: 1px solid #FFF;
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(0, 0, 0, .4);
|
||||||
|
|
||||||
|
&.lt {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
cursor: nw-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rt {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
cursor: ne-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.lb {
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
cursor: ne-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rb {
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
cursor: nw-resize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
|
||||||
|
.item {
|
||||||
|
border: var(--tui-border);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.x128 {
|
||||||
|
width: 128px;
|
||||||
|
height: 128px;
|
||||||
|
max-width: 128px;
|
||||||
|
max-height: 128px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.x64 {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
max-width: 64px;
|
||||||
|
max-height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
position: absolute;
|
||||||
|
transform-origin: top left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
46
src/components/ProfileVisitor.vue
Normal file
46
src/components/ProfileVisitor.vue
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="value" class="profile-visitor">
|
||||||
|
<t-form>
|
||||||
|
<t-form-item v-if="level" label="等级" name="level">
|
||||||
|
<user-level class="level-icon" :value="level.value" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item v-if="value.email" label="邮箱" name="email">
|
||||||
|
<div v-text="value.email"></div>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item v-if="value.profile.sex" label="性别" name="sex">
|
||||||
|
<icon
|
||||||
|
:class="{ girl: isGirl, boy: isBoy }"
|
||||||
|
:name="isBoy ? 'BOY' : 'GIRL'"
|
||||||
|
:fill="`var(--tui-${isBoy ? 'blue' : 'pink'})`"
|
||||||
|
/>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item v-if="value.profile.birthdate" label="出生日期" name="birthdate">
|
||||||
|
<div v-text="Time.toDateTime(value.profile.birthdate)"></div>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item v-if="value.profile.qq" label="QQ" name="qq">
|
||||||
|
<div v-text="value.profile.qq"></div>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="注册时间" name="createdAt">
|
||||||
|
<div v-text="Time.toDateTime(value.createdAt)"></div>
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Icon, Time, Toolkit, UserLevel } from "timi-web";
|
||||||
|
import { UserView } from "timi-web";
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
value?: UserView
|
||||||
|
}>(), {});
|
||||||
|
|
||||||
|
const { value } = toRefs(props);
|
||||||
|
|
||||||
|
const level = computed(() => Toolkit.toUserLevel(value.value?.profile.exp));
|
||||||
|
const isBoy = computed(() => value.value?.profile.sex === 1);
|
||||||
|
const isGirl = computed(() => value.value?.profile.sex === 0);
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
128
src/components/WrapperSelector.vue
Normal file
128
src/components/WrapperSelector.vue
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wrapper-selector">
|
||||||
|
<div class="header">
|
||||||
|
<div class="file">
|
||||||
|
<t-input class="name" type="text" :value="fileName" placeholder="" readonly />
|
||||||
|
<t-button>选择文件</t-button>
|
||||||
|
<input ref="file" class="mask" type="file" accept="image/png" @change="onSelectedFile($event)" />
|
||||||
|
</div>
|
||||||
|
<t-button
|
||||||
|
class="reset"
|
||||||
|
theme="default"
|
||||||
|
@click="reset"
|
||||||
|
>还原默认</t-button>
|
||||||
|
<t-button
|
||||||
|
v-show="fileName"
|
||||||
|
class="cancel"
|
||||||
|
theme="default"
|
||||||
|
@click="cancel"
|
||||||
|
>取消</t-button>
|
||||||
|
</div>
|
||||||
|
<img v-if="previewSrc" class="preview" :src="previewSrc" alt="preview" />
|
||||||
|
<canvas ref="canvas" class="canvas" hidden></canvas>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { CommonAPI, IOSize, SettingKey, SettingMapper } from "timi-web";
|
||||||
|
|
||||||
|
const img = ref<HTMLImageElement>();
|
||||||
|
const file = ref<HTMLInputElement>();
|
||||||
|
const canvas = ref<HTMLCanvasElement>();
|
||||||
|
const fileName = ref<string>();
|
||||||
|
const previewSrc = ref();
|
||||||
|
|
||||||
|
const onSelectedFile = (event: any) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file && file.type === "image/png") {
|
||||||
|
if (file.size <= IOSize.MB * 2) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.onload = () => {
|
||||||
|
if (reader.readyState === 2 && reader.result) {
|
||||||
|
img.value = new Image();
|
||||||
|
img.value.crossOrigin = "anonymous";
|
||||||
|
img.value.src = previewSrc.value = reader.result as string;
|
||||||
|
fileName.value = file.name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const reset = async () => {
|
||||||
|
const value = SettingMapper.getValue(SettingKey.PUBLIC_RESOURCES);
|
||||||
|
if (value) {
|
||||||
|
const res = JSON.parse(value);
|
||||||
|
|
||||||
|
img.value = new Image();
|
||||||
|
img.value.crossOrigin = "anonymous";
|
||||||
|
img.value.src = previewSrc.value = CommonAPI.getAttachmentReadAPI(res.DEFAULT_WRAPPER);
|
||||||
|
fileName.value = "default.png";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 取消编辑 */
|
||||||
|
const cancel = () => {
|
||||||
|
fileName.value = "";
|
||||||
|
img.value = previewSrc.value = undefined;
|
||||||
|
if (file.value) {
|
||||||
|
file.value.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @returns 回调头像编辑结果(Base64 数据),为 null 时表示没有更新头像 */
|
||||||
|
const getValue = (): string | null => {
|
||||||
|
if (img.value && canvas.value) {
|
||||||
|
canvas.value.width = img.value.width;
|
||||||
|
canvas.value.height = img.value.height;
|
||||||
|
|
||||||
|
const ctx = canvas.value.getContext("2d");
|
||||||
|
if (ctx) {
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
ctx.drawImage(img.value, 0, 0, img.value.width, img.value.height);
|
||||||
|
return canvas.value.toDataURL();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getValue,
|
||||||
|
cancel
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.wrapper-selector {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.file {
|
||||||
|
width: 80%;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.mask {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset,
|
||||||
|
.cancel {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
88
src/layout/CommentLayout.vue
Normal file
88
src/layout/CommentLayout.vue
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-comment-layout">
|
||||||
|
<t-tabs class="menu" placement="left" v-model="tabValue">
|
||||||
|
<t-tab-panel :value="Tab.COMMENT" label="我的评论" />
|
||||||
|
<t-tab-panel :value="Tab.REPLY_BY_SENDER" label="我的回复" />
|
||||||
|
<t-tab-panel :value="Tab.REPLY_BY_RECEIVER" label="回复我的" />
|
||||||
|
</t-tabs>
|
||||||
|
<div class="content">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useVisitUserStore } from "@/store/visitUser.ts";
|
||||||
|
|
||||||
|
enum Tab {
|
||||||
|
COMMENT = "CommentList",
|
||||||
|
REPLY_BY_SENDER = "ReplyBySenderList",
|
||||||
|
REPLY_BY_RECEIVER = "ReplyByReceiverList"
|
||||||
|
}
|
||||||
|
|
||||||
|
const regexMenu: {
|
||||||
|
[key: string]: Tab
|
||||||
|
} = {
|
||||||
|
"CommentList|CommentLayout": Tab.COMMENT,
|
||||||
|
"ReplyBySenderList": Tab.REPLY_BY_SENDER,
|
||||||
|
"ReplyByReceiverList": Tab.REPLY_BY_RECEIVER
|
||||||
|
};
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const userStore = useVisitUserStore();
|
||||||
|
|
||||||
|
const tabValue = ref<Tab>();
|
||||||
|
|
||||||
|
const syncMenuRouter = (routerName?: string) => {
|
||||||
|
if (routerName) {
|
||||||
|
for (const item in regexMenu) {
|
||||||
|
if (new RegExp(item).test(routerName as string)) {
|
||||||
|
tabValue.value = regexMenu[item];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => route.name, (routerName) => syncMenuRouter(routerName as string));
|
||||||
|
watch(tabValue, async () => {
|
||||||
|
await router.push({
|
||||||
|
name: tabValue.value,
|
||||||
|
params: {
|
||||||
|
id: userStore.visitUserView?.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
syncMenuRouter(route.name as string);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.space-comment-layout {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: var(--tui-border);
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
display: flex;
|
||||||
|
min-width: 10rem;
|
||||||
|
justify-content: end;
|
||||||
|
|
||||||
|
:deep(.t-tabs__nav-container) {
|
||||||
|
padding-top: 2rem;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
width: calc(100% - 10rem);
|
||||||
|
min-height: 520px;
|
||||||
|
border-left: var(--tui-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
313
src/layout/IndexLayout.vue
Normal file
313
src/layout/IndexLayout.vue
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-layout">
|
||||||
|
<loading :show-on="!userView" />
|
||||||
|
<div v-if="userView" class="header">
|
||||||
|
<div class="info">
|
||||||
|
<t-image
|
||||||
|
class="wrapper"
|
||||||
|
:class="(<any>ImageType)[userView.profile.wrapperType]"
|
||||||
|
:src="UserAPI.getWrapperURL(userView.profile)"
|
||||||
|
/>
|
||||||
|
<div class="base-info">
|
||||||
|
<t-image
|
||||||
|
class="avatar"
|
||||||
|
:class="(<any>ImageType)[userView.profile.avatarType]"
|
||||||
|
:src="UserAPI.getAvatarURL(userView.profile)"
|
||||||
|
/>
|
||||||
|
<div class="text selectable">
|
||||||
|
<h4 class="name" v-text="userView.name"></h4>
|
||||||
|
<p v-if="userView.profile.description" class="description" v-text="userView.profile.description"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="action">
|
||||||
|
<t-button
|
||||||
|
v-if="!userStore.isLogged()"
|
||||||
|
class="button login"
|
||||||
|
theme="default"
|
||||||
|
@click="visitableAuth = true"
|
||||||
|
>登录</t-button>
|
||||||
|
<!-- TODO 加已登录头像 -->
|
||||||
|
<t-button
|
||||||
|
v-if="userStore.isLogged() && visitUserStore.isVisitor"
|
||||||
|
class="button index"
|
||||||
|
theme="default"
|
||||||
|
@click="toIndex"
|
||||||
|
>个人首页</t-button>
|
||||||
|
<t-button
|
||||||
|
v-if="userStore.isLogged()"
|
||||||
|
class="button logout"
|
||||||
|
theme="default"
|
||||||
|
@click="logout()"
|
||||||
|
>退出登录</t-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="menu">
|
||||||
|
<t-tabs class="tab" v-model="tabValue">
|
||||||
|
<t-tab-panel :value="Tab.PROFILE" label="账号中心" />
|
||||||
|
<t-tab-panel :value="Tab.PRIVACY" label="隐私" />
|
||||||
|
<t-tab-panel :value="Tab.FOREVER_MC" label="ForeverMC" />
|
||||||
|
<t-tab-panel :value="Tab.DEVELOPER" label="开发者" />
|
||||||
|
<t-tab-panel :value="Tab.COMMENT" label="评论" />
|
||||||
|
</t-tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<router-view />
|
||||||
|
</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 { ImageType, Loading, UserAPI, userStore } from "timi-web";
|
||||||
|
import { AuthorizeForm } from "timi-tdesign-pc";
|
||||||
|
import { useVisitUserStore } from "@/store/visitUser.ts";
|
||||||
|
|
||||||
|
enum Tab {
|
||||||
|
PROFILE = "ProfileInfo",
|
||||||
|
PRIVACY = "PrivacyInfo",
|
||||||
|
FOREVER_MC = "ForeverMC",
|
||||||
|
DEVELOPER = "Developer",
|
||||||
|
COMMENT = "CommentLayout"
|
||||||
|
}
|
||||||
|
|
||||||
|
const regexMenu: {
|
||||||
|
[key: string]: Tab
|
||||||
|
} = {
|
||||||
|
"ProfileInfo|SpaceLayout": Tab.PROFILE,
|
||||||
|
"PrivacyInfo": Tab.PRIVACY,
|
||||||
|
"ForeverMC": Tab.FOREVER_MC,
|
||||||
|
"Developer": Tab.DEVELOPER,
|
||||||
|
"CommentLayout|CommentList|ReplyBySenderList|ReplyByReceiverList": Tab.COMMENT
|
||||||
|
};
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const visitUserStore = useVisitUserStore();
|
||||||
|
|
||||||
|
const tabValue = ref<Tab>();
|
||||||
|
const visitableAuth = ref<boolean>(false);
|
||||||
|
|
||||||
|
const userView = computed(() => visitUserStore.visitUserView);
|
||||||
|
|
||||||
|
watch(() => route.name, (routerName) => syncMenuRouter(routerName as string));
|
||||||
|
watch(tabValue, async () => {
|
||||||
|
await router.push({
|
||||||
|
name: tabValue.value,
|
||||||
|
params: {
|
||||||
|
id: visitUserStore.visitUserView?.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const toIndex = async () => {
|
||||||
|
await router.push({
|
||||||
|
name: "ProfileInfo",
|
||||||
|
params: {
|
||||||
|
id: userStore.loginUser.user?.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await visitUserStore.updateVisitUserView(route.params.id as any as number);
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
userStore.logout();
|
||||||
|
router.push("/");
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncMenuRouter = (routerName?: string) => {
|
||||||
|
if (routerName) {
|
||||||
|
for (const item in regexMenu) {
|
||||||
|
if (new RegExp(item).test(routerName as string)) {
|
||||||
|
tabValue.value = regexMenu[item];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
syncMenuRouter(route.name as string);
|
||||||
|
await visitUserStore.updateVisitUserView(route.params.id as any as number);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.space-layout {
|
||||||
|
|
||||||
|
.header {
|
||||||
|
top: -200px;
|
||||||
|
margin: 0;
|
||||||
|
z-index: 4;
|
||||||
|
position: sticky;
|
||||||
|
transition: top 500ms var(--tui-bezier);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.info {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-info {
|
||||||
|
bottom: -30px;
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
transition: 500ms var(--tui-bezier);
|
||||||
|
margin-left: 3rem;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 128px;
|
||||||
|
height: 128px;
|
||||||
|
padding: 3px;
|
||||||
|
min-width: 128px;
|
||||||
|
min-height: 128px;
|
||||||
|
transition: 500ms var(--tui-bezier);
|
||||||
|
background: #CCC;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: #FFF;
|
||||||
|
z-index: 1;
|
||||||
|
transition: 500ms var(--tui-bezier);
|
||||||
|
padding-left: 1rem;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
width: fit-content;
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 2px 4px;
|
||||||
|
min-width: fit-content;
|
||||||
|
transition: 500ms var(--tui-bezier);
|
||||||
|
background: rgba(0, 0, 0, .6);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin: 0 12px 0 0;
|
||||||
|
padding: 2px 4px;
|
||||||
|
display: -webkit-box;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: 500ms var(--tui-bezier);
|
||||||
|
background: rgba(0, 0, 0, .4);
|
||||||
|
border-radius: 2px;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
top: 2rem;
|
||||||
|
right: 1rem;
|
||||||
|
z-index: 2;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
opacity: 0;
|
||||||
|
transition: 250ms;
|
||||||
|
visibility: hidden;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
|
||||||
|
.action {
|
||||||
|
|
||||||
|
.button {
|
||||||
|
opacity: 1;
|
||||||
|
transition: 250ms;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
z-index: 5;
|
||||||
|
position: relative;
|
||||||
|
visibility: v-bind("visitUserStore.isVisitor ? 'hidden' : 'visible'");
|
||||||
|
transition: 500ms var(--tui-bezier);
|
||||||
|
background: rgba(255, 255, 255, .8);
|
||||||
|
margin-left: calc(3rem + 3px + 128px + 3px);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
:deep(.t-tabs__operations) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media screen and (max-width: 1920px) {
|
||||||
|
.space-layout {
|
||||||
|
|
||||||
|
.header {
|
||||||
|
top: -140px;
|
||||||
|
transition: 500ms var(--tui-bezier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 720px) {
|
||||||
|
.space-layout {
|
||||||
|
|
||||||
|
.header {
|
||||||
|
top: -60px;
|
||||||
|
transition: 500ms var(--tui-bezier);
|
||||||
|
|
||||||
|
.info {
|
||||||
|
|
||||||
|
.base-info {
|
||||||
|
margin-left: 2rem;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
min-width: 64px;
|
||||||
|
min-height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-left: .5rem;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin: 0 0 0 1rem;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
margin-left: calc(2rem + 3px + 64px + 3px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
87
src/main.ts
Normal file
87
src/main.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
import router from "@/router";
|
||||||
|
import type { NavigationGuardNext, RouteLocationNormalized } from "vue-router";
|
||||||
|
|
||||||
|
import { axios, Network, userStore, VDraggable, VPopup } from "timi-web";
|
||||||
|
import { DialogPlugin } from "tdesign-vue-next";
|
||||||
|
|
||||||
|
import Root from "@/Root.vue";
|
||||||
|
|
||||||
|
import "tdesign-vue-next/dist/tdesign.css";
|
||||||
|
import "timi-tdesign-pc/style.css";
|
||||||
|
import "timi-web/style.css";
|
||||||
|
|
||||||
|
console.log(`
|
||||||
|
______ __ _ _
|
||||||
|
/ __\\ \\ \\ \\ \\
|
||||||
|
/ . . \\ ' \\ \\ \\ \\
|
||||||
|
( ) imyeyu.com ) ) ) )
|
||||||
|
'\\ ___ /' / / / /
|
||||||
|
====='===='=====================/_/_/_/
|
||||||
|
`);
|
||||||
|
|
||||||
|
// ---------- axios 网络请求 ----------
|
||||||
|
axios.defaults.baseURL = import.meta.env.VITE_API;
|
||||||
|
axios.interceptors.request.use(Network.userTokenInterceptors);
|
||||||
|
// ---------- Router 路由 ----------
|
||||||
|
router.beforeEach(async (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext): Promise<void> => {
|
||||||
|
await userStore.login4Storage();
|
||||||
|
const isLogged = userStore.isLogged();
|
||||||
|
const visitUserId = to.params.id;
|
||||||
|
const currentUserId = userStore.loginUser.user?.id;
|
||||||
|
|
||||||
|
// 处理已登录用户的路由
|
||||||
|
if (isLogged) {
|
||||||
|
// 访问其他用户页面时,只允许查看其首页
|
||||||
|
if (visitUserId && Number(visitUserId) !== currentUserId && to.name !== "ProfileInfo") {
|
||||||
|
next({
|
||||||
|
name: "ProfileInfo",
|
||||||
|
params: { id: visitUserId }
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 空间首页重定向到个人信息页
|
||||||
|
if (to.name === "SpaceIndex") {
|
||||||
|
next({
|
||||||
|
name: "ProfileInfo",
|
||||||
|
params: { id: currentUserId }
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (to.name !== "ProfileInfo" && to.name !== "SpaceIndex") {
|
||||||
|
// 处理未登录用户的路由
|
||||||
|
next({ name: "SpaceIndex" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 有效路由
|
||||||
|
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(createPinia());
|
||||||
|
app.use(router);
|
||||||
|
app.directive("draggable", VDraggable as any);
|
||||||
|
app.directive("popup", VPopup as any);
|
||||||
|
app.mount("#root");
|
||||||
69
src/router/index.ts
Normal file
69
src/router/index.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
|
|
||||||
|
export default createRouter({
|
||||||
|
history: createWebHistory("/"),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
name: "SpaceIndex",
|
||||||
|
component: () => import("@/views/SpaceIndex.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/:id",
|
||||||
|
name: "IndexLayout",
|
||||||
|
component: () => import("@/layout/IndexLayout.vue"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
alias: "data",
|
||||||
|
name: "ProfileInfo",
|
||||||
|
component: () => import("@/views/ProfileInfo.vue"),
|
||||||
|
meta: {
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "forevermc",
|
||||||
|
name: "ForeverMC",
|
||||||
|
component: () => import("@/views/ForeverMC.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "privacy",
|
||||||
|
name: "PrivacyInfo",
|
||||||
|
component: () => import("@/views/PrivacyInfo.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "developer",
|
||||||
|
name: "Developer",
|
||||||
|
component: () => import("@/views/Developer.vue"),
|
||||||
|
meta: {
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "comment/",
|
||||||
|
name: "CommentLayout",
|
||||||
|
component: () => import("@/layout/CommentLayout.vue"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
alias: "list",
|
||||||
|
name: "CommentList",
|
||||||
|
component: () => import("@/views/comment/CommentList.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "reply/sender",
|
||||||
|
name: "ReplyBySenderList",
|
||||||
|
component: () => import("@/views/comment/ReplyBySenderList.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "reply/receiver",
|
||||||
|
name: "ReplyByReceiverList",
|
||||||
|
component: () => import("@/views/comment/ReplyByReceiverList.vue")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
43
src/store/visitUser.ts
Normal file
43
src/store/visitUser.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { UserAPI, userStore, UserView } from "timi-web";
|
||||||
|
|
||||||
|
const store = defineStore("visitUser", () => {
|
||||||
|
|
||||||
|
const visitUserView = ref<UserView>();
|
||||||
|
|
||||||
|
const isVisitor = computed(() => {
|
||||||
|
const loginUser = userStore.loginUser;
|
||||||
|
return !userStore.isLogged() || !loginUser.token || !visitUserView.value || loginUser.token.id !== visitUserView.value.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateVisitUserView(id: number): Promise<void> {
|
||||||
|
if (visitUserView.value && visitUserView.value.id === id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!visitUserView.value && userStore.loginUser.user?.id === Number(id)) {
|
||||||
|
visitUserView.value = userStore.loginUser.user;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visitUserView.value = await UserAPI.view(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadVisitUserView(): Promise<void> {
|
||||||
|
if (visitUserView.value) {
|
||||||
|
const id = visitUserView.value.id as number;
|
||||||
|
visitUserView.value = undefined;
|
||||||
|
await updateVisitUserView(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isVisitor,
|
||||||
|
visitUserView,
|
||||||
|
|
||||||
|
updateVisitUserView,
|
||||||
|
reloadVisitUserView
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useVisitUserStore() {
|
||||||
|
return store();
|
||||||
|
}
|
||||||
7
src/types/MinecraftPlayer.ts
Normal file
7
src/types/MinecraftPlayer.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { Model } from "timi-web";
|
||||||
|
|
||||||
|
export type MinecraftPlayer = {
|
||||||
|
playerId: number;
|
||||||
|
name: string;
|
||||||
|
lastLoginAt: number;
|
||||||
|
} & Model
|
||||||
28
src/types/User.ts
Normal file
28
src/types/User.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Page, User, UserProfile } from "timi-web";
|
||||||
|
|
||||||
|
export type UserRequest = {
|
||||||
|
profile: {
|
||||||
|
avatar?: File;
|
||||||
|
wrapper?: File;
|
||||||
|
} & UserProfile
|
||||||
|
} & User
|
||||||
|
|
||||||
|
export type UserPrivacy = {
|
||||||
|
userId: number;
|
||||||
|
email?: boolean;
|
||||||
|
sex?: boolean;
|
||||||
|
birthdate?: boolean;
|
||||||
|
qq?: boolean;
|
||||||
|
lastLoginAt?: boolean;
|
||||||
|
createdAt?: boolean;
|
||||||
|
updatedAt?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UserCommentBizType {
|
||||||
|
ARTICLE = "博客文章",
|
||||||
|
GIT_ISSUE = "开源站 - 反馈",
|
||||||
|
GIT_MERGE = "开源站 - 合并请求"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCommentPage = {
|
||||||
|
} & Page;
|
||||||
105
src/views/Developer.vue
Normal file
105
src/views/Developer.vue
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<div class="developer">
|
||||||
|
<loading :show-on="!developer" />
|
||||||
|
<t-alert v-if="developer" class="top-tips" :theme="developer.rsa ? 'success' : 'info'">
|
||||||
|
<template #message>
|
||||||
|
<span v-if="developer.rsa">已</span>
|
||||||
|
<span>完善开发者信息可参与 </span>
|
||||||
|
<a :href="gitURL" v-text="gitURL"></a>
|
||||||
|
<span> 的开源项目开发</span>
|
||||||
|
</template>
|
||||||
|
</t-alert>
|
||||||
|
<t-form v-if="developer" class="form" :data="developer" @submit="doSubmit">
|
||||||
|
<t-form-item class="name" label="开发者" name="name">
|
||||||
|
<t-input class="input" v-model="developer.name" placeholder="">
|
||||||
|
<template #tips>
|
||||||
|
<div class="tips gray markdown selectable">
|
||||||
|
<span>使用 </span>
|
||||||
|
<code>git config user.name</code>
|
||||||
|
<span> 命令获取</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</t-input>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item class="rsa" label="RSA 公钥" name="rsa">
|
||||||
|
<t-textarea
|
||||||
|
class="value break-all"
|
||||||
|
v-model="developer.rsa"
|
||||||
|
:autosize="{ minRows: 6, maxRows: 10 }"
|
||||||
|
>
|
||||||
|
</t-textarea>
|
||||||
|
</t-form-item>
|
||||||
|
<markdown-view :content="rsaTips" />
|
||||||
|
<t-button class="submit" type="submit">保存</t-button>
|
||||||
|
</t-form>
|
||||||
|
<markdown-view :content="tips" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
CommonAPI,
|
||||||
|
Developer,
|
||||||
|
DeveloperAPI as BaseDeveloperAPI,
|
||||||
|
Loading,
|
||||||
|
MarkdownView,
|
||||||
|
SettingKey,
|
||||||
|
SettingMapper,
|
||||||
|
TemplateBizType
|
||||||
|
} from "timi-web";
|
||||||
|
import { MessagePlugin } from "tdesign-vue-next";
|
||||||
|
import DeveloperAPI from "@/api/DeveloperAPI.ts";
|
||||||
|
|
||||||
|
const gitURL = ref(`https://${SettingMapper.getValue(SettingKey.DOMAIN_GIT)}`);
|
||||||
|
const tips = ref();
|
||||||
|
const rsaTips = ref();
|
||||||
|
const developer = ref<Developer>();
|
||||||
|
|
||||||
|
const doSubmit = () => {
|
||||||
|
if (developer.value) {
|
||||||
|
DeveloperAPI.update(developer.value).then(() => {
|
||||||
|
MessagePlugin.success("保存成功");
|
||||||
|
}).catch(msg => {
|
||||||
|
MessagePlugin.error("保存失败:" + msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
developer.value = await BaseDeveloperAPI.get();
|
||||||
|
rsaTips.value = await CommonAPI.getTemplate(TemplateBizType.GIT, "DEVELOPER_RSA");
|
||||||
|
tips.value = await CommonAPI.getTemplate(TemplateBizType.GIT, "DEVELOPER");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.developer {
|
||||||
|
padding: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.top-tips {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rsa {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit {
|
||||||
|
width: 8rem;
|
||||||
|
margin: 2rem auto 4rem auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
262
src/views/ForeverMC.vue
Normal file
262
src/views/ForeverMC.vue
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
<template>
|
||||||
|
<div class="forever-mc">
|
||||||
|
<loading v-if="doFetching" :show-on="doFetching" />
|
||||||
|
<template v-else>
|
||||||
|
<markdown-view :content="computedBindTips" />
|
||||||
|
<t-table
|
||||||
|
class="table"
|
||||||
|
row-key="id"
|
||||||
|
:data="playerList"
|
||||||
|
:columns="columns"
|
||||||
|
>
|
||||||
|
<template #lastLoginAt="{ row }">
|
||||||
|
<span v-text="Time.toDateTime(row.lastLoginAt)"></span>
|
||||||
|
</template>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<icon
|
||||||
|
class="cur-pointer"
|
||||||
|
name="FAIL"
|
||||||
|
fill="RED"
|
||||||
|
@click="doDelete(row.id)"
|
||||||
|
v-popup="`解除绑定`"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</t-table>
|
||||||
|
<t-form class="bind-form" :disabled="computedBoundMax">
|
||||||
|
<p
|
||||||
|
v-if="computedBoundMax"
|
||||||
|
class="bound-max-tips red"
|
||||||
|
v-text="`已达到最大绑定数量:${maxBind}`"
|
||||||
|
></p>
|
||||||
|
<t-form-item label="添加绑定" name="name">
|
||||||
|
<t-input class="short" v-model="newBind" placeholder="请输入玩家昵称" />
|
||||||
|
<template #statusIcon>
|
||||||
|
<t-button
|
||||||
|
class="submit"
|
||||||
|
theme="success"
|
||||||
|
:disabled="doSubmittingBind || computedBoundMax"
|
||||||
|
@click="doSubmitBind"
|
||||||
|
>提交</t-button>
|
||||||
|
</template>
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<t-dialog
|
||||||
|
class="unbind-confirm-dialog"
|
||||||
|
v-model:visible="visibleUnbindConfirmDialog"
|
||||||
|
attach="body"
|
||||||
|
:close-btn="false"
|
||||||
|
header="注意: "
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<markdown-view :content="computedUnbindTips" />
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="footer">
|
||||||
|
<t-button
|
||||||
|
class="submit"
|
||||||
|
theme="default"
|
||||||
|
@click="doSubmitUnbind"
|
||||||
|
:loading="doSubmittingUnbind"
|
||||||
|
:disabled="doSubmittingUnbind || delayConfirm"
|
||||||
|
:content="confirmBtnText"
|
||||||
|
/>
|
||||||
|
<t-button
|
||||||
|
@click="visibleUnbindConfirmDialog = false"
|
||||||
|
:disabled="doSubmittingUnbind"
|
||||||
|
content="取消"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</t-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
CommonAPI,
|
||||||
|
Icon,
|
||||||
|
Loading,
|
||||||
|
MarkdownView,
|
||||||
|
SettingKey,
|
||||||
|
SettingMapper,
|
||||||
|
TemplateBizType,
|
||||||
|
Time,
|
||||||
|
Toolkit
|
||||||
|
} from "timi-web";
|
||||||
|
import MinecraftAPI from "@/api/MinecraftAPI.ts";
|
||||||
|
import { useVisitUserStore } from "@/store/visitUser.ts";
|
||||||
|
import { MinecraftPlayer } from "@/types/MinecraftPlayer.ts";
|
||||||
|
import { MessagePlugin } from "tdesign-vue-next";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "ForeverMC"
|
||||||
|
});
|
||||||
|
|
||||||
|
const visitUserStore = useVisitUserStore();
|
||||||
|
|
||||||
|
const columns = computed(() => [
|
||||||
|
{
|
||||||
|
title: "昵称",
|
||||||
|
colKey: "name",
|
||||||
|
width: "40%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "上次登录",
|
||||||
|
colKey: "lastLoginAt",
|
||||||
|
align: "center"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: "right",
|
||||||
|
width: "3rem",
|
||||||
|
colKey: "id",
|
||||||
|
cell: "action"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const tipsBind = ref();
|
||||||
|
const tipsUnbind = ref();
|
||||||
|
const computedBindTips = computed(() => {
|
||||||
|
if (tipsBind.value) {
|
||||||
|
return Toolkit.format(tipsBind.value, {
|
||||||
|
maxBind: SettingMapper.getValue(SettingKey.FMC_MAX_BIND)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
Toolkit.async(async () => {
|
||||||
|
tipsBind.value = await CommonAPI.getTemplate(TemplateBizType.FOREVER_MC, "PLAYER_BIND");
|
||||||
|
tipsUnbind.value = await CommonAPI.getTemplate(TemplateBizType.FOREVER_MC, "PLAYER_UNBIND");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const playerList = ref<MinecraftPlayer[]>([]);
|
||||||
|
const doFetching = ref<boolean>(false);
|
||||||
|
async function doFetch() {
|
||||||
|
if (!doFetching.value) {
|
||||||
|
doFetching.value = true;
|
||||||
|
playerList.value.length = 0;
|
||||||
|
playerList.value.push(...(await MinecraftAPI.list()));
|
||||||
|
}
|
||||||
|
doFetching.value = false;
|
||||||
|
}
|
||||||
|
watch(() => visitUserStore.visitUserView, doFetch);
|
||||||
|
onMounted(() => Toolkit.async(async () => await doFetch()));
|
||||||
|
|
||||||
|
const newBind = ref();
|
||||||
|
const maxBind = SettingMapper.getValueRef(SettingKey.FMC_MAX_BIND);
|
||||||
|
const doSubmittingBind = ref<boolean>(false);
|
||||||
|
async function doSubmitBind() {
|
||||||
|
doSubmittingBind.value = true;
|
||||||
|
await Toolkit.sleep(1E3);
|
||||||
|
MinecraftAPI.bind(newBind.value).then(async () => {
|
||||||
|
await MessagePlugin.success("添加绑定成功");
|
||||||
|
await doFetch();
|
||||||
|
newBind.value = "";
|
||||||
|
doSubmittingBind.value = false;
|
||||||
|
}).catch(async (msg: string) => {
|
||||||
|
await MessagePlugin.error(msg);
|
||||||
|
doSubmittingBind.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const doSubmittingUnbind = ref<boolean>(false);
|
||||||
|
const visibleUnbindConfirmDialog = ref<boolean>(false);
|
||||||
|
const unbindId = ref<number>();
|
||||||
|
const delayConfirm = ref<boolean>(false);
|
||||||
|
const delaySeconds = ref<number>(8);
|
||||||
|
const delayTimer = ref();
|
||||||
|
watch(visibleUnbindConfirmDialog, () => delayConfirm.value = visibleUnbindConfirmDialog.value);
|
||||||
|
watch(delayConfirm, () => {
|
||||||
|
if (delayTimer.value) {
|
||||||
|
clearInterval(delayTimer.value);
|
||||||
|
}
|
||||||
|
delaySeconds.value = 8;
|
||||||
|
delayTimer.value = setInterval(() => {
|
||||||
|
delaySeconds.value--;
|
||||||
|
if (delaySeconds.value === 0) {
|
||||||
|
delayConfirm.value = false;
|
||||||
|
clearInterval(delayTimer.value);
|
||||||
|
}
|
||||||
|
}, 1E3);
|
||||||
|
});
|
||||||
|
const computedBoundMax = computed(() => SettingMapper.getValue(SettingKey.FMC_MAX_BIND) as any as number <= playerList.value.length);
|
||||||
|
const computedUnbindTips = computed(() => {
|
||||||
|
if (unbindId.value) {
|
||||||
|
return Toolkit.format(tipsUnbind.value, {
|
||||||
|
name: playerList.value.find(i => i.id === unbindId.value)?.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
const confirmBtnText = computed(() => {
|
||||||
|
if (delayConfirm.value) {
|
||||||
|
return `确认 (${delaySeconds.value})`;
|
||||||
|
}
|
||||||
|
if (doSubmittingUnbind.value) {
|
||||||
|
return "正在保存..";
|
||||||
|
}
|
||||||
|
return "确认";
|
||||||
|
});
|
||||||
|
async function doSubmitUnbind() {
|
||||||
|
if (unbindId.value) {
|
||||||
|
doSubmittingUnbind.value = true;
|
||||||
|
|
||||||
|
await Toolkit.sleep(1E3);
|
||||||
|
MinecraftAPI.unbind(unbindId.value).then(async () => {
|
||||||
|
await MessagePlugin.success("解除绑定成功");
|
||||||
|
await doFetch();
|
||||||
|
visibleUnbindConfirmDialog.value = doSubmittingUnbind.value = false;
|
||||||
|
}).catch(async (msg: string) => {
|
||||||
|
await MessagePlugin.error(msg);
|
||||||
|
visibleUnbindConfirmDialog.value = doSubmittingUnbind.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function doDelete(id: number) {
|
||||||
|
unbindId.value = id;
|
||||||
|
visibleUnbindConfirmDialog.value = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.forever-mc {
|
||||||
|
padding: 2rem 4rem;
|
||||||
|
display: flex;
|
||||||
|
transition: padding 500ms var(--tui-bezier);
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.table {
|
||||||
|
max-width: 26rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-form {
|
||||||
|
margin: 1rem 0;
|
||||||
|
|
||||||
|
.bound-max-tips {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit {
|
||||||
|
width: 4rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 720px) {
|
||||||
|
.forever-mc {
|
||||||
|
padding: 2rem;
|
||||||
|
transition: padding 500ms var(--tui-bezier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.update-confirm-dialog .footer) {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
81
src/views/PrivacyInfo.vue
Normal file
81
src/views/PrivacyInfo.vue
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<div class="privacy-info">
|
||||||
|
<h4>允许访客看到你以下选中的信息:</h4>
|
||||||
|
<loading :show-on="!privacy" />
|
||||||
|
<t-form v-if="privacy" class="form">
|
||||||
|
<t-form-item label="邮箱">
|
||||||
|
<t-checkbox v-model="privacy.email" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="性别">
|
||||||
|
<t-checkbox v-model="privacy.sex" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="出生日期">
|
||||||
|
<t-checkbox v-model="privacy.birthdate" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="QQ">
|
||||||
|
<t-checkbox v-model="privacy.qq" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="最近登录">
|
||||||
|
<t-checkbox v-model="privacy.lastLoginAt" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="注册时间">
|
||||||
|
<t-checkbox v-model="privacy.createdAt" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-button
|
||||||
|
class="submit"
|
||||||
|
@click="doSubmit"
|
||||||
|
:loading="doSubmitting"
|
||||||
|
:disabled="doSubmitting"
|
||||||
|
:content="doSubmitting ? '正在保存..' : '保存'"
|
||||||
|
/>
|
||||||
|
</t-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Loading, Toolkit } from "../../../timi-web";
|
||||||
|
import { MessagePlugin } from "tdesign-vue-next";
|
||||||
|
import { UserPrivacy } from "@/types/User.ts";
|
||||||
|
import UserAPI from "@/api/UserAPI.ts";
|
||||||
|
|
||||||
|
const doSubmitting = ref<boolean>(false);
|
||||||
|
const privacy = ref<UserPrivacy>();
|
||||||
|
|
||||||
|
const doSubmit = async () => {
|
||||||
|
if (!privacy.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
doSubmitting.value = true;
|
||||||
|
|
||||||
|
await Toolkit.sleep(500);
|
||||||
|
await UserAPI.updatePrivacy(privacy.value);
|
||||||
|
|
||||||
|
await MessagePlugin.success("保存成功");
|
||||||
|
doSubmitting.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
privacy.value = await UserAPI.getPrivacy();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.privacy-info {
|
||||||
|
width: 24rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem 4rem;
|
||||||
|
|
||||||
|
.form {
|
||||||
|
|
||||||
|
:deep(.t-form__item) {
|
||||||
|
margin-bottom: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit {
|
||||||
|
width: 8rem;
|
||||||
|
margin: 2rem auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
247
src/views/ProfileInfo.vue
Normal file
247
src/views/ProfileInfo.vue
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
<template>
|
||||||
|
<div class="profile-info">
|
||||||
|
<loading :show-on="!userRequest" />
|
||||||
|
<template v-if="userRequest">
|
||||||
|
<profile-visitor v-if="visitUserStore.isVisitor" :value="userRequest" />
|
||||||
|
<t-form v-else>
|
||||||
|
<t-form-item label="修改头像" name="avatar">
|
||||||
|
<avatar-editor ref="avatarEditorComp" :renderer-type="userRequest.profile.avatarType" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="修改背景" name="avatar">
|
||||||
|
<wrapper-selector ref="wrapperSelectorComp" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="头像渲染方式" name="avatar-type">
|
||||||
|
<t-select
|
||||||
|
class="image-type-selector"
|
||||||
|
v-model="userRequest.profile.avatarType"
|
||||||
|
>
|
||||||
|
<t-option v-for="(value, key) in ImageTypeItem" :key="key" :value="key" :label="value" />
|
||||||
|
</t-select>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="封面渲染方式" name="wrapper-type">
|
||||||
|
<t-select class="image-type-selector" v-model="userRequest.profile.wrapperType">
|
||||||
|
<t-option v-for="(value, key) in ImageTypeItem" :key="key" :value="key" :label="value" />
|
||||||
|
</t-select>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item v-if="level" label="等级" name="level">
|
||||||
|
<user-level class="level-icon" :value="level.value" />
|
||||||
|
<div class="level" :style="levelStyle">
|
||||||
|
<div class="value" :style="levelValueStyle"></div>
|
||||||
|
</div>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="昵称" name="name">
|
||||||
|
<t-input class="short" v-model="userRequest.name" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="邮箱" name="email">
|
||||||
|
<t-input class="short" v-model="userRequest.email" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="性别" name="sex">
|
||||||
|
<icon
|
||||||
|
class="sex boy"
|
||||||
|
:class="{ active: isBoy }"
|
||||||
|
name="BOY"
|
||||||
|
fill="var(--tui-blue)"
|
||||||
|
@click="updateSex(1)"
|
||||||
|
v-popup="`男生`"
|
||||||
|
/>
|
||||||
|
<icon
|
||||||
|
class="sex girl"
|
||||||
|
:class="{ active: isGirl }"
|
||||||
|
name="GIRL"
|
||||||
|
fill="var(--tui-pink)"
|
||||||
|
@click="updateSex(0)"
|
||||||
|
v-popup="`女生`"
|
||||||
|
/>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="出生日期" name="birthdate">
|
||||||
|
<t-date-picker
|
||||||
|
class="short"
|
||||||
|
valueType="time-stamp"
|
||||||
|
:firstDayOfWeek="7"
|
||||||
|
v-model="userRequest.profile.birthdate"
|
||||||
|
placeholder=" "
|
||||||
|
/>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="QQ" name="qq">
|
||||||
|
<t-input class="short" v-model="userRequest.profile.qq" placeholder="" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="说明" name="description">
|
||||||
|
<t-textarea v-model="userRequest.profile.description" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="注册时间" name="createdAt">
|
||||||
|
<div v-text="Time.toDateTime(userRequest.createdAt)"></div>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="最近登录" name="lastLoggedAt">
|
||||||
|
<div v-text="Time.toDateTime(userRequest.profile.lastLoginAt)"></div>
|
||||||
|
</t-form-item>
|
||||||
|
<t-button
|
||||||
|
class="submit"
|
||||||
|
@click="doSubmit"
|
||||||
|
:loading="doSubmitting"
|
||||||
|
:disabled="doSubmitting"
|
||||||
|
:content="doSubmitting ? '正在保存..' : '保存'"
|
||||||
|
/>
|
||||||
|
</t-form>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import AvatarEditor from "@/components/AvatarEditor.vue";
|
||||||
|
import { Icon, Loading, Time, Toolkit, UserLevel, userStore } from "timi-web";
|
||||||
|
import { useVisitUserStore } from "@/store/visitUser.ts";
|
||||||
|
import { MessagePlugin } from "tdesign-vue-next";
|
||||||
|
import UserAPI from "@/api/UserAPI.ts";
|
||||||
|
import { UserRequest } from "@/types/User.ts";
|
||||||
|
import WrapperSelector from "@/components/WrapperSelector.vue";
|
||||||
|
import ProfileVisitor from "@/components/ProfileVisitor.vue";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "ProfileInfo"
|
||||||
|
});
|
||||||
|
|
||||||
|
enum ImageTypeItem {
|
||||||
|
PIXELATED = "像素",
|
||||||
|
SMOOTH = "平滑",
|
||||||
|
AUTO = "双线性"
|
||||||
|
}
|
||||||
|
|
||||||
|
const visitUserStore = useVisitUserStore();
|
||||||
|
|
||||||
|
// ref
|
||||||
|
const userRequest = ref<UserRequest>();
|
||||||
|
const avatarEditorComp = ref();
|
||||||
|
const wrapperSelectorComp = ref();
|
||||||
|
const doSubmitting = ref<boolean>(false);
|
||||||
|
|
||||||
|
// computed
|
||||||
|
const level = computed(() => Toolkit.toUserLevel(userRequest.value?.profile.exp));
|
||||||
|
const levelStyle = computed(() => {
|
||||||
|
return {
|
||||||
|
border: `1px solid var(--tui-level-${level.value?.value})`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const levelValueStyle = computed(() => {
|
||||||
|
return {
|
||||||
|
width: level.value?.percent * 100 + "%",
|
||||||
|
background: `var(--tui-level-${level.value?.value})`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const isBoy = computed(() => userRequest.value?.profile.sex === 1);
|
||||||
|
const isGirl = computed(() => userRequest.value?.profile.sex === 0);
|
||||||
|
|
||||||
|
// method
|
||||||
|
const updateSex = (newSex: number) => {
|
||||||
|
if (userRequest.value) {
|
||||||
|
if (userRequest.value.profile.sex === newSex) {
|
||||||
|
userRequest.value.profile.sex = undefined;
|
||||||
|
} else {
|
||||||
|
userRequest.value.profile.sex = newSex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const doSubmit = async () => {
|
||||||
|
if (!userRequest.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
doSubmitting.value = true;
|
||||||
|
|
||||||
|
await Toolkit.sleep(500);
|
||||||
|
|
||||||
|
if (wrapperSelectorComp.value) {
|
||||||
|
const value = wrapperSelectorComp.value.getValue();
|
||||||
|
if (value) {
|
||||||
|
userRequest.value.profile.wrapper = Toolkit.base64ToFile(value, "wrapper.png");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (avatarEditorComp.value) {
|
||||||
|
const value = avatarEditorComp.value.getValue();
|
||||||
|
if (value) {
|
||||||
|
userRequest.value.profile.avatar = Toolkit.base64ToFile(value, "avatar.png");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UserAPI.updateProfile(userRequest.value).then(async () => {
|
||||||
|
await MessagePlugin.success("保存成功");
|
||||||
|
await userStore.reloadProfile();
|
||||||
|
await visitUserStore.reloadVisitUserView();
|
||||||
|
updateProfile();
|
||||||
|
|
||||||
|
wrapperSelectorComp.value && wrapperSelectorComp.value.cancel();
|
||||||
|
avatarEditorComp.value && avatarEditorComp.value.cancel();
|
||||||
|
doSubmitting.value = false;
|
||||||
|
}).catch(async (msg) => {
|
||||||
|
await MessagePlugin.error("保存失败:" + msg);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateProfile = () => {
|
||||||
|
if (visitUserStore.visitUserView) {
|
||||||
|
const cloneObj = {} as UserRequest;
|
||||||
|
Toolkit.deepClone(visitUserStore.visitUserView, cloneObj);
|
||||||
|
userRequest.value = cloneObj;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
watch(() => visitUserStore.visitUserView, updateProfile);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!userRequest.value) {
|
||||||
|
updateProfile();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.profile-info {
|
||||||
|
padding: 2rem 4rem;
|
||||||
|
transition: padding 500ms var(--tui-bezier);
|
||||||
|
|
||||||
|
.image-type-selector {
|
||||||
|
width: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level {
|
||||||
|
width: calc(16rem - 19px - 8px);
|
||||||
|
height: 5px;
|
||||||
|
display: flex;
|
||||||
|
padding: 1px;
|
||||||
|
margin-left: 4px;
|
||||||
|
|
||||||
|
.value {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.short {
|
||||||
|
width: 16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sex {
|
||||||
|
cursor: var(--pointer);
|
||||||
|
border: 1px solid #FAC7D4;
|
||||||
|
margin: 4px .618rem 0 0;
|
||||||
|
display: flex;
|
||||||
|
padding: 3px;
|
||||||
|
background: #FFF;
|
||||||
|
pointer-events: all;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border: 1px solid #525870;
|
||||||
|
background: #F1F1F1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit {
|
||||||
|
width: 8rem;
|
||||||
|
margin: 2rem auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 720px) {
|
||||||
|
.profile-info {
|
||||||
|
padding: 2rem;
|
||||||
|
transition: padding 500ms var(--tui-bezier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
36
src/views/SpaceIndex.vue
Normal file
36
src/views/SpaceIndex.vue
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-index">
|
||||||
|
<authorize-form
|
||||||
|
class="form"
|
||||||
|
@login-success="onSuccess"
|
||||||
|
@register-success="onSuccess"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { userStore } from "timi-web";
|
||||||
|
import { AuthorizeForm } from "timi-tdesign-pc";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const onSuccess = async () => {
|
||||||
|
await router.push({
|
||||||
|
name: "ProfileInfo",
|
||||||
|
params: {
|
||||||
|
id: userStore.loginUser.user?.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.space-index {
|
||||||
|
margin-top: 4rem;
|
||||||
|
|
||||||
|
.form {
|
||||||
|
width: 20rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
134
src/views/comment/CommentList.vue
Normal file
134
src/views/comment/CommentList.vue
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<template>
|
||||||
|
<div class="comment-list">
|
||||||
|
<div class="items">
|
||||||
|
<loading :showOn="!pageResult" />
|
||||||
|
<template v-if="pageResult">
|
||||||
|
<section class="item" v-for="item in pageResult.list" :key="item.id">
|
||||||
|
<header class="header">
|
||||||
|
<div class="left">
|
||||||
|
<div class="biz-type" v-text="(<any> UserCommentBizType)[item.bizType]"></div>
|
||||||
|
<h5 v-if="item.article" class="title selectable" v-text="(item.article as any).title"></h5>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<t-link
|
||||||
|
:href="UserAPI.getCommentTargetUrl(item)"
|
||||||
|
class="word-space"
|
||||||
|
theme="primary"
|
||||||
|
hover="color"
|
||||||
|
target="_blank"
|
||||||
|
>查看</t-link>
|
||||||
|
<t-popconfirm
|
||||||
|
content="关联这个评论的所有回复也会删除,确定继续删除这条评论吗?"
|
||||||
|
@confirm="doDelete(item)"
|
||||||
|
:popup-props="{ placement: 'left' }"
|
||||||
|
>
|
||||||
|
<t-link class="del" theme="danger" hover="color">删除</t-link>
|
||||||
|
</t-popconfirm>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="content gray clip-text selectable" v-text="item.content"></div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<footer v-if="pageResult && 16 < pageResult.total" class="footer">
|
||||||
|
<t-pagination
|
||||||
|
:total="pageResult.total"
|
||||||
|
:pageSize="16"
|
||||||
|
:showPageSize="false"
|
||||||
|
:onCurrentChange="(current: number) => page.index = current - 1"
|
||||||
|
>
|
||||||
|
<template #totalContent>
|
||||||
|
<div style="flex: 1" v-text="`共 ${pageResult.total} 条评论`"></div>
|
||||||
|
</template>
|
||||||
|
</t-pagination>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { CommentView, Loading, Page, PageResult, Toolkit } from "timi-web";
|
||||||
|
import { UserCommentBizType } from "@/types/User.ts";
|
||||||
|
import UserAPI from "@/api/UserAPI.ts";
|
||||||
|
|
||||||
|
const page = reactive<Page>({
|
||||||
|
index: 0,
|
||||||
|
size: 12
|
||||||
|
});
|
||||||
|
const pageResult = ref<PageResult<CommentView>>();
|
||||||
|
const fetchList = Toolkit.debounce(async () => pageResult.value = await UserAPI.listComment(page));
|
||||||
|
watch(() => page.index, fetchList);
|
||||||
|
onMounted(fetchList);
|
||||||
|
|
||||||
|
async function doDelete(item: CommentView) {
|
||||||
|
item.id && await UserAPI.deleteComment(item.id);
|
||||||
|
await fetchList();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.comment-list {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.items {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
font-size: 1rem;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
padding: .5rem 1rem 2rem 1rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--tui-dark-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.biz-type {
|
||||||
|
width: fit-content;
|
||||||
|
color: #FFF;
|
||||||
|
padding: .1rem .5rem;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--tui-light-blue);
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
display: flex;
|
||||||
|
font-size: 14px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
144
src/views/comment/ReplyByReceiverList.vue
Normal file
144
src/views/comment/ReplyByReceiverList.vue
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
<template>
|
||||||
|
<div class="reply-by-receiver-list">
|
||||||
|
<div class="items">
|
||||||
|
<loading :showOn="!pageResult"/>
|
||||||
|
<template v-if="pageResult">
|
||||||
|
<section class="item" v-for="item in pageResult.list" :key="item.id">
|
||||||
|
<header class="header">
|
||||||
|
<div class="left">
|
||||||
|
<div class="biz-type" v-text="(<any> UserCommentBizType)[item.comment.bizType]"></div>
|
||||||
|
<h5 v-if="item.comment.article" class="title selectable" v-text="(item.comment.article as any).title"></h5>
|
||||||
|
<div class="gray sender">
|
||||||
|
<span class="word-space" v-if="item.sender" v-text="item.sender.name"></span>
|
||||||
|
<span class="word-space" v-if="item.senderNick" v-text="item.senderNick"></span>
|
||||||
|
<span v-if="item.sender || item.senderNick">回复</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<t-link
|
||||||
|
:href="UserAPI.getCommentTargetUrl(item.comment)"
|
||||||
|
class="word-space"
|
||||||
|
theme="primary"
|
||||||
|
hover="color" target="_blank"
|
||||||
|
>查看</t-link>
|
||||||
|
<t-popconfirm
|
||||||
|
content="确定继续删除这条回复通知吗?"
|
||||||
|
@confirm="doDelete(item)"
|
||||||
|
:popup-props="{ placement: 'left' }"
|
||||||
|
>
|
||||||
|
<t-link class="del" theme="danger" hover="color">删除</t-link>
|
||||||
|
</t-popconfirm>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="content gray clip-text selectable" v-text="item.content"></div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<footer v-if="pageResult && 16 < pageResult.total" class="footer">
|
||||||
|
<t-pagination
|
||||||
|
v-if="pageResult"
|
||||||
|
:total="pageResult.total"
|
||||||
|
:pageSize="16"
|
||||||
|
:showPageSize="false"
|
||||||
|
:onCurrentChange="(current: number) => page.index = current - 1"
|
||||||
|
>
|
||||||
|
<template #totalContent>
|
||||||
|
<div style="flex: 1" v-text="`共 ${pageResult.total} 条回复`"></div>
|
||||||
|
</template>
|
||||||
|
</t-pagination>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { CommentReplyBizType, CommentReplyPage, CommentReplyView, Loading, PageResult, Toolkit } from "timi-web";
|
||||||
|
import { UserCommentBizType } from "@/types/User.ts";
|
||||||
|
import UserAPI from "@/api/UserAPI.ts";
|
||||||
|
|
||||||
|
const page = reactive<CommentReplyPage>({
|
||||||
|
bizType: (<any>CommentReplyBizType).RECEIVER,
|
||||||
|
index: 0,
|
||||||
|
size: 12
|
||||||
|
});
|
||||||
|
const pageResult = ref<PageResult<CommentReplyView>>();
|
||||||
|
const fetchList = Toolkit.debounce(async () => pageResult.value = await UserAPI.listCommentReply(page));
|
||||||
|
watch(() => page.index, fetchList);
|
||||||
|
onMounted(fetchList);
|
||||||
|
|
||||||
|
async function doDelete(item: CommentReplyView) {
|
||||||
|
item.id && await UserAPI.ignoreCommentReply(item.id);
|
||||||
|
await fetchList();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.reply-by-receiver-list {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.items {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 1rem;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
padding: .5rem 1rem 2rem 1rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--tui-dark-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.biz-type {
|
||||||
|
width: fit-content;
|
||||||
|
color: #FFF;
|
||||||
|
padding: .1rem .5rem;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--tui-light-blue);
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
display: flex;
|
||||||
|
font-size: 14px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
147
src/views/comment/ReplyBySenderList.vue
Normal file
147
src/views/comment/ReplyBySenderList.vue
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<div class="reply-by-sender-list">
|
||||||
|
<div class="items">
|
||||||
|
<loading :showOn="!pageResult"/>
|
||||||
|
<template v-if="pageResult">
|
||||||
|
<section class="item" v-for="item in pageResult.list" :key="item.id">
|
||||||
|
<header class="header">
|
||||||
|
<div class="left">
|
||||||
|
<div class="biz-type" v-text="(<any> UserCommentBizType)[item.comment.bizType]"></div>
|
||||||
|
<h5 v-if="item.comment.article" class="title selectable" v-text="(item.comment.article as any).title"></h5>
|
||||||
|
<div class="gray receiver">
|
||||||
|
<span v-if="item.receiver || item.receiverNick" class="word-space">回复</span>
|
||||||
|
<span v-if="item.receiver" v-text="item.receiver.name"></span>
|
||||||
|
<span v-if="item.receiverNick" v-text="item.receiverNick"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<t-link
|
||||||
|
:href="UserAPI.getCommentTargetUrl(item.comment)"
|
||||||
|
class="word-space"
|
||||||
|
theme="primary"
|
||||||
|
hover="color"
|
||||||
|
target="_blank"
|
||||||
|
>查看</t-link>
|
||||||
|
<t-popconfirm
|
||||||
|
content="确定继续删除这条回复吗?"
|
||||||
|
@confirm="doDelete(item)"
|
||||||
|
:popup-props="{ placement: 'left' }"
|
||||||
|
>
|
||||||
|
<t-link class="del" theme="danger" hover="color">删除</t-link>
|
||||||
|
</t-popconfirm>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="content gray clip-text selectable" v-text="item.content"></div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<footer v-if="pageResult && 16 < pageResult.total" class="footer">
|
||||||
|
<t-pagination
|
||||||
|
v-if="pageResult"
|
||||||
|
:total="pageResult.total"
|
||||||
|
:pageSize="16"
|
||||||
|
:showPageSize="false"
|
||||||
|
:onCurrentChange="(current: number) => page.index = current - 1"
|
||||||
|
>
|
||||||
|
<template #totalContent>
|
||||||
|
<div style="flex: 1" v-text="`共 ${pageResult.total} 条回复`"></div>
|
||||||
|
</template>
|
||||||
|
</t-pagination>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { CommentReplyBizType, CommentReplyPage, CommentReplyView, Loading, PageResult, Toolkit } from "timi-web";
|
||||||
|
import { UserCommentBizType } from "@/types/User.ts";
|
||||||
|
import UserAPI from "@/api/UserAPI.ts";
|
||||||
|
|
||||||
|
const page = reactive<CommentReplyPage>({
|
||||||
|
bizType: (<any>CommentReplyBizType).SENDER,
|
||||||
|
index: 0,
|
||||||
|
size: 12
|
||||||
|
});
|
||||||
|
const pageResult = ref<PageResult<CommentReplyView>>();
|
||||||
|
const fetchList = Toolkit.debounce(async () => pageResult.value = await UserAPI.listCommentReply(page));
|
||||||
|
watch(() => page.index, fetchList);
|
||||||
|
onMounted(fetchList);
|
||||||
|
|
||||||
|
async function doDelete(item: CommentReplyView) {
|
||||||
|
item.id && await UserAPI.deleteCommentReply(item.id);
|
||||||
|
await fetchList();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.reply-by-sender-list {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.items {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
font-size: 1rem;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
padding: .5rem 1rem 2rem 1rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--tui-dark-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.biz-type {
|
||||||
|
width: fit-content;
|
||||||
|
color: #FFF;
|
||||||
|
padding: .1rem .5rem;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--tui-light-blue);
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receiver {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
display: flex;
|
||||||
|
font-size: 14px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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
|
||||||
|
}
|
||||||
42
tsconfig.json
Normal file
42
tsconfig.json
Normal 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
12
tsconfig.node.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"vite.config.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
90
vite.config.ts
Normal file
90
vite.config.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
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"
|
||||||
|
],
|
||||||
|
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: 82,
|
||||||
|
host: "space.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user