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
|
||||||
78
.eslintrc.cjs
Normal file
78
.eslintrc.cjs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
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" // 允许单个词语的组件名
|
||||||
|
}
|
||||||
|
};
|
||||||
146
.gitignore
vendored
146
.gitignore
vendored
@ -1,138 +1,30 @@
|
|||||||
# ---> 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
|
||||||
|
/vite.config.ts.timestamp*
|
||||||
|
|||||||
19
README.md
19
README.md
@ -1,3 +1,18 @@
|
|||||||
# git
|
# Vue 3 + TypeScript + Vite
|
||||||
|
|
||||||
Git 前端
|
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.
|
||||||
|
|||||||
12
index.html
Normal file
12
index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!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" />
|
||||||
|
<title>开源项目 - 夜雨</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
47
package.json
Normal file
47
package.json
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "git",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"less": "^4.3.0",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"tdesign-vue-next": "^1.14.2",
|
||||||
|
"timi-tdesign-pc": "link:..\\timi-tdesign-pc",
|
||||||
|
"timi-web": "link:..\\timi-web",
|
||||||
|
"vue-clipboard3": "^2.0.0",
|
||||||
|
"vue-router": "4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.13.5",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.25.0",
|
||||||
|
"@typescript-eslint/parser": "^8.25.0",
|
||||||
|
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.4",
|
||||||
|
"@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",
|
||||||
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.3",
|
||||||
|
"eslint-plugin-promise": "^7.2.1",
|
||||||
|
"eslint-plugin-vue": "^9.32.0",
|
||||||
|
"prettier": "^3.5.2",
|
||||||
|
"less": "^4.3.0",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"unplugin-auto-import": "^19.3.0",
|
||||||
|
"unplugin-vue-components": "^28.7.0",
|
||||||
|
"vite": "6.3.4",
|
||||||
|
"vite-plugin-dts": "^4.5.4",
|
||||||
|
"vite-plugin-vue-setup-extend": "^0.4.0",
|
||||||
|
"vue": "^3.5.17",
|
||||||
|
"vue-tsc": "^2.2.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
9970
pnpm-lock.yaml
generated
Normal file
9970
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 |
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="ready" class="diselect" icp="粤ICP备2025368555号-1" domain="imyeyu.com" author="夜雨">
|
||||||
|
<router-view />
|
||||||
|
</root-layout>
|
||||||
|
<popup />
|
||||||
|
<user-profile-popup />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { BEFlowerFall, Popup } from "timi-web";
|
||||||
|
import { RootLayout, UserProfilePopup } from "timi-tdesign-pc";
|
||||||
|
|
||||||
|
const ready = ref(false);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
ready.value = true;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.background {
|
||||||
|
background: url("@/assets/img/main.png") fixed right bottom;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
27
src/api/IssueAPI.ts
Normal file
27
src/api/IssueAPI.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { axios, CaptchaData, PageResult } from "timi-web";
|
||||||
|
import { Issue, IssueView, Page } from "@/types/Issue";
|
||||||
|
|
||||||
|
const BASE_URI = "/git/issue";
|
||||||
|
|
||||||
|
async function view(id: number): Promise<IssueView> {
|
||||||
|
return axios.get(`${BASE_URI}/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function page(page: Page): Promise<PageResult<Issue>> {
|
||||||
|
return axios.post(`${BASE_URI}/list`, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create(captchaData: CaptchaData<Issue>): Promise<void> {
|
||||||
|
return axios.post(`${BASE_URI}/create`, captchaData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update(issue: Issue): Promise<void> {
|
||||||
|
return axios.post(`${BASE_URI}/update`, issue);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
view,
|
||||||
|
page,
|
||||||
|
create,
|
||||||
|
update
|
||||||
|
};
|
||||||
27
src/api/MergeAPI.ts
Normal file
27
src/api/MergeAPI.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { axios, CaptchaData, PageResult } from "timi-web";
|
||||||
|
import { Merge, MergeView, Page } from "@/types/Merge";
|
||||||
|
|
||||||
|
const BASE_URI = "/git/merge";
|
||||||
|
|
||||||
|
async function view(id: number): Promise<MergeView> {
|
||||||
|
return axios.get(`${BASE_URI}/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function page(page: Page): Promise<PageResult<Merge>> {
|
||||||
|
return axios.post(`${BASE_URI}/list`, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create(captchaData: CaptchaData<Merge>): Promise<void> {
|
||||||
|
return axios.post(`${BASE_URI}/create`, captchaData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update(merge: Merge): Promise<void> {
|
||||||
|
return axios.post(`${BASE_URI}/update`, merge);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
view,
|
||||||
|
page,
|
||||||
|
create,
|
||||||
|
update
|
||||||
|
};
|
||||||
12
src/api/ReleaseAPI.ts
Normal file
12
src/api/ReleaseAPI.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { axios, PageResult } from "timi-web";
|
||||||
|
import { Page, Release } from "@/types/Release";
|
||||||
|
|
||||||
|
const BASE_URI = "/git/release";
|
||||||
|
|
||||||
|
async function page(page: Page): Promise<PageResult<Release>> {
|
||||||
|
return axios.post(`${BASE_URI}/list`, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
page
|
||||||
|
};
|
||||||
48
src/api/RepositoryAPI.ts
Normal file
48
src/api/RepositoryAPI.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { axios, Page, PageResult } from "timi-web";
|
||||||
|
import { File, Repository, RepositoryView } from "@/types/Repository";
|
||||||
|
import { ActionLogView } from "@/types/Common.ts";
|
||||||
|
|
||||||
|
const BASE_URI = "/git/repository";
|
||||||
|
const API_HOST = import.meta.env.VITE_API;
|
||||||
|
|
||||||
|
async function view(name: string): Promise<RepositoryView> {
|
||||||
|
return axios.get(`${BASE_URI}/${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function page(page: Page): Promise<PageResult<Repository>> {
|
||||||
|
return axios.post(`${BASE_URI}/list`, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pagePush(name: string, branch: string, page: Page): Promise<PageResult<ActionLogView>> {
|
||||||
|
return axios.post(`${BASE_URI}/${name}:${branch}/log/push`, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listFile(name: string, branch: string, path: string): Promise<File[]> {
|
||||||
|
return axios.get(`${BASE_URI}/${name}:${branch}/file/list${path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fileRaw(name: string, branch: string, path: string): Promise<ArrayBuffer> {
|
||||||
|
return axios({
|
||||||
|
url: `${BASE_URI}/${name}:${branch}/file/raw${path}`,
|
||||||
|
method: "GET",
|
||||||
|
responseType: "arraybuffer"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadArchive(name: string, branch: string) {
|
||||||
|
window.open(`${API_HOST}${BASE_URI}/${name}:${branch}/archive`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pageLog(page: Page): Promise<PageResult<ActionLogView>> {
|
||||||
|
return axios.post(`${BASE_URI}/log`, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
view,
|
||||||
|
page,
|
||||||
|
pagePush,
|
||||||
|
listFile,
|
||||||
|
fileRaw,
|
||||||
|
downloadArchive,
|
||||||
|
pageLog
|
||||||
|
};
|
||||||
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 |
90
src/components/LoginMenu.vue
Normal file
90
src/components/LoginMenu.vue
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-menu">
|
||||||
|
<t-dropdown
|
||||||
|
v-if="userStore.loginUser.user"
|
||||||
|
class="logged"
|
||||||
|
trigger="click"
|
||||||
|
>
|
||||||
|
<t-button class="button" variant="text">
|
||||||
|
<template #icon>
|
||||||
|
<t-avatar
|
||||||
|
class="icon"
|
||||||
|
:class="(<any>ImageType)[userStore.loginUser.user.profile.avatarType]"
|
||||||
|
:image="UserAPI.getAvatarURL(userStore.loginUser.user.profile)"
|
||||||
|
size="small"
|
||||||
|
shape="round"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<span v-text="userStore.loginUser.user.name"></span>
|
||||||
|
<template #suffix>
|
||||||
|
<icon name="ARROW_1_S" />
|
||||||
|
</template>
|
||||||
|
</t-button>
|
||||||
|
<t-dropdown-menu>
|
||||||
|
<t-dropdown-item @click="toDeveloperConfig">
|
||||||
|
<template #prefixIcon>
|
||||||
|
<icon name="TOOL" />
|
||||||
|
</template>
|
||||||
|
<span>开发配置</span>
|
||||||
|
</t-dropdown-item>
|
||||||
|
<t-dropdown-item @click="userStore.logout()">
|
||||||
|
<template #prefixIcon>
|
||||||
|
<icon />
|
||||||
|
</template>
|
||||||
|
<span>退出登录</span>
|
||||||
|
</t-dropdown-item>
|
||||||
|
</t-dropdown-menu>
|
||||||
|
</t-dropdown>
|
||||||
|
<t-button
|
||||||
|
v-if="!userStore.isLogged()"
|
||||||
|
variant="text"
|
||||||
|
@click="visitableAuth = true"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<icon class="icon" name="USER" />
|
||||||
|
</template>
|
||||||
|
<span>登录</span>
|
||||||
|
</t-button>
|
||||||
|
</div>
|
||||||
|
<t-dialog
|
||||||
|
v-model:visible="visitableAuth"
|
||||||
|
attach="body"
|
||||||
|
:close-btn="false"
|
||||||
|
:header="false"
|
||||||
|
:footer="false"
|
||||||
|
>
|
||||||
|
<authorize-form
|
||||||
|
v-if="visitableAuth"
|
||||||
|
@login-success="visitableAuth = false"
|
||||||
|
@register-success="visitableAuth = false"
|
||||||
|
/>
|
||||||
|
</t-dialog>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Icon, ImageType, SettingKey, SettingMapper, UserAPI, userStore } from "timi-web";
|
||||||
|
import { AuthorizeForm } from "timi-tdesign-pc";
|
||||||
|
|
||||||
|
const visitableAuth = ref<boolean>(false);
|
||||||
|
|
||||||
|
const toDeveloperConfig = () => {
|
||||||
|
const loginUserId = userStore.loginUser.token?.id;
|
||||||
|
if (loginUserId) {
|
||||||
|
window.open(`https://${SettingMapper.getValue(SettingKey.DOMAIN_SPACE)}/${loginUserId}/developer`, "_blank");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.login-menu {
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logged {
|
||||||
|
|
||||||
|
&.button {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
124
src/components/PushLogTimeline.vue
Normal file
124
src/components/PushLogTimeline.vue
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<t-timeline class="push-log-timeline" mode="alternate">
|
||||||
|
<t-timeline-item
|
||||||
|
class="action"
|
||||||
|
v-for="(action, actionIndex) in items"
|
||||||
|
:key="actionIndex"
|
||||||
|
dot-color="primary"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<div class="time" v-text="Time.toPassedDate(action.operatedAt)" v-popup="Time.toDateTime(action.operatedAt)"></div>
|
||||||
|
</template>
|
||||||
|
<t-card class="item">
|
||||||
|
<div>
|
||||||
|
<div class="user">
|
||||||
|
<icon class="avatar" name="USER"/>
|
||||||
|
<span v-text="action.operator.name"></span>
|
||||||
|
</div>
|
||||||
|
<div v-if="showRepo" class="repository">
|
||||||
|
<span>仓库:</span>
|
||||||
|
<router-link class="link selectable" :to="`/${action.repoName}/TODO_BRANCH`">
|
||||||
|
{{ action.repoName }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div v-if="showBranch" class="branch">
|
||||||
|
<span>分支:</span>
|
||||||
|
<span class="selectable" v-text="action.refName"></span>
|
||||||
|
</div>
|
||||||
|
<template v-if="action.commitList">
|
||||||
|
<h4 v-if="showRepo || showBranch">提交记录:</h4>
|
||||||
|
<div class="commits">
|
||||||
|
<div
|
||||||
|
class="commit"
|
||||||
|
v-for="(commit, commitIndex) in action.commitList"
|
||||||
|
:key="commitIndex"
|
||||||
|
>
|
||||||
|
<t-button
|
||||||
|
class="sha1"
|
||||||
|
size="small"
|
||||||
|
theme="default"
|
||||||
|
:content="`#${commit.sha.substring(0, 8)}`"
|
||||||
|
@click="MessagePlugin.warning('文件比对功能暂不可用')"
|
||||||
|
></t-button>
|
||||||
|
<div class="msg selectable" v-text="commit.message"></div>
|
||||||
|
<div
|
||||||
|
class="light-gray"
|
||||||
|
v-text="Time.toPassedDate(commit.committedAt)"
|
||||||
|
v-popup="Time.toDateTime(commit.committedAt)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else>初始化分支</div>
|
||||||
|
</div>
|
||||||
|
</t-card>
|
||||||
|
</t-timeline-item>
|
||||||
|
</t-timeline>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Icon, Time } from "timi-web";
|
||||||
|
import { MessagePlugin } from "tdesign-vue-next";
|
||||||
|
import { ActionLogView } from "@/types/Common.ts";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "PushLogTimeline"
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
items?: ActionLogView[];
|
||||||
|
showRepo?: boolean;
|
||||||
|
showBranch?: boolean;
|
||||||
|
}>(), {
|
||||||
|
items: () => [],
|
||||||
|
showRepo: true,
|
||||||
|
showBranch: true
|
||||||
|
});
|
||||||
|
const { items } = toRefs(props);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.push-log-timeline {
|
||||||
|
|
||||||
|
:deep(.action) {
|
||||||
|
|
||||||
|
.t-timeline-item__label {
|
||||||
|
width: 90px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
|
||||||
|
.user {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.repository {
|
||||||
|
margin-top: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commits {
|
||||||
|
|
||||||
|
.commit {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&:nth-last-child(n + 2) {
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sha1 {
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
568
src/components/TimelineComment.vue
Normal file
568
src/components/TimelineComment.vue
Normal file
@ -0,0 +1,568 @@
|
|||||||
|
<template>
|
||||||
|
<div class="comment-time-line">
|
||||||
|
<t-timeline
|
||||||
|
class="comments"
|
||||||
|
layout="vertical"
|
||||||
|
mode="alternate"
|
||||||
|
>
|
||||||
|
<t-timeline-item
|
||||||
|
class="comment"
|
||||||
|
:class="{'system': comment.systemComment}"
|
||||||
|
v-for="(comment, index) in comments"
|
||||||
|
:key="index"
|
||||||
|
dot-color="primary"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<div class="time">
|
||||||
|
<t-popup
|
||||||
|
:delay="0"
|
||||||
|
:content="Time.toDateTime(comment.systemComment ? comment.systemComment.time : comment.createdAt)"
|
||||||
|
placement="left"
|
||||||
|
:show-arrow="true"
|
||||||
|
:destroy-on-close="true"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-text="Time.toPassedDateTime(comment.systemComment ? comment.systemComment.time : comment.createdAt)"></span>
|
||||||
|
</t-popup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<t-card class="shadow">
|
||||||
|
<div
|
||||||
|
v-if="comment.systemComment"
|
||||||
|
class="system-comment-line"
|
||||||
|
:class="comment.systemComment.colorClass"
|
||||||
|
></div>
|
||||||
|
<template #title>
|
||||||
|
<div class="comment-user">
|
||||||
|
<template v-if="comment.user">
|
||||||
|
<t-avatar
|
||||||
|
class="icon"
|
||||||
|
:class="(<any>ImageType)[comment.user.profile.avatarType]"
|
||||||
|
:image="UserAPI.getAvatarURL(comment.user.profile)"
|
||||||
|
size="small"
|
||||||
|
shape="round"
|
||||||
|
/>
|
||||||
|
<h4 class="name" v-text="comment.user.name"></h4>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<icon v-if="!comment.systemComment" class="icon" name="USER"/>
|
||||||
|
<h4 v-if="comment.systemComment" class="name">系统消息</h4>
|
||||||
|
<h4 v-else class="name" v-text="comment.nick"></h4>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-if="comment.systemComment" v-text="comment.systemComment.msg"></div>
|
||||||
|
<markdown-view v-else :content="comment.content"/>
|
||||||
|
<div v-if="!comment.systemComment" class="reply-btn">
|
||||||
|
<t-button
|
||||||
|
v-if="props.canCreate"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
:content="`回复(${comment.repliesLength})`"
|
||||||
|
@click="doCommentReply(comment)"
|
||||||
|
></t-button>
|
||||||
|
<span v-else v-text="`回复(${comment.repliesLength})`"></span>
|
||||||
|
</div>
|
||||||
|
<div class="reply-pane"
|
||||||
|
v-if="!comment.systemComment && (replyTo === comment || 0 < comment.repliesLength)">
|
||||||
|
<t-list
|
||||||
|
class="replies"
|
||||||
|
:split="true"
|
||||||
|
v-if="comment.replies"
|
||||||
|
>
|
||||||
|
<t-list-item
|
||||||
|
class="reply"
|
||||||
|
v-for="reply in comment.replies"
|
||||||
|
:key="reply.id"
|
||||||
|
>
|
||||||
|
<div class="content">
|
||||||
|
<div class="header">
|
||||||
|
<div class="user">
|
||||||
|
<template v-if="reply.sender">
|
||||||
|
<t-avatar
|
||||||
|
class="user-icon"
|
||||||
|
:class="(<any>ImageType)[reply.sender.profile.avatarType]"
|
||||||
|
:image="UserAPI.getAvatarURL(reply.sender.profile)"
|
||||||
|
size="small"
|
||||||
|
shape="round"
|
||||||
|
/>
|
||||||
|
<div class="sender" v-text="reply.sender.name"></div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<icon name="USER"/>
|
||||||
|
<div class="sender" v-text="reply.senderNick"></div>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
v-if="reply.receiverId !== comment.userId || reply.receiverNick !== comment.nick">
|
||||||
|
<div class="reply-to gray">回复</div>
|
||||||
|
<div v-if="reply.receiver" v-text="reply.receiver.name"></div>
|
||||||
|
<div v-else v-text="reply.receiverNick"></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="time light-gray" v-text="Time.toPassedDateTime(reply.createdAt)"></div>
|
||||||
|
</div>
|
||||||
|
<markdown-view :content="reply.content"/>
|
||||||
|
<div v-if="props.canCreate" class="reply-ctrl">
|
||||||
|
<t-button
|
||||||
|
class="button"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="doCommentReply(comment, reply)"
|
||||||
|
>回复
|
||||||
|
</t-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t-list-item>
|
||||||
|
</t-list>
|
||||||
|
<t-pagination
|
||||||
|
class="reply-pagination"
|
||||||
|
v-if="6 < comment.repliesLength"
|
||||||
|
:total="comment.repliesLength"
|
||||||
|
v-model="comment.repliesCurrentPage"
|
||||||
|
:pageSize="6"
|
||||||
|
:showPageSize="false"
|
||||||
|
:onCurrentChange="(current: number) => doFetchRepliesEvent(comment, current)"
|
||||||
|
/>
|
||||||
|
<t-form
|
||||||
|
class="form"
|
||||||
|
v-if="replyTo === comment"
|
||||||
|
:data="replyRequest"
|
||||||
|
@submit="doCommentReplySubmit(comment)"
|
||||||
|
>
|
||||||
|
<t-form-item label="昵称" name="nick">
|
||||||
|
<t-input
|
||||||
|
v-if="userStore.isLogged()"
|
||||||
|
class="nick"
|
||||||
|
:value="userStore.loginUser.user?.name"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<t-input v-else class="nick" v-model="replyRequest.senderNick" placeholder=""/>
|
||||||
|
<div
|
||||||
|
v-if="replyToName"
|
||||||
|
class="reply-to gray"
|
||||||
|
v-text="`回复 ${replyToName}`"
|
||||||
|
></div>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="内容" name="content">
|
||||||
|
<markdown-editor
|
||||||
|
:minRows="4"
|
||||||
|
:maxRows="16"
|
||||||
|
v-model:data="replyRequest.content"
|
||||||
|
/>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="验证码" name="captcha">
|
||||||
|
<t-input-adornment>
|
||||||
|
<t-input class="captcha" v-model="replyCaptcha" placeholder=""/>
|
||||||
|
<template #append>
|
||||||
|
<captcha :width="90" :height="28" :api="CommonAPI.getCaptchaAPI()"
|
||||||
|
:from="CaptchaFrom.COMMENT_REPLY"/>
|
||||||
|
</template>
|
||||||
|
</t-input-adornment>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item>
|
||||||
|
<t-space>
|
||||||
|
<t-button type="submit">提交</t-button>
|
||||||
|
<t-button theme="default" variant="outline" @click="replyTo = undefined">取消
|
||||||
|
</t-button>
|
||||||
|
</t-space>
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
</div>
|
||||||
|
</t-card>
|
||||||
|
</t-timeline-item>
|
||||||
|
</t-timeline>
|
||||||
|
<div class="more">
|
||||||
|
<p v-if="isFinished" class="gray" @click="doFetchEvent">没有更多评论</p>
|
||||||
|
<t-button v-if="!isFetching && !isFinished" @click="doFetchEvent">加载更多</t-button>
|
||||||
|
</div>
|
||||||
|
<t-form v-if="props.canCreate" class="comment-form" :data="commentRequest" @submit="doCommentSubmit">
|
||||||
|
<t-form-item label="昵称" name="name">
|
||||||
|
<t-input
|
||||||
|
v-if="userStore.isLogged()"
|
||||||
|
class="nick"
|
||||||
|
:value="userStore.loginUser?.user?.name"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<t-input v-else class="nick" v-model="commentRequest.nick" placeholder=""/>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="内容" name="course">
|
||||||
|
<markdown-editor :minRows="8" v-model:data="commentRequest.content"/>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="验证码" name="captcha">
|
||||||
|
<t-input-adornment>
|
||||||
|
<t-input class="captcha" v-model="commentCaptcha" placeholder=""/>
|
||||||
|
<template #append>
|
||||||
|
<captcha :width="90" :height="28" :api="CommonAPI.getCaptchaAPI()"
|
||||||
|
:from="CaptchaFrom.COMMENT"/>
|
||||||
|
</template>
|
||||||
|
</t-input-adornment>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item>
|
||||||
|
<t-button type="submit">提交</t-button>
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { MessagePlugin } from "tdesign-vue-next";
|
||||||
|
import {
|
||||||
|
Captcha,
|
||||||
|
CaptchaFrom,
|
||||||
|
Comment,
|
||||||
|
CommentAPI,
|
||||||
|
CommentBizType,
|
||||||
|
CommentReply,
|
||||||
|
CommentReplyBizType,
|
||||||
|
CommentReplyView,
|
||||||
|
CommentView,
|
||||||
|
CommonAPI,
|
||||||
|
Icon,
|
||||||
|
ImageType,
|
||||||
|
MarkdownEditor,
|
||||||
|
MarkdownView,
|
||||||
|
OrderType,
|
||||||
|
Page,
|
||||||
|
Time,
|
||||||
|
UserAPI,
|
||||||
|
userStore
|
||||||
|
} from "timi-web";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
bizType: CommentBizType,
|
||||||
|
bizId: number;
|
||||||
|
canCreate?: boolean;
|
||||||
|
systemComments?: SystemComment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SystemComment = {
|
||||||
|
colorClass?: string;
|
||||||
|
time: number;
|
||||||
|
msg?: string;
|
||||||
|
inserted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommentItem = {
|
||||||
|
systemComment?: SystemComment;
|
||||||
|
repliesCurrentPage: number;
|
||||||
|
} & CommentView;
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
canCreate: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const comments = reactive<CommentItem[]>([]);
|
||||||
|
const commentCaptcha = ref();
|
||||||
|
|
||||||
|
const commentRequest = reactive<Comment>({
|
||||||
|
bizType: props.bizType,
|
||||||
|
bizId: props.bizId,
|
||||||
|
nick: "",
|
||||||
|
content: ""
|
||||||
|
});
|
||||||
|
const replyCaptcha = ref();
|
||||||
|
const replyRequest = reactive<CommentReply>({
|
||||||
|
commentId: -1,
|
||||||
|
replyId: undefined,
|
||||||
|
senderNick: "",
|
||||||
|
content: ""
|
||||||
|
});
|
||||||
|
const replyTo = ref<CommentItem>();
|
||||||
|
const replyToName = ref();
|
||||||
|
const isFinished = ref(false);
|
||||||
|
const isFetching = ref(true);
|
||||||
|
|
||||||
|
const page = ref<Page>({
|
||||||
|
index: 0,
|
||||||
|
size: 12
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => commentRequest.nick, () => {
|
||||||
|
if (!userStore.isLogged()) {
|
||||||
|
replyRequest.senderNick = commentRequest.nick;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
watch(() => replyRequest.senderNick, () => {
|
||||||
|
if (!userStore.isLogged()) {
|
||||||
|
commentRequest.nick = replyRequest.senderNick;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
watch(replyTo, () => {
|
||||||
|
if (!replyTo.value) {
|
||||||
|
replyRequest.content = "";
|
||||||
|
replyCaptcha.value = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const insertSystemComment = (item: SystemComment): boolean => {
|
||||||
|
let insertAt = null;
|
||||||
|
{
|
||||||
|
// 查找插入位置
|
||||||
|
for (let i = comments.length - 1; -1 < i; i--) {
|
||||||
|
const element = comments[i];
|
||||||
|
if (element.createdAt && item.time < element.createdAt) {
|
||||||
|
insertAt = i;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((insertAt === null && isFinished.value) || comments.length === 0) {
|
||||||
|
insertAt = comments.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (insertAt !== null) {
|
||||||
|
// 可插入
|
||||||
|
item.colorClass = item.colorClass ?? "bg-blue";
|
||||||
|
comments.splice(insertAt, 0, {
|
||||||
|
systemComment: item
|
||||||
|
} as CommentItem);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const doFetchEvent = async () => {
|
||||||
|
isFetching.value = true;
|
||||||
|
|
||||||
|
const fetchResult = await CommentAPI.page({
|
||||||
|
bizType: props.bizType,
|
||||||
|
bizId: props.bizId,
|
||||||
|
orderMap: {
|
||||||
|
"createdAt": OrderType.ASC
|
||||||
|
},
|
||||||
|
...page.value
|
||||||
|
});
|
||||||
|
const list = fetchResult.list;
|
||||||
|
for (let i = 0; i < list.length; i++) {
|
||||||
|
comments.push({
|
||||||
|
repliesCurrentPage: 1,
|
||||||
|
...list[i]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
page.value.index++;
|
||||||
|
if (fetchResult.list.length < page.value.size) {
|
||||||
|
isFinished.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isFetching.value = false;
|
||||||
|
|
||||||
|
if (props.systemComments) {
|
||||||
|
for (let i = 0; i < props.systemComments.length; i++) {
|
||||||
|
const item = props.systemComments[i];
|
||||||
|
if (!item.inserted) {
|
||||||
|
item.inserted = insertSystemComment(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const doFetchRepliesEvent = async (comment: CommentItem, toPage: number) => {
|
||||||
|
if (comment.id) {
|
||||||
|
const result = await CommentAPI.pageReply({
|
||||||
|
bizType: CommentReplyBizType.COMMENT,
|
||||||
|
bizId: comment.id,
|
||||||
|
index: toPage - 1,
|
||||||
|
size: 6
|
||||||
|
});
|
||||||
|
comment.replies = result.list;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const doCommentReply = async (comment: CommentItem, reply?: CommentReplyView) => {
|
||||||
|
replyTo.value = comment;
|
||||||
|
replyRequest.commentId = comment.id!;
|
||||||
|
|
||||||
|
if (reply) {
|
||||||
|
replyRequest.replyId = reply.id;
|
||||||
|
replyToName.value = reply.sender ? reply.sender.name : reply.senderNick;
|
||||||
|
} else {
|
||||||
|
replyRequest.replyId = undefined;
|
||||||
|
replyToName.value = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const doCommentSubmit = () => {
|
||||||
|
CommentAPI.create({
|
||||||
|
from: CaptchaFrom.GIT_ISSUE,
|
||||||
|
captcha: commentCaptcha.value,
|
||||||
|
data: commentRequest
|
||||||
|
}).then(() => {
|
||||||
|
if (isFinished.value) {
|
||||||
|
isFinished.value = false;
|
||||||
|
page.value.index--;
|
||||||
|
const size = comments.length % page.value.size;
|
||||||
|
comments.splice(comments.length - size, size);
|
||||||
|
doFetchEvent();
|
||||||
|
}
|
||||||
|
commentRequest.content = "";
|
||||||
|
commentCaptcha.value = "";
|
||||||
|
|
||||||
|
MessagePlugin.success("提交成功");
|
||||||
|
}).catch(msg => {
|
||||||
|
MessagePlugin.error("提交失败:" + msg);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const doCommentReplySubmit = async (comment: CommentItem) => {
|
||||||
|
CommentAPI.createReply({
|
||||||
|
from: CaptchaFrom.COMMENT_REPLY,
|
||||||
|
captcha: replyCaptcha.value,
|
||||||
|
data: replyRequest
|
||||||
|
}).then(() => {
|
||||||
|
if (0 < comment.repliesLength) {
|
||||||
|
const size = comment.replies.length % 6;
|
||||||
|
comment.replies.splice(comment.replies.length - size, size);
|
||||||
|
doFetchRepliesEvent(comment, comment.repliesCurrentPage = Math.ceil(comment.repliesLength / 6));
|
||||||
|
} else {
|
||||||
|
doFetchRepliesEvent(comment, comment.repliesCurrentPage = 1);
|
||||||
|
}
|
||||||
|
comment.repliesLength++;
|
||||||
|
|
||||||
|
replyTo.value = undefined;
|
||||||
|
MessagePlugin.success("提交成功");
|
||||||
|
}).catch((msg: string) => {
|
||||||
|
MessagePlugin.error("提交失败:" + msg);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(doFetchEvent);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.comment-time-line {
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
border-top: var(--tui-border);
|
||||||
|
|
||||||
|
.comments {
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
|
||||||
|
.system-comment-line {
|
||||||
|
top: -1px;
|
||||||
|
left: -1px;
|
||||||
|
width: 4px;
|
||||||
|
height: calc(100% + 2px);
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
width: 90px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-user {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-btn {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-pane {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 1rem;
|
||||||
|
align-items: end;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.replies {
|
||||||
|
width: 100%;
|
||||||
|
background: #F8F8F8;
|
||||||
|
padding-left: 2rem;
|
||||||
|
|
||||||
|
.reply {
|
||||||
|
padding: .5rem;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:hover .reply-ctrl .button {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.user {
|
||||||
|
display: flex;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
.sender {
|
||||||
|
margin-left: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-to {
|
||||||
|
margin: 0 .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-ctrl {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
visibility: hidden;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-pagination {
|
||||||
|
margin: .5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
.nick {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-to {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha {
|
||||||
|
width: 6rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.more {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-form {
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
.nick {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha {
|
||||||
|
width: 6rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
102
src/layout/IndexLayout.vue
Normal file
102
src/layout/IndexLayout.vue
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<div class="index-layout">
|
||||||
|
<div class="header">
|
||||||
|
<t-head-menu class="menu" theme="light" v-model="menuValue">
|
||||||
|
<template #logo>
|
||||||
|
<div
|
||||||
|
v-if="owner"
|
||||||
|
class="owner"
|
||||||
|
v-popup:config="popupUserStore.config"
|
||||||
|
@mouseenter="popupUserStore.user.value = owner"
|
||||||
|
>
|
||||||
|
<t-avatar
|
||||||
|
class="icon ir-pixelated"
|
||||||
|
:class="(<any>ImageType)[owner.profile.avatarType]"
|
||||||
|
:image="UserAPI.getAvatarURL(owner.profile)"
|
||||||
|
size="large"
|
||||||
|
shape="round"
|
||||||
|
/>
|
||||||
|
<div class="name">夜雨</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<t-menu-item to="/" :value="Menu.LIST">仓库列表</t-menu-item>
|
||||||
|
<t-menu-item to="/log" :value="Menu.LOG">最近推送</t-menu-item>
|
||||||
|
<template #operations>
|
||||||
|
<login-menu />
|
||||||
|
</template>
|
||||||
|
</t-head-menu>
|
||||||
|
</div>
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ImageType, UserAPI, UserView } from "timi-web";
|
||||||
|
import LoginMenu from "@/components/LoginMenu.vue";
|
||||||
|
import { popupUserStore } from "timi-tdesign-pc";
|
||||||
|
|
||||||
|
// ---------- 个人信息 ----------
|
||||||
|
|
||||||
|
const owner = ref<UserView>();
|
||||||
|
onMounted(async () => owner.value = await UserAPI.view(1));
|
||||||
|
|
||||||
|
// ---------- 导航 ----------
|
||||||
|
|
||||||
|
enum Menu {
|
||||||
|
LIST = "LIST",
|
||||||
|
LOG = "LOG"
|
||||||
|
}
|
||||||
|
|
||||||
|
const regexMenu: {
|
||||||
|
[key: string]: Menu
|
||||||
|
} = {
|
||||||
|
"RepositoryList": Menu.LIST,
|
||||||
|
"RepositoryLog": Menu.LOG
|
||||||
|
};
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const menuValue = ref();
|
||||||
|
|
||||||
|
const syncMenuRouter = (routerName?: string) => {
|
||||||
|
if (routerName) {
|
||||||
|
for (const item in regexMenu) {
|
||||||
|
if (new RegExp(item).test(routerName as string)) {
|
||||||
|
menuValue.value = regexMenu[item];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
watch(() => route.name, (routerName) => syncMenuRouter(routerName as string));
|
||||||
|
onMounted(() => syncMenuRouter(route.name as string));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.index-layout {
|
||||||
|
|
||||||
|
.header {
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
position: sticky;
|
||||||
|
background: rgba(231, 234, 239, .8);
|
||||||
|
border-bottom: var(--tui-border);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
.owner {
|
||||||
|
margin: 0 3rem 0 .25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
166
src/layout/RepositoryLayout.vue
Normal file
166
src/layout/RepositoryLayout.vue
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
<template>
|
||||||
|
<div class="repository-layout" v-if="repositoryStore.value">
|
||||||
|
<t-layout class="header">
|
||||||
|
<t-content>
|
||||||
|
<t-space class="title" align="center">
|
||||||
|
<h3 class="title" v-text="repositoryStore.value.name"></h3>
|
||||||
|
<p class="light-gray" v-text="repositoryStore.value.description"></p>
|
||||||
|
</t-space>
|
||||||
|
</t-content>
|
||||||
|
<t-aside class="aside" width="auto">
|
||||||
|
<t-space align="center">
|
||||||
|
<t-button class="repository-list" theme="default" variant="text" @click="backRepositoryList">
|
||||||
|
<template #icon>
|
||||||
|
<icon class="icon" name="LIST" />
|
||||||
|
</template>
|
||||||
|
<span>仓库列表</span>
|
||||||
|
</t-button>
|
||||||
|
<login-menu />
|
||||||
|
</t-space>
|
||||||
|
</t-aside>
|
||||||
|
</t-layout>
|
||||||
|
<t-head-menu class="menu" v-model="menuValue">
|
||||||
|
<t-menu-item
|
||||||
|
:to="`/${repositoryStore.value.name}/${repositoryStore.value.defaultBranch}`"
|
||||||
|
:value="Menu.FILE"
|
||||||
|
>
|
||||||
|
项目文件
|
||||||
|
</t-menu-item>
|
||||||
|
<t-menu-item
|
||||||
|
:to="`/${repositoryStore.value.name}/commits`"
|
||||||
|
:value="Menu.COMMIT_LOG"
|
||||||
|
>
|
||||||
|
更新记录
|
||||||
|
</t-menu-item>
|
||||||
|
<t-menu-item
|
||||||
|
:to="`/${repositoryStore.value.name}/issues`"
|
||||||
|
:value="Menu.ISSUE"
|
||||||
|
>
|
||||||
|
问题反馈
|
||||||
|
</t-menu-item>
|
||||||
|
<t-menu-item
|
||||||
|
:to="`/${repositoryStore.value.name}/merges`"
|
||||||
|
:value="Menu.MERGE"
|
||||||
|
>
|
||||||
|
合并请求
|
||||||
|
</t-menu-item>
|
||||||
|
<t-menu-item
|
||||||
|
:to="`/${repositoryStore.value.name}/releases`"
|
||||||
|
:value="Menu.RELEASE"
|
||||||
|
>
|
||||||
|
版本发布
|
||||||
|
</t-menu-item>
|
||||||
|
</t-head-menu>
|
||||||
|
<div class="content">
|
||||||
|
<router-view v-slot="{ Component }">
|
||||||
|
<keep-alive>
|
||||||
|
<component :is="Component" v-if="route.meta?.keepAlive" />
|
||||||
|
</keep-alive>
|
||||||
|
<component :is="Component" v-if="!route.meta?.keepAlive" />
|
||||||
|
</router-view>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import RepositoryAPI from "@/api/RepositoryAPI";
|
||||||
|
import { Icon } from "timi-web";
|
||||||
|
import { useRepositoryStore } from "@/store/repository.ts";
|
||||||
|
import LoginMenu from "@/components/LoginMenu.vue";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const repositoryStore = useRepositoryStore();
|
||||||
|
|
||||||
|
// ---------- 导航 ----------
|
||||||
|
|
||||||
|
enum Menu {
|
||||||
|
FILE = "FILE",
|
||||||
|
COMMIT_LOG = "COMMIT_LOG",
|
||||||
|
ISSUE = "ISSUE",
|
||||||
|
MERGE = "MERGE",
|
||||||
|
RELEASE = "RELEASE"
|
||||||
|
}
|
||||||
|
|
||||||
|
const regexMenu: {
|
||||||
|
[key: string]: Menu
|
||||||
|
} = {
|
||||||
|
"FileDetail": Menu.FILE,
|
||||||
|
"CommitLog": Menu.COMMIT_LOG,
|
||||||
|
"Issue.*": Menu.ISSUE,
|
||||||
|
"Merge.*": Menu.MERGE,
|
||||||
|
"Release.*": Menu.RELEASE
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuValue = ref();
|
||||||
|
const syncMenuRouter = (routerName?: string) => {
|
||||||
|
if (routerName) {
|
||||||
|
for (const item in regexMenu) {
|
||||||
|
if (new RegExp(item).test(routerName as string)) {
|
||||||
|
menuValue.value = regexMenu[item];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
watch(() => route.name, (routerName) => syncMenuRouter(routerName as string));
|
||||||
|
onMounted(async () => syncMenuRouter(route.name as string));
|
||||||
|
|
||||||
|
// ---------- 返回主页 ----------
|
||||||
|
|
||||||
|
const backRepositoryList = () => {
|
||||||
|
router.push("/");
|
||||||
|
repositoryStore.value = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------- 页面加载 ----------
|
||||||
|
onMounted(async () => repositoryStore.value = await RepositoryAPI.view(route.params.repository as string));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.repository-layout {
|
||||||
|
border-bottom: var(--tui-border);
|
||||||
|
|
||||||
|
.header {
|
||||||
|
top: 0;
|
||||||
|
height: 50px;
|
||||||
|
z-index: 3;
|
||||||
|
position: sticky;
|
||||||
|
background: rgba(231, 234, 239, .8);
|
||||||
|
border-bottom: var(--tui-border);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
.title {
|
||||||
|
padding-left: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-right: 1rem;
|
||||||
|
|
||||||
|
.repository-list {
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
top: 50px;
|
||||||
|
z-index: 3;
|
||||||
|
position: sticky;
|
||||||
|
background: rgba(255, 255, 255, .8);
|
||||||
|
border-bottom: var(--tui-border);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
:deep(.t-menu) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
62
src/main.ts
Normal file
62
src/main.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
import router from "@/router";
|
||||||
|
import { NavigationGuardNext, RouteLocationNormalized } from "vue-router";
|
||||||
|
|
||||||
|
import { axios, Scroller, 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-web/style.css";
|
||||||
|
import "timi-tdesign-pc/style.css";
|
||||||
|
|
||||||
|
console.log(`
|
||||||
|
______ __ _ _
|
||||||
|
/ __\\ \\ \\ \\ \\
|
||||||
|
/ . . \\ ' \\ \\ \\ \\
|
||||||
|
( ) imyeyu.com ) ) ) )
|
||||||
|
'\\ ___ /' / / / /
|
||||||
|
====='===='=====================/_/_/_/
|
||||||
|
`);
|
||||||
|
|
||||||
|
// ---------- 网络 ----------
|
||||||
|
axios.defaults.baseURL = import.meta.env.VITE_API;
|
||||||
|
// ---------- 路由 ----------
|
||||||
|
router.beforeEach(async (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext): Promise<void> => {
|
||||||
|
await userStore.login4Storage();
|
||||||
|
if (to.matched.length !== 0) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (from.name) {
|
||||||
|
next({name: from.name});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 找不到页面
|
||||||
|
const alert = DialogPlugin.alert({
|
||||||
|
header: "错误:404",
|
||||||
|
body: "找不到该页面",
|
||||||
|
confirmBtn: {
|
||||||
|
content: "关闭",
|
||||||
|
variant: "base",
|
||||||
|
theme: "default"
|
||||||
|
},
|
||||||
|
onConfirm: () => {
|
||||||
|
alert.destroy();
|
||||||
|
next("/");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
router.afterEach(() => {
|
||||||
|
if (!window.location.hash) {
|
||||||
|
Scroller.toTop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// ---------- Vue ----------
|
||||||
|
const app = createApp(Root);
|
||||||
|
app.use(router);
|
||||||
|
app.use(createPinia());
|
||||||
|
app.directive("draggable", VDraggable as any);
|
||||||
|
app.directive("popup", VPopup as any);
|
||||||
|
app.mount("#root");
|
||||||
30
src/router/index.ts
Normal file
30
src/router/index.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import IndexLayout from "@/layout/IndexLayout.vue";
|
||||||
|
import RepositoryList from "@/views/index/RepositoryList.vue";
|
||||||
|
import RepositoryLog from "@/views/index/RepositoryLog.vue";
|
||||||
|
import {createRouter, createWebHistory} from "vue-router";
|
||||||
|
import repository from "./repository";
|
||||||
|
|
||||||
|
export default createRouter({
|
||||||
|
history: createWebHistory("/"),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
// 主路由
|
||||||
|
path: "/",
|
||||||
|
name: "IndexLayout",
|
||||||
|
component: IndexLayout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
name: "RepositoryList",
|
||||||
|
component: RepositoryList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/log",
|
||||||
|
name: "RepositoryLog",
|
||||||
|
component: RepositoryLog
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
repository
|
||||||
|
]
|
||||||
|
});
|
||||||
71
src/router/repository.ts
Normal file
71
src/router/repository.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import RepositoryLayout from "@/layout/RepositoryLayout.vue";
|
||||||
|
import CommitLog from "@/views/repository/CommitLog.vue";
|
||||||
|
import FileDetail from "@/views/repository/FileDetail.vue";
|
||||||
|
import IssueDetail from "@/views/repository/IssueDetail.vue";
|
||||||
|
import IssueEdit from "@/views/repository/IssueEdit.vue";
|
||||||
|
import IssueList from "@/views/repository/IssueList.vue";
|
||||||
|
import MergeDetail from "@/views/repository/MergeDetail.vue";
|
||||||
|
import MergeEdit from "@/views/repository/MergeEdit.vue";
|
||||||
|
import MergeList from "@/views/repository/MergeList.vue";
|
||||||
|
import ReleaseList from "@/views/repository/ReleaseList.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
path: "/:repository",
|
||||||
|
name: "RepositoryLayout",
|
||||||
|
component: RepositoryLayout,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: ":branch",
|
||||||
|
name: "FileDetail",
|
||||||
|
component: FileDetail,
|
||||||
|
meta: {
|
||||||
|
keepAlive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "commits",
|
||||||
|
name: "CommitLog",
|
||||||
|
component: CommitLog
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "issues",
|
||||||
|
name: "IssueList",
|
||||||
|
component: IssueList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "issues",
|
||||||
|
name: "IssueList",
|
||||||
|
component: IssueList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "issues/edit/:id?",
|
||||||
|
name: "IssueEdit",
|
||||||
|
component: IssueEdit
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "issues/:id",
|
||||||
|
name: "IssueDetail",
|
||||||
|
component: IssueDetail
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "merges",
|
||||||
|
name: "MergeList",
|
||||||
|
component: MergeList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "merges/edit/:id?",
|
||||||
|
name: "MergeEdit",
|
||||||
|
component: MergeEdit
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "merges/:id",
|
||||||
|
name: "MergeDetail",
|
||||||
|
component: MergeDetail
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "releases",
|
||||||
|
name: "ReleaseList",
|
||||||
|
component: ReleaseList
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
15
src/store/repository.ts
Normal file
15
src/store/repository.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { RepositoryView } from "@/types/Repository.ts";
|
||||||
|
|
||||||
|
const store = defineStore("user", () => {
|
||||||
|
|
||||||
|
const value = ref<RepositoryView>();
|
||||||
|
|
||||||
|
return {
|
||||||
|
value
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useRepositoryStore() {
|
||||||
|
return store();
|
||||||
|
}
|
||||||
21
src/types/Common.ts
Normal file
21
src/types/Common.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { UserView } from "../../../timi-web";
|
||||||
|
|
||||||
|
export type ActionLogView = {
|
||||||
|
|
||||||
|
repoId: number;
|
||||||
|
repoName: string;
|
||||||
|
refName: string;
|
||||||
|
operatedAt: number;
|
||||||
|
|
||||||
|
operator: UserView;
|
||||||
|
commitList?: ActionLogCommit[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ActionLogCommit = {
|
||||||
|
|
||||||
|
sha: string;
|
||||||
|
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
committedAt: number;
|
||||||
|
}
|
||||||
49
src/types/Issue.ts
Normal file
49
src/types/Issue.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { Attachment, Model, Page as BasePage, UserView } from "timi-web";
|
||||||
|
|
||||||
|
export type Issue = {
|
||||||
|
repositoryId?: number;
|
||||||
|
publisherId?: number;
|
||||||
|
publisherNick?: string;
|
||||||
|
type?: Type;
|
||||||
|
version?: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
status?: Status;
|
||||||
|
confirmedAt?: number;
|
||||||
|
developAt?: number;
|
||||||
|
closedAt?: number;
|
||||||
|
} & Model;
|
||||||
|
|
||||||
|
export type IssueView = {
|
||||||
|
|
||||||
|
publisher?: UserView;
|
||||||
|
|
||||||
|
attachmentList: Attachment[]
|
||||||
|
} & Issue;
|
||||||
|
|
||||||
|
export enum Type {
|
||||||
|
BUG = "异常",
|
||||||
|
FEATURE = "功能",
|
||||||
|
SECURITY = "安全",
|
||||||
|
QUESTION = "问题",
|
||||||
|
DISCUSS = "讨论",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Status {
|
||||||
|
BEFORE_CONFIRM = "待确认",
|
||||||
|
CONFIRMED = "已确认",
|
||||||
|
DEVELOPING = "开发中",
|
||||||
|
FINISHED = "已完成",
|
||||||
|
CLOSED = "已关闭",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Page = {
|
||||||
|
repositoryId?: number;
|
||||||
|
keyword?: string;
|
||||||
|
type?: Type;
|
||||||
|
status?: Status;
|
||||||
|
} & BasePage;
|
||||||
|
|
||||||
|
export type CommentPage = {
|
||||||
|
issueId?: number;
|
||||||
|
} & BasePage;
|
||||||
52
src/types/Merge.ts
Normal file
52
src/types/Merge.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { IssueView } from "./Issue";
|
||||||
|
import { Attachment, Model, Page as BasePage, UserView } from "timi-web";
|
||||||
|
|
||||||
|
export type Merge = {
|
||||||
|
repositoryId?: number;
|
||||||
|
requesterId?: number;
|
||||||
|
issueId?: number;
|
||||||
|
type?: Type;
|
||||||
|
fromBranch?: string;
|
||||||
|
toBranch: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
checkedAt?: number;
|
||||||
|
mergedAt?: number;
|
||||||
|
rejectedAt?: number;
|
||||||
|
rejectReason?: string;
|
||||||
|
closedAt?: number;
|
||||||
|
status?: Status;
|
||||||
|
} & Model;
|
||||||
|
|
||||||
|
export type MergeView = {
|
||||||
|
requester?: UserView;
|
||||||
|
issue?: IssueView;
|
||||||
|
attachmentList: Attachment[]
|
||||||
|
} & Merge;
|
||||||
|
|
||||||
|
export enum Type {
|
||||||
|
BUG = "异常",
|
||||||
|
FEATURE = "功能",
|
||||||
|
SECURITY = "安全",
|
||||||
|
DOCUMENT = "文档",
|
||||||
|
REFACTOR = "重构",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Status {
|
||||||
|
BEFORE_CHECK = "待审查",
|
||||||
|
WAITING = "待合并",
|
||||||
|
MERGED = "已合并",
|
||||||
|
REJECTED = "已拒绝",
|
||||||
|
CLOSED = "已关闭",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Page = {
|
||||||
|
repositoryId?: number;
|
||||||
|
keyword?: string;
|
||||||
|
type?: Type;
|
||||||
|
status?: Status;
|
||||||
|
} & BasePage;
|
||||||
|
|
||||||
|
export type CommentPage = {
|
||||||
|
mergeId?: number;
|
||||||
|
} & BasePage;
|
||||||
15
src/types/Release.ts
Normal file
15
src/types/Release.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { AttachmentView, Model, Page as BasePage } from "timi-web";
|
||||||
|
|
||||||
|
export type Release = {
|
||||||
|
repositoryId: number;
|
||||||
|
version: string;
|
||||||
|
description: string;
|
||||||
|
sha1: string;
|
||||||
|
commits: number;
|
||||||
|
|
||||||
|
attachmentList: AttachmentView[];
|
||||||
|
} & Model;
|
||||||
|
|
||||||
|
export type Page = {
|
||||||
|
repositoryId?: number;
|
||||||
|
} & BasePage;
|
||||||
63
src/types/Repository.ts
Normal file
63
src/types/Repository.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { Model, Page, UserView } from "timi-web";
|
||||||
|
|
||||||
|
export type Repository = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
size: number;
|
||||||
|
language: string;
|
||||||
|
sshUrl: string;
|
||||||
|
cloneUrl: string;
|
||||||
|
defaultBranch: string;
|
||||||
|
archived: boolean;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
archiveAt: string;
|
||||||
|
license?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RepositoryView = {
|
||||||
|
branchList: Branch[];
|
||||||
|
} & Repository;
|
||||||
|
|
||||||
|
export type Branch = {
|
||||||
|
name: string;
|
||||||
|
protected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Commit = {
|
||||||
|
id: string;
|
||||||
|
msg: string;
|
||||||
|
time: number;
|
||||||
|
|
||||||
|
committer: UserView;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CommitLog = {
|
||||||
|
pushId: number;
|
||||||
|
sha1: string;
|
||||||
|
message: string;
|
||||||
|
} & Model;
|
||||||
|
|
||||||
|
export type CommitPage = {
|
||||||
|
repositoryId?: number;
|
||||||
|
branch?: string;
|
||||||
|
} & Page;
|
||||||
|
|
||||||
|
export enum FileType {
|
||||||
|
|
||||||
|
FILE = "file",
|
||||||
|
DIRECTORY = "dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type File = {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
type: FileType
|
||||||
|
sha: string;
|
||||||
|
size: number;
|
||||||
|
lastCommitSha: string;
|
||||||
|
lastCommitterDate: number;
|
||||||
|
|
||||||
|
children: boolean;
|
||||||
|
}
|
||||||
117
src/views/index/RepositoryList.vue
Normal file
117
src/views/index/RepositoryList.vue
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<div class="repository-list">
|
||||||
|
<t-list class="items" v-if="page" :split="true">
|
||||||
|
<t-list-item class="item" v-for="item in pageResult.list" :key="item.id">
|
||||||
|
<t-layout>
|
||||||
|
<t-aside class="icon" width="auto">
|
||||||
|
<icon name="DUPLICATE" :scale="2" />
|
||||||
|
</t-aside>
|
||||||
|
<t-content class="content">
|
||||||
|
<t-layout>
|
||||||
|
<t-header class="header" height="auto">
|
||||||
|
<div class="name">
|
||||||
|
<router-link class="link black" :to="`/${item.name}/${item.defaultBranch}`">
|
||||||
|
{{ item.name }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="time light-gray"
|
||||||
|
v-text="`最近推送 ${Time.toPassedDate(item.updatedAt)}`"
|
||||||
|
v-popup="Time.toDateTime(item.updatedAt)"
|
||||||
|
></div>
|
||||||
|
</t-header>
|
||||||
|
<t-content>
|
||||||
|
<span class="gray" v-text="item.description"></span>
|
||||||
|
</t-content>
|
||||||
|
</t-layout>
|
||||||
|
</t-content>
|
||||||
|
</t-layout>
|
||||||
|
</t-list-item>
|
||||||
|
</t-list>
|
||||||
|
<footer class="footer">
|
||||||
|
<t-pagination
|
||||||
|
class="pagination"
|
||||||
|
:total="pageResult.total"
|
||||||
|
:pageSize="16"
|
||||||
|
:showPageSize="false"
|
||||||
|
>
|
||||||
|
<template #totalContent>
|
||||||
|
<div class="total" v-text="`共 ${pageResult.total} 个项目`"></div>
|
||||||
|
</template>
|
||||||
|
</t-pagination>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import RepositoryAPI from "@/api/RepositoryAPI";
|
||||||
|
import { Repository } from "@/types/Repository";
|
||||||
|
import { Icon, Page, PageResult, Time } from "timi-web";
|
||||||
|
|
||||||
|
const page = ref<Page>({
|
||||||
|
index: 0,
|
||||||
|
size: 12
|
||||||
|
});
|
||||||
|
const pageResult = reactive<PageResult<Repository>>({
|
||||||
|
total: 0,
|
||||||
|
list: []
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const result = await RepositoryAPI.page(page.value);
|
||||||
|
pageResult.total = result.total;
|
||||||
|
pageResult.list = result.list;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.repository-list {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.items {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 520px;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
padding: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding-left: .5rem;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
flex: 1;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.link {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
bottom: -1px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
position: sticky;
|
||||||
|
background: rgba(255, 255, 255, .8);
|
||||||
|
border-top: var(--tui-border);
|
||||||
|
border-bottom: var(--tui-border);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
|
||||||
|
.total {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
44
src/views/index/RepositoryLog.vue
Normal file
44
src/views/index/RepositoryLog.vue
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<div class="repository-log">
|
||||||
|
<push-log-timeline :items="list"/>
|
||||||
|
<div class="bottom">
|
||||||
|
<loading :showOn="isLoading"/>
|
||||||
|
<empty-tips :showOn="isFinished && !isLoading"/>
|
||||||
|
<t-button v-show="!isFinished && !isLoading" @click="doFetchEvent">加载更多</t-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import RepositoryAPI from "@/api/RepositoryAPI";
|
||||||
|
import { EmptyTips, Loading, Page } from "timi-web";
|
||||||
|
import { ActionLogView } from "@/types/Common.ts";
|
||||||
|
import PushLogTimeline from "@/components/PushLogTimeline.vue";
|
||||||
|
|
||||||
|
const page = ref<Page>({
|
||||||
|
index: 0,
|
||||||
|
size: 12
|
||||||
|
});
|
||||||
|
const list = reactive<ActionLogView[]>([]);
|
||||||
|
const isFinished = ref(false);
|
||||||
|
const isLoading = ref(true);
|
||||||
|
|
||||||
|
const doFetchEvent = async () => {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
const result = await RepositoryAPI.pageLog(page.value);
|
||||||
|
list.push(...result.list);
|
||||||
|
page.value.index++;
|
||||||
|
if (result.list.length < page.value.size) {
|
||||||
|
isFinished.value = true;
|
||||||
|
}
|
||||||
|
isLoading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(doFetchEvent);
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.repository-log {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
90
src/views/repository/CommitLog.vue
Normal file
90
src/views/repository/CommitLog.vue
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="repositoryStore.value" class="commit-log">
|
||||||
|
<t-layout>
|
||||||
|
<t-header class="header">
|
||||||
|
<div>
|
||||||
|
<t-select class="branch" v-model="branch" label="分支:" placeholder="全部" clearable>
|
||||||
|
<t-option
|
||||||
|
v-for="item in repositoryStore.value.branchList"
|
||||||
|
:key="item.name"
|
||||||
|
:value="item.name"
|
||||||
|
:label="item.name"
|
||||||
|
/>
|
||||||
|
</t-select>
|
||||||
|
</div>
|
||||||
|
</t-header>
|
||||||
|
<t-content class="content">
|
||||||
|
<push-log-timeline :items="commits" :show-repo="false" :show-branch="!branch" />
|
||||||
|
<div class="more">
|
||||||
|
<empty-tips :showOn="isFinished" />
|
||||||
|
<t-button v-if="!isFinished" @click="doFetchEvent">加载更多</t-button>
|
||||||
|
</div>
|
||||||
|
</t-content>
|
||||||
|
</t-layout>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { EmptyTips, Page } from "timi-web";
|
||||||
|
import RepositoryAPI from "@/api/RepositoryAPI";
|
||||||
|
import { useRepositoryStore } from "@/store/repository.ts";
|
||||||
|
import { ActionLogView } from "@/types/Common.ts";
|
||||||
|
import PushLogTimeline from "@/components/PushLogTimeline.vue";
|
||||||
|
|
||||||
|
const repositoryStore = useRepositoryStore();
|
||||||
|
|
||||||
|
const branch = ref();
|
||||||
|
|
||||||
|
const page = ref<Page>({
|
||||||
|
index: 0,
|
||||||
|
size: 12
|
||||||
|
});
|
||||||
|
const commits = reactive<ActionLogView[]>([]);
|
||||||
|
const isFinished = ref(false);
|
||||||
|
|
||||||
|
const doFetchEvent = async () => {
|
||||||
|
if (repositoryStore.value) {
|
||||||
|
const result = await RepositoryAPI.pagePush(repositoryStore.value.name, branch.value ?? "all", page.value);
|
||||||
|
const list = result.list;
|
||||||
|
commits.push(...list);
|
||||||
|
page.value.index++;
|
||||||
|
if (result.list.length < page.value.size) {
|
||||||
|
isFinished.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
watch(branch, () => {
|
||||||
|
if (repositoryStore.value) {
|
||||||
|
page.value.index = 0;
|
||||||
|
commits.length = 0;
|
||||||
|
isFinished.value = false;
|
||||||
|
doFetchEvent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onMounted(doFetchEvent);
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.commit-log {
|
||||||
|
|
||||||
|
.header {
|
||||||
|
top: calc(50px + 49px);
|
||||||
|
padding: 0 20px;
|
||||||
|
display: flex;
|
||||||
|
z-index: 3;
|
||||||
|
position: sticky;
|
||||||
|
background: rgba(231, 234, 239, .8);
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: var(--tui-border);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 1rem 2rem 1rem 1rem;
|
||||||
|
|
||||||
|
.more {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
468
src/views/repository/FileDetail.vue
Normal file
468
src/views/repository/FileDetail.vue
Normal file
@ -0,0 +1,468 @@
|
|||||||
|
<template>
|
||||||
|
<div class="file-detail" v-if="repositoryStore.value && repositoryStore.value.branchList">
|
||||||
|
<div class="left">
|
||||||
|
<t-select class="branch" v-model="branch" label="分支:" :borderless="true">
|
||||||
|
<t-option
|
||||||
|
v-for="item in repositoryStore.value.branchList"
|
||||||
|
:key="item.name"
|
||||||
|
:value="item.name"
|
||||||
|
:label="item.name"
|
||||||
|
></t-option>
|
||||||
|
</t-select>
|
||||||
|
<div class="tree-container">
|
||||||
|
<t-tree
|
||||||
|
ref="tree"
|
||||||
|
class="tree"
|
||||||
|
:data="treeItems"
|
||||||
|
:keys="treeMap"
|
||||||
|
:line="true"
|
||||||
|
:activable="true"
|
||||||
|
:transition="false"
|
||||||
|
:active-multiple="false"
|
||||||
|
:expand-on-click-node="true"
|
||||||
|
:expand-parent="true"
|
||||||
|
value-mode="all"
|
||||||
|
expand-all
|
||||||
|
:load="treeLoad"
|
||||||
|
:onActive="onTreeActivated"
|
||||||
|
>
|
||||||
|
<template #icon="{ node }">
|
||||||
|
<icon name="FILE" fill="GRAY" v-if="node.data.type === FileType.FILE" />
|
||||||
|
<icon name="FOLDER" v-if="node.data.type === FileType.DIRECTORY" />
|
||||||
|
</template>
|
||||||
|
</t-tree>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<div class="header">
|
||||||
|
<div class="clone-label">克隆地址</div>
|
||||||
|
<t-select class="clone-type" v-model="cloneType" borderless>
|
||||||
|
<t-option
|
||||||
|
v-for="item in CloneType"
|
||||||
|
:key="item"
|
||||||
|
:value="item"
|
||||||
|
:label="item"
|
||||||
|
></t-option>
|
||||||
|
</t-select>
|
||||||
|
<t-input
|
||||||
|
v-if="branch"
|
||||||
|
class="address"
|
||||||
|
:value="cloneAddr"
|
||||||
|
placeholder=""
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
<t-button theme="success" @click="doCopyCloneAddr">复制</t-button>
|
||||||
|
<t-button @click="RepositoryAPI.downloadArchive(repositoryStore.value.name, branch)">下载源码</t-button>
|
||||||
|
</div>
|
||||||
|
<t-tabs
|
||||||
|
class="tabs"
|
||||||
|
theme="card"
|
||||||
|
v-model="tabActivated"
|
||||||
|
:onRemove="onTabRemove"
|
||||||
|
>
|
||||||
|
<t-tab-panel
|
||||||
|
v-for="file in tabFiles"
|
||||||
|
:key="file.path"
|
||||||
|
:value="file.path"
|
||||||
|
:label="file.name"
|
||||||
|
:removable="file.removable"
|
||||||
|
@click="onClickOpenFile(file)"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span
|
||||||
|
v-text="file.name"
|
||||||
|
@click="onClickOpenFile(file)"
|
||||||
|
></span>
|
||||||
|
</template>
|
||||||
|
</t-tab-panel>
|
||||||
|
</t-tabs>
|
||||||
|
<div v-if="tabActivatedFileViewer"
|
||||||
|
:class="`viewer ${viewerClass}`"
|
||||||
|
@click="onClickViewer"
|
||||||
|
>
|
||||||
|
<markdown-view
|
||||||
|
v-if="tabActivatedFile && tabActivatedFileViewer !== PrismjsViewer.TEXT"
|
||||||
|
class="content selectable"
|
||||||
|
:show-code-border="false"
|
||||||
|
max-height="auto"
|
||||||
|
:content="tabActivatedFile.data"
|
||||||
|
@click="onClickViewer"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
v-if="tabActivatedFile && tabActivatedFileViewer === PrismjsViewer.TEXT"
|
||||||
|
class="content selectable"
|
||||||
|
:value="tabActivatedFile.data"
|
||||||
|
readonly
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div v-else class="viewer not-support">
|
||||||
|
<div class="content">不支持预览</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Icon, MarkdownView, Prismjs, PrismjsType, PrismjsViewer, Toolkit } from "timi-web";
|
||||||
|
import type { TreeInstanceFunctions, TreeProps } from "tdesign-vue-next";
|
||||||
|
import RepositoryAPI from "@/api/RepositoryAPI";
|
||||||
|
import { File, FileType } from "@/types/Repository";
|
||||||
|
import { useRepositoryStore } from "@/store/repository.ts";
|
||||||
|
import useClipboard from "vue-clipboard3";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { toClipboard } = useClipboard();
|
||||||
|
const repositoryStore = useRepositoryStore();
|
||||||
|
|
||||||
|
// ---------- 分支 ----------
|
||||||
|
const branch = ref();
|
||||||
|
onMounted(async () => {
|
||||||
|
if (repositoryStore.value && repositoryStore.value.defaultBranch) {
|
||||||
|
branch.value = repositoryStore.value.defaultBranch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------- 文件树 ----------
|
||||||
|
const tree = ref<TreeInstanceFunctions>();
|
||||||
|
const treeItems = ref<File[]>([]);
|
||||||
|
const treeMap = {
|
||||||
|
value: "path",
|
||||||
|
label: "name"
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(branch, async () => {
|
||||||
|
if (!repositoryStore.value || !branch.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
treeItems.value = await RepositoryAPI.listFile(repositoryStore.value!.name, branch.value, "/");
|
||||||
|
treeItems.value.forEach((item) => item.children = item.type === FileType.DIRECTORY);
|
||||||
|
await nextTick();
|
||||||
|
// 文件树
|
||||||
|
if (tree.value) {
|
||||||
|
const items = treeItems.value;
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.path === "README.md") {
|
||||||
|
await doOpenFile(tree.value.getItem(item.path).data as File);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const treeLoad: TreeProps["load"] = node => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Toolkit.async(async () => {
|
||||||
|
const file = node.data;
|
||||||
|
const result = await RepositoryAPI.listFile(
|
||||||
|
repositoryStore.value!.name,
|
||||||
|
branch.value,
|
||||||
|
`/${file.path}`
|
||||||
|
);
|
||||||
|
result.forEach((item) => item.children = item.type === FileType.DIRECTORY);
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------- 克隆地址 ----------
|
||||||
|
|
||||||
|
enum CloneType {
|
||||||
|
SSH = "SSH",
|
||||||
|
HTTP = "HTTP"
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloneType = ref(CloneType.HTTP);
|
||||||
|
const cloneAddr = computed(() => cloneType.value === CloneType.HTTP ? repositoryStore.value!.cloneUrl : repositoryStore.value!.sshUrl);
|
||||||
|
|
||||||
|
async function doCopyCloneAddr() {
|
||||||
|
try {
|
||||||
|
await toClipboard(cloneAddr.value);
|
||||||
|
await MessagePlugin.success("复制成功");
|
||||||
|
} catch (e) {
|
||||||
|
await MessagePlugin.success("复制失败");
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 打开文件 ----------
|
||||||
|
|
||||||
|
type OpenFile = {
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
data?: string;
|
||||||
|
removable: boolean;
|
||||||
|
isPreview: boolean;
|
||||||
|
activatedAt?: number;
|
||||||
|
prismjsType?: PrismjsType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabFiles = ref<OpenFile[]>([]);
|
||||||
|
const tabActivated = ref();
|
||||||
|
const tabActivatedFile = ref<OpenFile>();
|
||||||
|
const tabActivatedFileViewer = ref<PrismjsViewer>();
|
||||||
|
|
||||||
|
const viewerClass = computed(() => {
|
||||||
|
if (tabActivatedFileViewer.value) {
|
||||||
|
return Toolkit.className(tabActivatedFileViewer.value.toString().toLowerCase());
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
watch(branch, async () => {
|
||||||
|
if (!branch.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await router.replace(`/${repositoryStore.value!.name}/${branch.value}`);
|
||||||
|
tabFiles.value = [];
|
||||||
|
tabActivated.value = null;
|
||||||
|
});
|
||||||
|
watch(tabActivated, value => {
|
||||||
|
if (tabActivated.value) {
|
||||||
|
const targetFile = tabFiles.value.find(item => item.path === value);
|
||||||
|
if (targetFile) {
|
||||||
|
targetFile.activatedAt = new Date().getTime();
|
||||||
|
tabActivatedFile.value = targetFile;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tabActivatedFile.value = undefined;
|
||||||
|
});
|
||||||
|
watch(tabActivatedFile, () => {
|
||||||
|
if (tabActivatedFile.value && tabActivatedFile.value.prismjsType) {
|
||||||
|
const properties = Prismjs.getFileProperties(tabActivatedFile.value.prismjsType);
|
||||||
|
if (properties) {
|
||||||
|
tabActivatedFileViewer.value = properties.viewer;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tabActivatedFileViewer.value = undefined;
|
||||||
|
});
|
||||||
|
const doOpenFile = async (file: File) => {
|
||||||
|
if (file.type === FileType.FILE) {
|
||||||
|
let opened = tabFiles.value.find(item => item.path === file.path);
|
||||||
|
const previewIndex = tabFiles.value.findIndex(item => item.isPreview);
|
||||||
|
const isReadme = file.path === "README.md";
|
||||||
|
if (!opened) {
|
||||||
|
opened = {
|
||||||
|
path: file.path,
|
||||||
|
name: file.name,
|
||||||
|
isPreview: !isReadme,
|
||||||
|
removable: !isReadme,
|
||||||
|
prismjsType: PrismjsType.PlainText
|
||||||
|
};
|
||||||
|
if (previewIndex === -1) {
|
||||||
|
tabFiles.value.push(opened);
|
||||||
|
} else {
|
||||||
|
// 如果存在已打开预览,则替换该预览项
|
||||||
|
tabFiles.value.splice(previewIndex, 1, opened);
|
||||||
|
}
|
||||||
|
const raw = await RepositoryAPI.fileRaw(repositoryStore.value!.name, branch.value, `/${opened.path}`);
|
||||||
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
opened.prismjsType = Prismjs.typeFromFileName(opened.name);
|
||||||
|
if (opened.prismjsType) {
|
||||||
|
const properties = Prismjs.getFileProperties(opened.prismjsType);
|
||||||
|
if (properties && properties.viewer === PrismjsViewer.CODE) {
|
||||||
|
opened.data = "```" + properties.prismjs + "\n" + decoder.decode(raw) + "\n```";
|
||||||
|
} else {
|
||||||
|
opened.data = decoder.decode(raw);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
opened.data = "该文件暂不支持预览";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opened.activatedAt = new Date().getTime();
|
||||||
|
tabActivated.value = file.path;
|
||||||
|
tabActivatedFile.value = opened;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO 明确类型会构建失败
|
||||||
|
const onTreeActivated = (_value: any, context: { node: any }) => {
|
||||||
|
doOpenFile(context.node.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTabRemove = (options: { index: number }) => {
|
||||||
|
tabFiles.value.splice(options.index, 1);
|
||||||
|
if (0 < tabFiles.value.length) {
|
||||||
|
tabActivated.value = tabFiles.value[tabFiles.value.length - 1].path;
|
||||||
|
} else {
|
||||||
|
tabActivated.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickOpenFile = (openFile: OpenFile) => {
|
||||||
|
openFile.isPreview = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickViewer = async () => {
|
||||||
|
const activated = tabFiles.value.find(item => item.path === tabActivated.value);
|
||||||
|
if (activated) {
|
||||||
|
activated.isPreview = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.file-detail {
|
||||||
|
// 100% 可视高度 - 上下外边距 - 边框 - 顶部 - 菜单 - 底部
|
||||||
|
height: calc(100vh - 128px * 2 - 2px - 50px - 50px - 94px);
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.left {
|
||||||
|
width: 16rem;
|
||||||
|
z-index: 4;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.branch {
|
||||||
|
border-bottom: var(--tui-border);
|
||||||
|
|
||||||
|
:deep(.t-select-input--borderless) {
|
||||||
|
|
||||||
|
.t-input {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
background: #FFF;
|
||||||
|
|
||||||
|
&:hover:not(.t-input--focused) {
|
||||||
|
background: var(--td-bg-color-container-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.t-input--focused {
|
||||||
|
box-shadow: 0 0 0 2px var(--td-brand-color-focus);
|
||||||
|
background: var(--td-bg-color-specialcomponent);
|
||||||
|
border-color: var(--td-brand-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-container {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
.tree {
|
||||||
|
|
||||||
|
&:deep(.t-tree__line) {
|
||||||
|
--color: #333;
|
||||||
|
margin-left: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:deep(.t-tree__icon) {
|
||||||
|
margin-left: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:deep(.t-tree__item.t-is-active) {
|
||||||
|
background: var(--td-brand-color-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
width: calc(100% - 16rem);
|
||||||
|
display: flex;
|
||||||
|
border-left: var(--tui-border);
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
top: calc(50px + 49px);
|
||||||
|
z-index: 4;
|
||||||
|
display: flex;
|
||||||
|
position: sticky;
|
||||||
|
|
||||||
|
.clone-label {
|
||||||
|
display: flex;
|
||||||
|
padding: 0 10px;
|
||||||
|
background: var(--td-bg-color-secondarycontainer);
|
||||||
|
line-height: 30px;
|
||||||
|
border-right: var(--tui-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clone-type {
|
||||||
|
width: 6rem;
|
||||||
|
border-right: var(--tui-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.address {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
:deep(.t-input) {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-input:hover),
|
||||||
|
:deep(.t-input--focused) {
|
||||||
|
border-color: var(--td-brand-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
border-top: var(--tui-border);
|
||||||
|
|
||||||
|
:deep(.t-tabs__nav-container.t-tabs__nav--card::after) {
|
||||||
|
background: var(--tui-light-gray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
&.text {
|
||||||
|
|
||||||
|
.content {
|
||||||
|
width: calc(100% - 8px);
|
||||||
|
height: calc(100% - 4px);
|
||||||
|
border: none;
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.code {
|
||||||
|
|
||||||
|
:deep(.tui-markdown-view) {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 3em;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(242, 242, 242, .9);
|
||||||
|
border-right: 1px solid #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre[class^="language-"] {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.markdown {
|
||||||
|
height: 100%;
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.not-support {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(pre[class*="language-"]) {
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1200px) {
|
||||||
|
.file-detail {
|
||||||
|
height: calc(100vh - 50px - 50px - 94px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
166
src/views/repository/IssueDetail.vue
Normal file
166
src/views/repository/IssueDetail.vue
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
<template>
|
||||||
|
<div class="issue-detail" v-if="issue && repositoryStore.value">
|
||||||
|
<div class="header">
|
||||||
|
<t-breadcrumb max-item-width="9999">
|
||||||
|
<t-breadcrumb-item>
|
||||||
|
<router-link :to="`/${repositoryStore.value.name}/issues`">反馈列表</router-link>
|
||||||
|
</t-breadcrumb-item>
|
||||||
|
<t-breadcrumb-item>
|
||||||
|
<h3 class="title" v-text="issue.title"></h3>
|
||||||
|
</t-breadcrumb-item>
|
||||||
|
</t-breadcrumb>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="description">
|
||||||
|
<markdown-view :content="issue.description" />
|
||||||
|
<div class="edit" v-if="canEdit">
|
||||||
|
<t-button @click="router.push(`/${repositoryStore.value.name}/issues/edit/${issue.id}`)">
|
||||||
|
修改问题反馈
|
||||||
|
</t-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<t-list>
|
||||||
|
<t-list-item>
|
||||||
|
<t-list-item-meta title="编号">
|
||||||
|
<template #description>
|
||||||
|
<span>#</span>
|
||||||
|
<span v-text="issue.id"></span>
|
||||||
|
</template>
|
||||||
|
</t-list-item-meta>
|
||||||
|
</t-list-item>
|
||||||
|
<t-list-item>
|
||||||
|
<t-list-item-meta title="用户" :description="issue.publisherNick">
|
||||||
|
<template #description>
|
||||||
|
<t-space size="6px">
|
||||||
|
<template v-if="issue.publisher">
|
||||||
|
<t-avatar
|
||||||
|
:class="(<any>ImageType)[issue.publisher.profile.avatarType]"
|
||||||
|
:image="UserAPI.getAvatarURL(issue.publisher.profile)"
|
||||||
|
size="small"
|
||||||
|
shape="round"
|
||||||
|
/>
|
||||||
|
<span v-text="issue.publisher.name"></span>
|
||||||
|
</template>
|
||||||
|
<span v-else v-text="issue.publisherNick"></span>
|
||||||
|
</t-space>
|
||||||
|
</template>
|
||||||
|
</t-list-item-meta>
|
||||||
|
</t-list-item>
|
||||||
|
<t-list-item>
|
||||||
|
<t-list-item-meta title="版本" :description="issue.version" />
|
||||||
|
</t-list-item>
|
||||||
|
<t-list-item v-if="issue.type">
|
||||||
|
<t-list-item-meta title="类型" :description="(<any>Type)[issue.type]" />
|
||||||
|
</t-list-item>
|
||||||
|
<t-list-item v-if="issue.status">
|
||||||
|
<t-list-item-meta title="状态" :description="(<any>Status)[issue.status]" />
|
||||||
|
</t-list-item>
|
||||||
|
<t-list-item v-if="issue.closedAt">
|
||||||
|
<t-list-item-meta title="关闭时间" :description="Time.toPassedDateTime(issue.closedAt)" />
|
||||||
|
</t-list-item>
|
||||||
|
</t-list>
|
||||||
|
</div>
|
||||||
|
<timeline-comment
|
||||||
|
v-if="!!issue.id"
|
||||||
|
:biz-type="CommentBizType.GIT_ISSUE"
|
||||||
|
:bizId="issue.id"
|
||||||
|
:canCreate="issue.status !== <Status>Toolkit.keyFromValue(Status, Status.CLOSED)"
|
||||||
|
:systemComments="commentTimelineSystems"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import IssueAPI from "@/api/IssueAPI";
|
||||||
|
import { MarkdownView, Time, Toolkit, userStore } from "timi-web";
|
||||||
|
import { CommentBizType, ImageType, UserAPI } from "timi-web";
|
||||||
|
import TimelineComment, { SystemComment } from "@/components/TimelineComment.vue";
|
||||||
|
import { IssueView, Status, Type } from "@/types/Issue";
|
||||||
|
import { useRepositoryStore } from "@/store/repository.ts";
|
||||||
|
|
||||||
|
// 引用数据
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const repositoryStore = useRepositoryStore();
|
||||||
|
|
||||||
|
// 基本数据
|
||||||
|
const issue = ref<IssueView>();
|
||||||
|
const commentTimelineSystems = ref<SystemComment[]>([]);
|
||||||
|
|
||||||
|
const canEdit = computed(() => {
|
||||||
|
const _issue = issue.value;
|
||||||
|
if (repositoryStore.value && _issue && _issue.id && userStore.loginUser) {
|
||||||
|
const profile = userStore.loginUser.user?.profile;
|
||||||
|
if (profile && profile.userId === _issue.publisherId) {
|
||||||
|
const typeCondition = _issue.type !== <Type>Toolkit.keyFromValue(Type, Type.DISCUSS);
|
||||||
|
const statusCondition = _issue.status !== <Status>Toolkit.keyFromValue(Status, Status.FINISHED);
|
||||||
|
return typeCondition && statusCondition && !_issue.closedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const issueId = route.params.id;
|
||||||
|
if (issueId) {
|
||||||
|
issue.value = await IssueAPI.view(issueId as any as number);
|
||||||
|
if (issue.value.confirmedAt) {
|
||||||
|
commentTimelineSystems.value.push({
|
||||||
|
inserted: false,
|
||||||
|
time: issue.value.confirmedAt,
|
||||||
|
msg: "开发者已确认,等待开发排期"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (issue.value.developAt) {
|
||||||
|
commentTimelineSystems.value.push({
|
||||||
|
colorClass: "bg-pink",
|
||||||
|
inserted: false,
|
||||||
|
time: issue.value.developAt,
|
||||||
|
msg: "正在开发.."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.issue-detail {
|
||||||
|
|
||||||
|
.header {
|
||||||
|
top: calc(50px + 49px);
|
||||||
|
margin: 0;
|
||||||
|
z-index: 3;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
position: sticky;
|
||||||
|
background: rgba(231, 234, 239, .8);
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 1rem;
|
||||||
|
border-bottom: var(--tui-border);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 80% 20%;
|
||||||
|
|
||||||
|
.description {
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-self: stretch;
|
||||||
|
border-right: var(--tui-border);
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.edit {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 2rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
192
src/views/repository/IssueEdit.vue
Normal file
192
src/views/repository/IssueEdit.vue
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
<template>
|
||||||
|
<t-layout v-if="repositoryStore.value" class="issue-edit">
|
||||||
|
<t-form class="form" :data="issue" @submit="doSubmit">
|
||||||
|
<t-header class="header shadow-down">
|
||||||
|
<t-breadcrumb>
|
||||||
|
<t-breadcrumb-item>
|
||||||
|
<router-link :to="`/${repositoryStore.value.name}/issues`">问题反馈列表</router-link>
|
||||||
|
</t-breadcrumb-item>
|
||||||
|
<t-breadcrumb-item v-if="issue.id">
|
||||||
|
<span v-text="issue.title"></span>
|
||||||
|
</t-breadcrumb-item>
|
||||||
|
<t-breadcrumb-item>
|
||||||
|
<span v-if="issue.id">编辑问题反馈</span>
|
||||||
|
<span v-else>新建反馈</span>
|
||||||
|
</t-breadcrumb-item>
|
||||||
|
</t-breadcrumb>
|
||||||
|
</t-header>
|
||||||
|
<t-content class="content">
|
||||||
|
<t-alert v-if="!userStore.isLogged()" class="login-tips" theme="info">
|
||||||
|
<span>建议登录账号以便修改已提交的反馈和接收邮件通知,</span>
|
||||||
|
<span class="pink">未登录账号提价后不可修改</span>
|
||||||
|
</t-alert>
|
||||||
|
<t-form-item label="昵称" name="name">
|
||||||
|
<t-input
|
||||||
|
v-if="userStore.isLogged()"
|
||||||
|
class="name"
|
||||||
|
:value="userStore.loginUser?.user?.name"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<t-input v-else class="name" v-model="issue.publisherNick" placeholder="" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="标题" name="title">
|
||||||
|
<t-input v-model="issue.title" placeholder="" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="类型" name="type">
|
||||||
|
<t-select
|
||||||
|
class="type"
|
||||||
|
v-model="issue.type"
|
||||||
|
:defaultValue="Type.BUG"
|
||||||
|
>
|
||||||
|
<t-option v-for="(value, key) in Type" :key="key" :value="key" :label="value" />
|
||||||
|
</t-select>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="版本" name="version">
|
||||||
|
<t-input class="version" v-model="issue.version" placeholder="" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="描述" name="description">
|
||||||
|
<markdown-editor v-model:data="issue.description" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item></t-form-item><!-- 用于保持边距 -->
|
||||||
|
</t-content>
|
||||||
|
<t-footer class="footer shadow-up">
|
||||||
|
<t-form-item v-if="!issue.id" label="验证码" name="captcha">
|
||||||
|
<t-input-adornment>
|
||||||
|
<t-input class="captcha" v-model="captchaValue" placeholder="" />
|
||||||
|
<template #append>
|
||||||
|
<captcha
|
||||||
|
:api="CommonAPI.getCaptchaAPI()"
|
||||||
|
:width="90"
|
||||||
|
:height="28"
|
||||||
|
:from="CaptchaFrom.GIT_ISSUE"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</t-input-adornment>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item>
|
||||||
|
<t-space>
|
||||||
|
<t-button type="submit">提交</t-button>
|
||||||
|
<t-button
|
||||||
|
v-if="issue.id"
|
||||||
|
@click="router.back()"
|
||||||
|
theme="default"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</t-button>
|
||||||
|
</t-space>
|
||||||
|
</t-form-item>
|
||||||
|
</t-footer>
|
||||||
|
</t-form>
|
||||||
|
</t-layout>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { CaptchaFrom, CommonAPI, userStore } from "timi-web";
|
||||||
|
import { Captcha, MarkdownEditor } from "timi-web";
|
||||||
|
import { MessagePlugin } from "tdesign-vue-next";
|
||||||
|
|
||||||
|
import IssueAPI from "@/api/IssueAPI";
|
||||||
|
import { Issue, Type } from "@/types/Issue";
|
||||||
|
import { useRepositoryStore } from "@/store/repository.ts";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const repositoryStore = useRepositoryStore();
|
||||||
|
|
||||||
|
const issue = reactive<Issue>({
|
||||||
|
publisherNick: "",
|
||||||
|
type: undefined,
|
||||||
|
version: "",
|
||||||
|
title: "",
|
||||||
|
description: ""
|
||||||
|
});
|
||||||
|
const captchaValue = ref();
|
||||||
|
|
||||||
|
const doSubmit = () => {
|
||||||
|
if (issue.id) {
|
||||||
|
IssueAPI.update(issue).then(() => {
|
||||||
|
if (repositoryStore.value) {
|
||||||
|
router.push(`/${repositoryStore.value.name}/issues/${issue.id}`);
|
||||||
|
}
|
||||||
|
MessagePlugin.success("提交成功");
|
||||||
|
}).catch(msg => {
|
||||||
|
MessagePlugin.error("提交失败:" + msg);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
IssueAPI.create({
|
||||||
|
from: CaptchaFrom.GIT_ISSUE,
|
||||||
|
captcha: captchaValue.value,
|
||||||
|
data: issue
|
||||||
|
}).then(() => {
|
||||||
|
router.push({name: "IssueList"});
|
||||||
|
MessagePlugin.success("提交成功");
|
||||||
|
}).catch(msg => {
|
||||||
|
MessagePlugin.error("提交失败:" + msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (repositoryStore.value) {
|
||||||
|
issue.repositoryId = repositoryStore.value.id;
|
||||||
|
|
||||||
|
const issueId = route.params.id;
|
||||||
|
if (issueId) {
|
||||||
|
const result = await IssueAPI.view(issueId as any as number);
|
||||||
|
issue.id = result.id;
|
||||||
|
issue.title = result.title;
|
||||||
|
issue.type = result.type;
|
||||||
|
issue.version = result.version;
|
||||||
|
issue.description = result.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.form {
|
||||||
|
|
||||||
|
.header {
|
||||||
|
top: calc(50px + 49px);
|
||||||
|
margin: 0;
|
||||||
|
height: 32px;
|
||||||
|
z-index: 3;
|
||||||
|
display: flex;
|
||||||
|
position: sticky;
|
||||||
|
background: rgba(255, 255, 255, .8);
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 1rem;
|
||||||
|
border-bottom: var(--tui-border);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.login-tips {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name,
|
||||||
|
.type,
|
||||||
|
.version {
|
||||||
|
width: 10rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 20px;
|
||||||
|
bottom: -1px;
|
||||||
|
position: sticky;
|
||||||
|
background: rgba(255, 255, 255, .8);
|
||||||
|
border-top: var(--tui-border);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
.captcha {
|
||||||
|
width: 6rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
185
src/views/repository/IssueList.vue
Normal file
185
src/views/repository/IssueList.vue
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
<template>
|
||||||
|
<t-layout class="issue-list" v-if="repositoryStore.value">
|
||||||
|
<t-header class="header">
|
||||||
|
<t-row justify="space-between">
|
||||||
|
<t-col flex="auto">
|
||||||
|
<t-input-adornment prepend="搜索">
|
||||||
|
<t-input v-model="page.keyword" placeholder="请输入关键字" />
|
||||||
|
</t-input-adornment>
|
||||||
|
</t-col>
|
||||||
|
<t-col class="right">
|
||||||
|
<t-space>
|
||||||
|
<t-select v-model="page.type" label="类型" placeholder="全部" autoWidth clearable>
|
||||||
|
<t-option v-for="(value, key) in Type" :key="key" :value="key" :label="value" />
|
||||||
|
</t-select>
|
||||||
|
<t-select v-model="page.status" label="状态" placeholder="全部" autoWidth clearable>
|
||||||
|
<t-option v-for="(value, key) in Status" :key="key" :value="key" :label="value" />
|
||||||
|
</t-select>
|
||||||
|
<router-link :to="`/${repositoryStore.value.name}/issues/edit`">
|
||||||
|
<t-button theme="success">新建反馈</t-button>
|
||||||
|
</router-link>
|
||||||
|
</t-space>
|
||||||
|
</t-col>
|
||||||
|
</t-row>
|
||||||
|
</t-header>
|
||||||
|
<t-content class="list">
|
||||||
|
<t-list v-if="pageResult" :split="true">
|
||||||
|
<t-list-item class="action" v-for="item in pageResult.list" :key="item.id">
|
||||||
|
<t-layout>
|
||||||
|
<t-aside width="auto">
|
||||||
|
<t-space class="tags" :size="4">
|
||||||
|
<div
|
||||||
|
v-if="item.type"
|
||||||
|
class="tag type"
|
||||||
|
:class="colorType[(<any>Type)[item.type]]"
|
||||||
|
v-text="(<any>Type)[item.type]"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
v-if="item.status"
|
||||||
|
class="tag status"
|
||||||
|
:class="colorStatus[(<any>Status)[item.status]]"
|
||||||
|
v-text="(<any>Status)[item.status]"
|
||||||
|
></div>
|
||||||
|
</t-space>
|
||||||
|
</t-aside>
|
||||||
|
<t-content class="content">
|
||||||
|
<router-link :to="`/${repositoryStore.value.name}/issues/${item.id}`">
|
||||||
|
<t-link theme="default" size="large" hover="color" :content="item.title" />
|
||||||
|
</router-link>
|
||||||
|
<p class="time gray">
|
||||||
|
<span v-if="item.closedAt" v-text="`关闭于 ${Time.toPassedDateTime(item.closedAt)}`"></span>
|
||||||
|
<span v-else v-text="`创建于 ${Time.toPassedDateTime(item.createdAt)}`"></span>
|
||||||
|
</p>
|
||||||
|
</t-content>
|
||||||
|
</t-layout>
|
||||||
|
</t-list-item>
|
||||||
|
</t-list>
|
||||||
|
<empty-tips :showOn="pageResult?.total === 0" />
|
||||||
|
</t-content>
|
||||||
|
<t-footer class="footer" height="auto">
|
||||||
|
<t-pagination
|
||||||
|
v-if="pageResult"
|
||||||
|
:total="pageResult.total"
|
||||||
|
:pageSize="16"
|
||||||
|
:showPageSize="false"
|
||||||
|
:onCurrentChange="(current: number) => page.index = current - 1"
|
||||||
|
/>
|
||||||
|
</t-footer>
|
||||||
|
</t-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { EmptyTips, PageResult, Time, Toolkit } from "timi-web";
|
||||||
|
import IssueAPI from "@/api/IssueAPI";
|
||||||
|
import { Issue, Page, Status, Type } from "@/types/Issue";
|
||||||
|
import { useRepositoryStore } from "@/store/repository.ts";
|
||||||
|
|
||||||
|
const colorType = {
|
||||||
|
[Type.BUG.toString()]: "bg-red",
|
||||||
|
[Type.FEATURE.toString()]: "bg-blue",
|
||||||
|
[Type.SECURITY.toString()]: "bg-orange",
|
||||||
|
[Type.QUESTION.toString()]: "bg-pink",
|
||||||
|
[Type.DISCUSS.toString()]: "bg-gray"
|
||||||
|
};
|
||||||
|
const colorStatus = {
|
||||||
|
[Status.BEFORE_CONFIRM.toString()]: "orange",
|
||||||
|
[Status.CONFIRMED.toString()]: "blue",
|
||||||
|
[Status.DEVELOPING.toString()]: "pink",
|
||||||
|
[Status.FINISHED.toString()]: "green",
|
||||||
|
[Status.CLOSED.toString()]: "gray"
|
||||||
|
};
|
||||||
|
|
||||||
|
const repositoryStore = useRepositoryStore();
|
||||||
|
|
||||||
|
const page = reactive<Page>({
|
||||||
|
repositoryId: undefined,
|
||||||
|
index: 0,
|
||||||
|
size: 12
|
||||||
|
});
|
||||||
|
const pageResult = ref<PageResult<Issue>>();
|
||||||
|
|
||||||
|
const fetchList = Toolkit.debounce(async () => pageResult.value = await IssueAPI.page(page));
|
||||||
|
|
||||||
|
watch(page, fetchList);
|
||||||
|
onMounted(async () => {
|
||||||
|
if (repositoryStore.value) {
|
||||||
|
page.repositoryId = repositoryStore.value.id;
|
||||||
|
await fetchList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.issue-list {
|
||||||
|
min-height: 480px;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
top: calc(50px + 49px);
|
||||||
|
z-index: 3;
|
||||||
|
padding: 0 20px;
|
||||||
|
display: flex;
|
||||||
|
position: sticky;
|
||||||
|
background: rgba(231, 234, 239, .8);
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: var(--tui-border);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
.t-row {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 1px;
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
padding: 1px 5px;
|
||||||
|
|
||||||
|
&.type {
|
||||||
|
color: #FFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding-left: .5rem;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 8px 16px;
|
||||||
|
bottom: -1px;
|
||||||
|
position: sticky;
|
||||||
|
background: rgba(255, 255, 255, .8);
|
||||||
|
border-top: var(--tui-border);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 992px) {
|
||||||
|
.issue-list {
|
||||||
|
min-height: calc(100vh - 100px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
181
src/views/repository/MergeDetail.vue
Normal file
181
src/views/repository/MergeDetail.vue
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="merge && repositoryStore.value" class="merge-detail">
|
||||||
|
<div class="header shadow-down">
|
||||||
|
<t-breadcrumb max-item-width="9999">
|
||||||
|
<t-breadcrumb-item>
|
||||||
|
<router-link :to="`/${repositoryStore.value.name}/merges`">合并请求列表</router-link>
|
||||||
|
</t-breadcrumb-item>
|
||||||
|
<t-breadcrumb-item>
|
||||||
|
<h3 class="title" v-text="merge.title"></h3>
|
||||||
|
</t-breadcrumb-item>
|
||||||
|
</t-breadcrumb>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="description">
|
||||||
|
<markdown-view :content="merge.description" />
|
||||||
|
<div class="edit" v-if="canEdit">
|
||||||
|
<t-button @click="router.push(`/${repositoryStore.value.name}/merges/edit/${merge.id}`)">
|
||||||
|
修改合并请求
|
||||||
|
</t-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<t-list>
|
||||||
|
<t-list-item>
|
||||||
|
<t-list-item-meta title="用户" :description="merge.requester?.name" />
|
||||||
|
</t-list-item>
|
||||||
|
<t-list-item v-if="merge.issue">
|
||||||
|
<t-list-item-meta title="相关反馈">
|
||||||
|
<template #description>
|
||||||
|
<t-link
|
||||||
|
v-popup="merge.issue.title"
|
||||||
|
theme="primary"
|
||||||
|
:href="`/${repositoryStore.value.name}/issues/${merge.issue.id}`"
|
||||||
|
target="_blank"
|
||||||
|
:content="`#${merge.issue.id}`"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</t-list-item-meta>
|
||||||
|
</t-list-item>
|
||||||
|
<t-list-item>
|
||||||
|
<t-list-item-meta title="来源分支" :description="merge.fromBranch" />
|
||||||
|
</t-list-item>
|
||||||
|
<t-list-item>
|
||||||
|
<t-list-item-meta title="去向分支" :description="merge.toBranch" />
|
||||||
|
</t-list-item>
|
||||||
|
<t-list-item v-if="merge.type">
|
||||||
|
<t-list-item-meta title="类型" :description="(<any>Type)[merge.type]" />
|
||||||
|
</t-list-item>
|
||||||
|
<t-list-item v-if="merge.status">
|
||||||
|
<t-list-item-meta title="状态" :description="(<any>Status)[merge.status]" />
|
||||||
|
</t-list-item>
|
||||||
|
<t-list-item v-if="merge.mergedAt">
|
||||||
|
<t-list-item-meta title="合并时间" :description="Time.toPassedDateTime(merge.mergedAt)" />
|
||||||
|
</t-list-item>
|
||||||
|
<t-list-item v-if="merge.rejectedAt">
|
||||||
|
<t-list-item-meta title="拒绝时间"
|
||||||
|
:description="Time.toPassedDateTime(merge.rejectedAt)" />
|
||||||
|
</t-list-item>
|
||||||
|
<t-list-item v-if="merge.closedAt">
|
||||||
|
<t-list-item-meta title="关闭时间" :description="Time.toPassedDateTime(merge.closedAt)" />
|
||||||
|
</t-list-item>
|
||||||
|
</t-list>
|
||||||
|
</div>
|
||||||
|
<timeline-comment
|
||||||
|
v-if="!!merge.id"
|
||||||
|
:biz-type="CommentBizType.GIT_MERGE"
|
||||||
|
:bizId="merge.id"
|
||||||
|
:canCreate="merge.status !== <Status>Toolkit.keyFromValue(Status, Status.CLOSED)"
|
||||||
|
:systemComments="commentTimelineSystems"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { CommentBizType, MarkdownView, Time, Toolkit, userStore } from "timi-web";
|
||||||
|
import IssueAPI from "@/api/IssueAPI";
|
||||||
|
import MergeAPI from "@/api/MergeAPI";
|
||||||
|
import TimelineComment, { SystemComment } from "@/components/TimelineComment.vue";
|
||||||
|
import { MergeView, Status, Type } from "@/types/Merge";
|
||||||
|
import { useRepositoryStore } from "@/store/repository.ts";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const repositoryStore = useRepositoryStore();
|
||||||
|
|
||||||
|
const merge = ref<MergeView>();
|
||||||
|
const commentTimelineSystems = ref<SystemComment[]>([]);
|
||||||
|
|
||||||
|
const canEdit = computed(() => {
|
||||||
|
const _merge = merge.value;
|
||||||
|
if (repositoryStore.value && _merge && _merge.id) {
|
||||||
|
const profile = userStore.loginUser?.user?.profile;
|
||||||
|
if (profile && profile.userId === _merge.requesterId) {
|
||||||
|
return !_merge.closedAt && _merge.status === <Status>Toolkit.keyFromValue(Status, Status.BEFORE_CHECK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const mergeId = router.currentRoute.value.params.id;
|
||||||
|
if (mergeId) {
|
||||||
|
merge.value = await MergeAPI.view(mergeId as any as number);
|
||||||
|
if (merge.value.checkedAt) {
|
||||||
|
commentTimelineSystems.value.push({
|
||||||
|
colorClass: "bg-pink",
|
||||||
|
inserted: false,
|
||||||
|
time: merge.value.checkedAt,
|
||||||
|
msg: "管理员已确认,等待合并"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (merge.value.mergedAt) {
|
||||||
|
commentTimelineSystems.value.push({
|
||||||
|
colorClass: "bg-green",
|
||||||
|
inserted: false,
|
||||||
|
time: merge.value.mergedAt,
|
||||||
|
msg: `已完成合并至分支 ${merge.value.toBranch}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (merge.value.rejectedAt) {
|
||||||
|
commentTimelineSystems.value.push({
|
||||||
|
colorClass: "bg-red",
|
||||||
|
inserted: false,
|
||||||
|
time: merge.value.rejectedAt,
|
||||||
|
msg: `管理员拒绝此合并请求:${merge.value.rejectReason}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (merge.value.closedAt) {
|
||||||
|
commentTimelineSystems.value.push({
|
||||||
|
colorClass: "bg-gray",
|
||||||
|
inserted: false,
|
||||||
|
time: merge.value.closedAt,
|
||||||
|
msg: "已关闭此合并请求"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (merge.value.issueId) {
|
||||||
|
merge.value.issue = await IssueAPI.view(merge.value.issueId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.merge-detail {
|
||||||
|
|
||||||
|
.header {
|
||||||
|
top: calc(50px + 49px);
|
||||||
|
margin: 0;
|
||||||
|
z-index: 3;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
position: sticky;
|
||||||
|
background: rgba(231, 234, 239, .8);
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 1rem;
|
||||||
|
border-bottom: var(--tui-border);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 80% 20%;
|
||||||
|
|
||||||
|
.description {
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-self: stretch;
|
||||||
|
border-right: var(--tui-border);
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.edit {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 2rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
264
src/views/repository/MergeEdit.vue
Normal file
264
src/views/repository/MergeEdit.vue
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
<template>
|
||||||
|
<t-layout v-if="repositoryStore.value" class="merge-edit">
|
||||||
|
<t-form class="form" :data="merge" @submit="doSubmit" :disabled="!userStore.isLogged()">
|
||||||
|
<t-header class="header">
|
||||||
|
<t-breadcrumb>
|
||||||
|
<t-breadcrumb-item>
|
||||||
|
<router-link :to="`/${repositoryStore.value.name}/merges`">合并请求列表</router-link>
|
||||||
|
</t-breadcrumb-item>
|
||||||
|
<t-breadcrumb-item v-if="merge.id">
|
||||||
|
<router-link :to="`/${repositoryStore.value.name}/merges/${merge.id}`">
|
||||||
|
<span v-text="merge.title"></span>
|
||||||
|
</router-link>
|
||||||
|
</t-breadcrumb-item>
|
||||||
|
<t-breadcrumb-item>
|
||||||
|
<span v-if="merge.id">编辑合并请求</span>
|
||||||
|
<span v-else>新建合并请求</span>
|
||||||
|
</t-breadcrumb-item>
|
||||||
|
</t-breadcrumb>
|
||||||
|
</t-header>
|
||||||
|
<t-content class="content">
|
||||||
|
<t-alert v-if="!userStore.isLogged()" class="login-tips" theme="error">合并请求需要登录账号</t-alert>
|
||||||
|
<t-form-item label="标题" name="title">
|
||||||
|
<t-input v-model="merge.title" placeholder="" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="类型" name="type">
|
||||||
|
<t-select
|
||||||
|
v-model="merge.type"
|
||||||
|
:defaultValue="Type.BUG"
|
||||||
|
autoWidth
|
||||||
|
>
|
||||||
|
<t-option v-for="(value, key) in Type" :key="key" :value="key" :label="value" />
|
||||||
|
</t-select>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="相关反馈" name="title">
|
||||||
|
<t-select
|
||||||
|
class="issue"
|
||||||
|
v-model="merge.issueId"
|
||||||
|
:loading="issueSearching"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
placeholder="请输入关键字搜索并选择"
|
||||||
|
:on-search="issueSearch"
|
||||||
|
:popup-props="{ overlayClassName: ['merge-edit', 'issue-options'] }"
|
||||||
|
>
|
||||||
|
<t-option
|
||||||
|
v-for="item in issueOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
>
|
||||||
|
<div class="option">
|
||||||
|
<div class="id" v-text="`#${item.value}`"></div>
|
||||||
|
<div v-text="item.label"></div>
|
||||||
|
</div>
|
||||||
|
</t-option>
|
||||||
|
</t-select>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="分支" name="branch">
|
||||||
|
<t-space>
|
||||||
|
<t-select class="branch" v-model="merge.fromBranch" label="来源:">
|
||||||
|
<t-option
|
||||||
|
v-for="item in fromBranches"
|
||||||
|
:key="item.name"
|
||||||
|
:value="item.name"
|
||||||
|
:label="item.name"
|
||||||
|
/>
|
||||||
|
</t-select>
|
||||||
|
<t-select class="branch" v-model="merge.toBranch" label="去向:">
|
||||||
|
<t-option
|
||||||
|
v-for="item in toBranches"
|
||||||
|
:key="item.name"
|
||||||
|
:value="item.name"
|
||||||
|
:label="item.name"
|
||||||
|
/>
|
||||||
|
</t-select>
|
||||||
|
</t-space>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="描述" name="description">
|
||||||
|
<markdown-editor v-model:data="merge.description" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item></t-form-item><!-- 用于保持边距 -->
|
||||||
|
</t-content>
|
||||||
|
<t-footer class="footer">
|
||||||
|
<t-form-item v-if="!merge.id" label="验证码" name="captcha">
|
||||||
|
<t-input-adornment>
|
||||||
|
<t-input class="captcha" v-model="captchaValue" placeholder="" />
|
||||||
|
<template #append>
|
||||||
|
<captcha
|
||||||
|
:api="CommonAPI.getCaptchaAPI()"
|
||||||
|
:from="CaptchaFrom.GIT_MERGE"
|
||||||
|
:width="90"
|
||||||
|
:height="28"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</t-input-adornment>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item>
|
||||||
|
<t-space>
|
||||||
|
<t-button type="submit">提交</t-button>
|
||||||
|
<t-button
|
||||||
|
v-if="merge.id"
|
||||||
|
@click="router.back()"
|
||||||
|
theme="default"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</t-button>
|
||||||
|
</t-space>
|
||||||
|
</t-form-item>
|
||||||
|
</t-footer>
|
||||||
|
</t-form>
|
||||||
|
</t-layout>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { CaptchaFrom, CommonAPI, LabelValue, userStore } from "timi-web";
|
||||||
|
import { Captcha, MarkdownEditor } from "timi-web";
|
||||||
|
import { MessagePlugin } from "tdesign-vue-next";
|
||||||
|
|
||||||
|
import IssueAPI from "@/api/IssueAPI";
|
||||||
|
import MergeAPI from "@/api/MergeAPI";
|
||||||
|
import { Merge, Type } from "@/types/Merge";
|
||||||
|
import { Branch } from "@/types/Repository";
|
||||||
|
import { useRepositoryStore } from "@/store/repository.ts";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const repositoryStore = useRepositoryStore();
|
||||||
|
|
||||||
|
const fromBranches = reactive<Branch[]>([]);
|
||||||
|
const toBranches = reactive<Branch[]>([]);
|
||||||
|
const issueOptions = reactive<LabelValue<number>[]>([]);
|
||||||
|
const issueSearching = ref<boolean>(false);
|
||||||
|
|
||||||
|
const merge = reactive<Merge>({
|
||||||
|
repositoryId: undefined,
|
||||||
|
title: "",
|
||||||
|
type: undefined,
|
||||||
|
description: "",
|
||||||
|
fromBranch: "",
|
||||||
|
toBranch: ""
|
||||||
|
});
|
||||||
|
const captchaValue = ref();
|
||||||
|
|
||||||
|
const issueSearch = async (keyword?: string) => {
|
||||||
|
issueSearching.value = true;
|
||||||
|
const result = await IssueAPI.page({
|
||||||
|
repositoryId: merge.repositoryId,
|
||||||
|
index: 0,
|
||||||
|
size: 10,
|
||||||
|
keyword
|
||||||
|
});
|
||||||
|
issueOptions.length = 0;
|
||||||
|
issueOptions.push(...result.list.map(item => {
|
||||||
|
return {
|
||||||
|
label: item.title,
|
||||||
|
value: item.id
|
||||||
|
} as LabelValue<number>;
|
||||||
|
}));
|
||||||
|
issueSearching.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const doSubmit = () => {
|
||||||
|
if (merge.id) {
|
||||||
|
MergeAPI.update(merge).then(() => {
|
||||||
|
if (repositoryStore.value) {
|
||||||
|
router.push(`/${repositoryStore.value.name}/merges/${merge.id}`);
|
||||||
|
}
|
||||||
|
MessagePlugin.success("提交成功");
|
||||||
|
}).catch(msg => {
|
||||||
|
MessagePlugin.error("提交失败:" + msg);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
MergeAPI.create({
|
||||||
|
from: CaptchaFrom.GIT_MERGE,
|
||||||
|
captcha: captchaValue.value,
|
||||||
|
data: merge
|
||||||
|
}).then(() => {
|
||||||
|
router.push({name: "MergeList"});
|
||||||
|
MessagePlugin.success("提交成功");
|
||||||
|
}).catch(msg => {
|
||||||
|
MessagePlugin.error("提交失败:" + msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (repositoryStore.value) {
|
||||||
|
merge.repositoryId = repositoryStore.value.id;
|
||||||
|
fromBranches.push(...repositoryStore.value.branchList);
|
||||||
|
toBranches.push(...repositoryStore.value.branchList);
|
||||||
|
await issueSearch();
|
||||||
|
|
||||||
|
const mergeId = route.params.id;
|
||||||
|
if (mergeId) {
|
||||||
|
const result = await MergeAPI.view(mergeId as any as number);
|
||||||
|
merge.id = result.id;
|
||||||
|
merge.title = result.title;
|
||||||
|
merge.type = result.type;
|
||||||
|
merge.issueId = result.issueId;
|
||||||
|
merge.fromBranch = result.fromBranch;
|
||||||
|
merge.toBranch = result.toBranch;
|
||||||
|
merge.description = result.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.merge-edit {
|
||||||
|
|
||||||
|
.header {
|
||||||
|
top: calc(50px + 49px);
|
||||||
|
margin: 0;
|
||||||
|
height: 32px;
|
||||||
|
z-index: 3;
|
||||||
|
display: flex;
|
||||||
|
position: sticky;
|
||||||
|
background: rgba(255, 255, 255, .8);
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 1rem;
|
||||||
|
border-bottom: var(--tui-border);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.login-tips {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issue {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
bottom: -1px;
|
||||||
|
padding: 20px;
|
||||||
|
position: sticky;
|
||||||
|
background: rgba(255, 255, 255, .8);
|
||||||
|
border-top: var(--tui-border);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
.captcha {
|
||||||
|
width: 6rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.merge-edit.issue-options .option) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.merge-edit.issue-options .option .id) {
|
||||||
|
color: #777;
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
199
src/views/repository/MergeList.vue
Normal file
199
src/views/repository/MergeList.vue
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
<template>
|
||||||
|
<t-layout v-if="repositoryStore.value" class="merge-list">
|
||||||
|
<t-header class="header">
|
||||||
|
<t-row justify="space-between">
|
||||||
|
<t-col flex="auto">
|
||||||
|
<t-input-adornment prepend="搜索">
|
||||||
|
<t-input v-model="page.keyword" placeholder="请输入关键字" />
|
||||||
|
</t-input-adornment>
|
||||||
|
</t-col>
|
||||||
|
<t-col class="right">
|
||||||
|
<t-space>
|
||||||
|
<t-select
|
||||||
|
v-model="page.type"
|
||||||
|
label="类型"
|
||||||
|
placeholder="全部"
|
||||||
|
autoWidth
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<t-option v-for="(value, key) in Type" :key="key" :value="key" :label="value" />
|
||||||
|
</t-select>
|
||||||
|
<t-select
|
||||||
|
v-model="page.status"
|
||||||
|
label="状态"
|
||||||
|
placeholder="全部"
|
||||||
|
autoWidth
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<t-option v-for="(value, key) in Status" :key="key" :value="key" :label="value" />
|
||||||
|
</t-select>
|
||||||
|
<router-link :to="`/${repositoryStore.value.name}/merges/edit`">
|
||||||
|
<t-button theme="success">申请合并</t-button>
|
||||||
|
</router-link>
|
||||||
|
</t-space>
|
||||||
|
</t-col>
|
||||||
|
</t-row>
|
||||||
|
</t-header>
|
||||||
|
<t-content class="list">
|
||||||
|
<t-list v-if="pageResult" :split="true">
|
||||||
|
<t-list-item class="action" v-for="item in pageResult.list" :key="item.id">
|
||||||
|
<t-layout>
|
||||||
|
<t-aside width="auto">
|
||||||
|
<t-space class="tags" :size="4">
|
||||||
|
<div
|
||||||
|
v-if="item.type"
|
||||||
|
class="tag type"
|
||||||
|
:class="colorType[(<any>Type)[item.type]]"
|
||||||
|
v-text="(<any>Type)[item.type]"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
v-if="item.status"
|
||||||
|
class="tag status"
|
||||||
|
:class="colorStatus[(<any>Status)[item.status]]"
|
||||||
|
v-text="(<any>Status)[item.status]"
|
||||||
|
></div>
|
||||||
|
</t-space>
|
||||||
|
</t-aside>
|
||||||
|
<t-content class="content">
|
||||||
|
<router-link :to="`/${repositoryStore.value.name}/merges/${item.id}`">
|
||||||
|
<t-link theme="default" size="large" hover="color" :content="item.title" />
|
||||||
|
</router-link>
|
||||||
|
<p class="time gray">
|
||||||
|
<span
|
||||||
|
v-if="item.rejectedAt"
|
||||||
|
v-text="`拒绝于 ${Time.toPassedDateTime(item.rejectedAt)}`"
|
||||||
|
></span>
|
||||||
|
<span v-else v-text="`申请于 ${Time.toPassedDateTime(item.createdAt)}`"></span>
|
||||||
|
</p>
|
||||||
|
</t-content>
|
||||||
|
</t-layout>
|
||||||
|
</t-list-item>
|
||||||
|
</t-list>
|
||||||
|
<empty-tips :showOn="pageResult?.total === 0" />
|
||||||
|
</t-content>
|
||||||
|
<t-footer class="footer" height="auto">
|
||||||
|
<t-pagination
|
||||||
|
v-if="pageResult"
|
||||||
|
:total="pageResult.total"
|
||||||
|
:pageSize="16"
|
||||||
|
:showPageSize="false"
|
||||||
|
:onCurrentChange="(current: number) => page.index = current - 1"
|
||||||
|
/>
|
||||||
|
</t-footer>
|
||||||
|
</t-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { EmptyTips, PageResult, Time, Toolkit } from "timi-web";
|
||||||
|
import MergeAPI from "@/api/MergeAPI";
|
||||||
|
import { Merge, Page, Status, Type } from "@/types/Merge";
|
||||||
|
import { useRepositoryStore } from "@/store/repository.ts";
|
||||||
|
|
||||||
|
const colorType = {
|
||||||
|
[Type.BUG.toString()]: "bg-red",
|
||||||
|
[Type.FEATURE.toString()]: "bg-blue",
|
||||||
|
[Type.SECURITY.toString()]: "bg-orange",
|
||||||
|
[Type.DOCUMENT.toString()]: "bg-green",
|
||||||
|
[Type.REFACTOR.toString()]: "bg-purple"
|
||||||
|
};
|
||||||
|
const colorStatus = {
|
||||||
|
[Status.BEFORE_CHECK.toString()]: "orange",
|
||||||
|
[Status.WAITING.toString()]: "blue",
|
||||||
|
[Status.MERGED.toString()]: "green",
|
||||||
|
[Status.REJECTED.toString()]: "red",
|
||||||
|
[Status.CLOSED.toString()]: "gray"
|
||||||
|
};
|
||||||
|
|
||||||
|
const repositoryStore = useRepositoryStore();
|
||||||
|
const page = reactive<Page>({
|
||||||
|
repositoryId: undefined,
|
||||||
|
index: 0,
|
||||||
|
size: 12
|
||||||
|
});
|
||||||
|
const pageResult = ref<PageResult<Merge>>();
|
||||||
|
const fetchList = Toolkit.debounce(async () => pageResult.value = await MergeAPI.page(page));
|
||||||
|
|
||||||
|
watch(page, fetchList);
|
||||||
|
onMounted(async () => {
|
||||||
|
if (repositoryStore.value) {
|
||||||
|
page.repositoryId = repositoryStore.value.id;
|
||||||
|
await fetchList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.merge-list {
|
||||||
|
min-height: 480px;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
top: calc(50px + 49px);
|
||||||
|
z-index: 3;
|
||||||
|
padding: 0 20px;
|
||||||
|
display: flex;
|
||||||
|
position: sticky;
|
||||||
|
background: rgba(231, 234, 239, .8);
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: var(--tui-border);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
.t-row {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 1px;
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
padding: 1px 5px;
|
||||||
|
|
||||||
|
&.type {
|
||||||
|
color: #FFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding-left: .5rem;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 8px 16px;
|
||||||
|
bottom: -1px;
|
||||||
|
position: sticky;
|
||||||
|
background: rgba(255, 255, 255, .8);
|
||||||
|
border-top: var(--tui-border);
|
||||||
|
border-top: var(--tui-border);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 992px) {
|
||||||
|
.merge-list {
|
||||||
|
min-height: calc(100vh - 100px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
130
src/views/repository/ReleaseList.vue
Normal file
130
src/views/repository/ReleaseList.vue
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
<template>
|
||||||
|
<div class="release-list">
|
||||||
|
<t-timeline class="releases" mode="alternate" v-if="pageResult">
|
||||||
|
<t-timeline-item
|
||||||
|
class="release"
|
||||||
|
v-for="(release, index) in pageResult.list"
|
||||||
|
:key="index"
|
||||||
|
dot-color="primary"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<div class="time" v-text="Time.toPassedDateTime(release.createdAt)"></div>
|
||||||
|
</template>
|
||||||
|
<div class="content">
|
||||||
|
<h3 class="version" v-text="release.version"></h3>
|
||||||
|
<div class="commits gray" v-text="`在该版本发布之后已有 ${release.commits} 次代码提交更新`"></div>
|
||||||
|
<markdown-view class="description" :content="release.description" />
|
||||||
|
<div v-if="release.attachmentList && 0 < release.attachmentList.length" class="attachments">
|
||||||
|
<h4 class="title">下载附件</h4>
|
||||||
|
<t-list :split="true" size="small">
|
||||||
|
<t-list-item
|
||||||
|
class="attachment"
|
||||||
|
v-for="attachment in release.attachmentList"
|
||||||
|
:key="attachment.id"
|
||||||
|
>
|
||||||
|
<t-link
|
||||||
|
:href="CommonAPI.getAttachmentReadAPI(attachment.mongoId)"
|
||||||
|
:content="`${attachment.title}(${attachment.name})`"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<template #prefix-icon>
|
||||||
|
<icon name="FILE" />
|
||||||
|
</template>
|
||||||
|
</t-link>
|
||||||
|
<div class="size" v-text="IOSize.format(attachment.size)"></div>
|
||||||
|
</t-list-item>
|
||||||
|
</t-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t-timeline-item>
|
||||||
|
</t-timeline>
|
||||||
|
<empty-tips :showOn="Toolkit.isEmpty(pageResult?.list)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import ReleaseAPI from "@/api/ReleaseAPI";
|
||||||
|
import { Page, Release } from "@/types/Release";
|
||||||
|
import { CommonAPI, EmptyTips, Icon, IOSize, MarkdownView, PageResult, Time, Toolkit } from "timi-web";
|
||||||
|
import { useRepositoryStore } from "@/store/repository.ts";
|
||||||
|
|
||||||
|
const repositoryStore = useRepositoryStore();
|
||||||
|
|
||||||
|
const page = reactive<Page>({
|
||||||
|
repositoryId: undefined,
|
||||||
|
index: 0,
|
||||||
|
size: 12
|
||||||
|
});
|
||||||
|
const pageResult = ref<PageResult<Release>>();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (repositoryStore.value) {
|
||||||
|
page.repositoryId = repositoryStore.value.id;
|
||||||
|
pageResult.value = await ReleaseAPI.page(page);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.release-list {
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
|
||||||
|
.releases {
|
||||||
|
|
||||||
|
.release {
|
||||||
|
|
||||||
|
.time {
|
||||||
|
width: 70px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
|
.version {
|
||||||
|
margin: 0 0 .5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commits {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachments {
|
||||||
|
border-top: 1px solid var(--td-component-border);
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: .5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment {
|
||||||
|
|
||||||
|
.file {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.release) {
|
||||||
|
|
||||||
|
.t-timeline-item__wrapper {
|
||||||
|
margin-left: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.t-timeline-item--last .t-timeline-item__tail {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 992px) {
|
||||||
|
.release-list {
|
||||||
|
min-height: calc(100vh - 98px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
15
src/vite-env.d.ts
vendored
Normal file
15
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
declare module "*.vue" {
|
||||||
|
import type { DefineComponent } from "vue";
|
||||||
|
const component: DefineComponent<{}, {}, any>;
|
||||||
|
export default component;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "prismjs";
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_API: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
42
tsconfig.json
Normal file
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"
|
||||||
|
]
|
||||||
|
}
|
||||||
91
vite.config.ts
Normal file
91
vite.config.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import dts from "vite-plugin-dts";
|
||||||
|
import { resolve } from "path";
|
||||||
|
import VueSetupExtend from "vite-plugin-vue-setup-extend";
|
||||||
|
import AutoImport from "unplugin-auto-import/vite";
|
||||||
|
import Components from "unplugin-vue-components/vite";
|
||||||
|
import { TDesignResolver } from "unplugin-vue-components/resolvers";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
base: "/",
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
dts({
|
||||||
|
include: "./src"
|
||||||
|
}),
|
||||||
|
VueSetupExtend(),
|
||||||
|
AutoImport({
|
||||||
|
imports: [
|
||||||
|
"vue",
|
||||||
|
"vue-router",
|
||||||
|
"pinia",
|
||||||
|
{
|
||||||
|
"axios": [
|
||||||
|
["default", "axios"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
dts: "src/auto-imports.d.ts",
|
||||||
|
eslintrc: {
|
||||||
|
enabled: true,
|
||||||
|
globalsPropValue: true
|
||||||
|
},
|
||||||
|
resolvers: [
|
||||||
|
TDesignResolver({
|
||||||
|
library: "vue-next"
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
dirs: [
|
||||||
|
"src/components",
|
||||||
|
"src/layout",
|
||||||
|
"src/views/components"
|
||||||
|
],
|
||||||
|
resolvers: [
|
||||||
|
TDesignResolver({
|
||||||
|
library: "vue-next"
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
less: {
|
||||||
|
math: "always",
|
||||||
|
lessOptions: {
|
||||||
|
modifyVars: {
|
||||||
|
"@btn-border-radius": "0"
|
||||||
|
},
|
||||||
|
javascriptEnabled: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
"process.env": {}
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": resolve(__dirname, "src")
|
||||||
|
},
|
||||||
|
extensions: [".js", ".json", ".ts"]
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: "git.imyeyu.dev",
|
||||||
|
port: 81
|
||||||
|
},
|
||||||
|
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