refacting: delete cloud comp

This commit is contained in:
simple321vip
2026-06-30 07:56:09 +08:00
parent 024f306636
commit 75169c70a5
10 changed files with 944 additions and 470 deletions
-31
View File
@@ -1,31 +0,0 @@
import { MockMethod } from 'vite-plugin-mock'
export default [
{
url: '/cloud/api/v1/file/list',
method: 'get',
response: ({ body, query }) => {
return [
{
server_filename: "java实战.pdf",
path: "/",
isDir: 0,
size: 1000
},
{
server_filename: "天道.pdf",
path: "/",
isDir: 0,
size: 1000
},
{
server_filename: "我得音乐",
path: "/",
isDir: 1,
size: 10001
},
]
}
},
] as MockMethod[]
+13 -2
View File
@@ -16,10 +16,21 @@ const post_event = (data: {
}) })
} }
const get_event = () => { /**
* 查询事件(按月份 + 分页)
* @param params.month yyyy-MM(必填,null 时后端默认当前月)
* @param params.page 页码(从 1 开始,默认 1)
* @param params.pageSize 每页大小(默认 10,上限 100)
*/
const get_event = (params?: {
month?: string
page?: number
pageSize?: number
}) => {
return service({ return service({
url: '/calendar/api/v1/events', url: '/calendar/api/v1/events',
method: 'GET' method: 'GET',
params
}) })
} }
-16
View File
@@ -1,16 +0,0 @@
import { service } from '../utils/request'
const headers = {
'Content-Type': 'application/json;charsetset=UTF-8'
}
const getFileList = (url: string) => {
return service({
url: '/cloud/api/v1/file/list',
method: 'GET',
params: { url: url },
headers: headers
})
}
export { getFileList }
-3
View File
@@ -21,9 +21,6 @@
<el-icon v-if="index == 4" :size="20"> <el-icon v-if="index == 4" :size="20">
<Calendar /> <Calendar />
</el-icon> </el-icon>
<el-icon v-if="index == 5" :size="20">
<MostlyCloudy />
</el-icon>
<el-icon v-if="index == 10" :size="20"> <el-icon v-if="index == 10" :size="20">
<Setting /> <Setting />
</el-icon> </el-icon>
+477 -40
View File
@@ -1,22 +1,73 @@
<template> <template>
<div class="ttt"> <div class="ttt">
<div class="yuliu" :style="style"> <!-- 动态光斑背景 -->
<div class="bg-orbs">
<span class="orb orb-1"></span>
<span class="orb orb-2"></span>
<span class="orb orb-3"></span>
</div> </div>
<el-card class="box-card">
<el-form> <div class="content">
<div class="title-container"> <!-- 玻璃卡片 -->
<h3>秘密基地</h3> <div class="glass-card">
<div class="brand">
<span class="brand-mark"></span>
<h1 class="brand-name">{{ SITE_NAME }}</h1>
<p class="brand-sub">Secret Base · 秘密基地</p>
</div> </div>
<div>
<el-button type="primary" size="large" @click="handleOidcLogin">统一登录</el-button> <div class="divider"></div>
<el-button
class="login-btn"
size="large"
@click="handleOidcLogin"
>
<span class="login-btn-content">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2L3 7v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-9-5zm0 2.18l7 3.89v4.93c0 4.43-3.07 8.58-7 9.71-3.93-1.13-7-5.28-7-9.71V8.07l7-3.89z"/>
<path d="M12 11h-2v2h-2v2h2v2h2v-2h2v-2h-2z"/>
</svg>
Login
</span>
</el-button>
<!-- <p class="login-hint">
通过 <a href="https://goauthentik.io" target="_blank">Authentik</a> OIDC 安全登录
</p> -->
</div> </div>
</el-form>
------------------------------------------------------------
<div>
微信/支付宝
</div> </div>
</el-card>
<!-- 右侧关联链接 -->
<div class="side-links">
<a
v-for="link in sideLinks"
:key="link.name"
:href="link.url"
target="_blank"
class="side-link"
:title="link.name"
>
<span class="side-link-icon" v-html="link.icon"></span>
<span class="side-link-name">{{ link.name }}</span>
</a>
</div>
<!-- 左侧外部网站链接 -->
<div class="external-links">
<a
v-for="link in externalLinks"
:key="link.name"
:href="link.url"
target="_blank"
class="external-link"
:title="link.name"
>
<span class="external-link-icon" v-html="link.icon"></span>
<span class="external-link-name">{{ link.name }}</span>
</a>
</div>
<div class="footer"> <div class="footer">
<div class="footer-left"> <div class="footer-left">
<span>{{ SITE_NAME }} {{ COPYRIGHT_TEXT }}</span> <span>{{ SITE_NAME }} {{ COPYRIGHT_TEXT }}</span>
@@ -32,7 +83,6 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { SITE_NAME, COPYRIGHT_TEXT, ICP_NUMBER, ICP_QUERY_URL } from '@/const/site' import { SITE_NAME, COPYRIGHT_TEXT, ICP_NUMBER, ICP_QUERY_URL } from '@/const/site'
@@ -62,50 +112,430 @@ const openGithub = () => {
window.open('https://github.com/simple321vip', '_blank') window.open('https://github.com/simple321vip', '_blank')
} }
const style = reactive({} as any) /**
style.width = window.innerWidth * 0.7 + 'px' * 右侧固定关联网站链接。改这里即可增删。
* icon: 简单 inline SVG24x24),用 currentColor 跟父元素文字色联动
*/
const sideLinks = [
{
name: 'Gitea',
url: 'https://gitea.violin-work.online',
// Gitea 标志性元素:圆形 + 三个肉垫(猫爪印)
icon: '<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="14" r="6"/><ellipse cx="6" cy="8" rx="2" ry="2.5"/><ellipse cx="12" cy="5" rx="2" ry="2.5"/><ellipse cx="18" cy="8" rx="2" ry="2.5"/></svg>'
},
{
name: 'Blog',
url: 'https://www.violin-home.cn/blog',
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zM7 10h10v2H7zm0 4h10v2H7z"/></svg>'
},
{
name: 'Authentik',
url: 'https://auth.violin-work.online',
// Authentik 标志性元素:盾牌 + 钥匙孔
icon: '<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.5a2 2 0 110 1 2 2 0 010-1zm-2.83 4.95L8 14l-1.17 2.45L4 18l2.83-1.55z"/></svg>'
},
{
name: 'KubeSphere',
url: 'https://k8s.violin-work.online',
// KubeSphere 标志性元素:球体 + 经线纬线(地球网格)
icon: '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><ellipse cx="12" cy="12" rx="10" ry="4"/><ellipse cx="12" cy="12" rx="4" ry="10"/><line x1="2" y1="12" x2="22" y2="12"/></svg>'
},
{
name: 'Viking (RAG)',
url: 'https://viking.violin-work.online/',
// Viking / 维京:维京头盔 + 翅膀,或简化为双翼头盔
icon: '<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L9 5H6v4l-3 2 3 1v3a6 6 0 0012 0v-3l3-1-3-2V5h-3l-3-3zm0 4a2 2 0 110 4 2 2 0 010-4z"/><path d="M3 11l-1 1 1 1zm18 0l1 1-1 1z"/></svg>'
},
{
name: 'Nextcloud',
url: 'https://cloud.violin-work.online/',
// Nextcloud 标志性元素:三个圆环组成的"云"
icon: '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="7" cy="14" r="4"/><circle cx="14" cy="9" r="4"/><circle cx="18" cy="15" r="3"/></svg>'
},
{
name: 'MinIO',
url: 'https://minio.violin-work.online/',
// MinIO 标志性元素:三层堆叠的小方块
icon: '<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><rect x="3" y="14" width="18" height="6" rx="1"/><rect x="5" y="8" width="14" height="5" rx="1" opacity="0.7"/><rect x="7" y="3" width="10" height="4" rx="1" opacity="0.45"/></svg>'
}
] as const
/**
* 左侧固定外部网站链接。
*/
const externalLinks = [
{
name: 'GitHub',
url: 'https://github.com/simple321vip',
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.4 3-.405 1.02.005 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'
},
{
name: 'MiniMax',
url: 'https://www.minimaxi.com/',
// MiniMax 标志性元素:六角星 + 中心点(极小化极大概括)
icon: '<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l2.5 5.5L20 9l-4 4 1 6-5-3-5 3 1-6-4-4 5.5-1.5L12 2z"/></svg>'
},
{
name: '百度',
url: 'https://www.baidu.com/',
// 百度:爪形脚印
icon: '<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><ellipse cx="9" cy="11" rx="3.5" ry="5"/><ellipse cx="15" cy="11" rx="3.5" ry="5"/><circle cx="6" cy="5" r="1.5"/><circle cx="12" cy="3" r="1.5"/><circle cx="18" cy="5" r="1.5"/></svg>'
},
{
name: '腾讯云',
url: 'https://cloud.tencent.com/',
// 腾讯云:云朵 + T 字标识
icon: '<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M6 18a4 4 0 010-8 5 5 0 019.58-1.5A4 4 0 0118 18H6z"/><text x="12" y="16" font-size="8" font-weight="bold" text-anchor="middle" fill="#0f0c29">T</text></svg>'
},
{
name: '菜鸟教程',
url: 'https://www.runoob.com/',
// 菜鸟教程:鸟的剪影(飞行)
icon: '<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M22 4l-2 2-3-1-5 3-4-2-3 3 2 2 4-2 3 2 4-2 4 1 1-2-3-2 2-2zM2 14l4 4 6-1 4 2 2-1-3-3-4 1-3-3-4 1-2-1z"/></svg>'
}
] as const
</script> </script>
<style scoped> <style scoped>
.text {
font-size: 14px;
}
.item {
padding: 18px 0;
}
.ttt { .ttt {
/* flex column + min-height 100vhfooter 永远在视口底部, position: relative;
内容少时 footer 贴底,内容多时 footer 在内容之后但不漂 */
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden;
background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%);
} }
.yuliu { /* ============ 动态光斑背景 ============ */
/* width: 600px; */ .bg-orbs {
height: 300px; position: absolute;
float: left; inset: 0;
/* background-color: antiquewhite; */ overflow: hidden;
background: url('../../assets/have_a_nice_day.jpeg') center top no-repeat; pointer-events: none;
background-size: cover; z-index: 0;
} }
.box-card { .orb {
/* 中间内容区撑开剩余空间 */ position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.5;
animation: float 20s ease-in-out infinite;
}
.orb-1 {
width: 500px;
height: 500px;
background: radial-gradient(circle, #667eea 0%, transparent 70%);
top: -150px;
left: -100px;
animation-delay: 0s;
}
.orb-2 {
width: 600px;
height: 600px;
background: radial-gradient(circle, #f093fb 0%, transparent 70%);
bottom: -200px;
right: -150px;
animation-delay: -7s;
}
.orb-3 {
width: 400px;
height: 400px;
background: radial-gradient(circle, #4facfe 0%, transparent 70%);
top: 40%;
left: 50%;
animation-delay: -14s;
}
@keyframes float {
0%, 100% { transform: translate(0, 0) scale(1); }
33% { transform: translate(50px, -30px) scale(1.1); }
66% { transform: translate(-30px, 50px) scale(0.9); }
}
/* ============ 内容区 ============ */
.content {
position: relative;
z-index: 1;
flex: 1; flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 20px;
} }
/* ============ 玻璃卡片 ============ */
.glass-card {
width: 100%;
max-width: 420px;
padding: 56px 40px 48px;
background: rgba(255, 255, 255, 0.06);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 20px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
color: #fff;
}
.brand {
text-align: center;
margin-bottom: 32px;
}
.brand-mark {
display: inline-block;
font-size: 32px;
background: linear-gradient(135deg, #667eea 0%, #f093fb 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
margin-bottom: 12px;
animation: pulse 3s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.7; transform: scale(1); }
50% { opacity: 1; transform: scale(1.1); }
}
.brand-name {
margin: 0 0 8px;
font-size: 32px;
font-weight: 600;
letter-spacing: 1px;
background: linear-gradient(135deg, #fff 0%, #b8c6db 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.brand-sub {
margin: 0;
font-size: 13px;
color: rgba(255, 255, 255, 0.5);
letter-spacing: 2px;
}
/* ============ 分隔线 ============ */
.divider {
height: 1px;
background: linear-gradient(90deg,
transparent 0%,
rgba(255, 255, 255, 0.2) 50%,
transparent 100%);
margin: 24px 0 32px;
}
/* ============ 登录按钮 ============ */
.login-btn {
width: 100%;
height: 50px !important;
border: none !important;
border-radius: 12px !important;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4) !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
overflow: hidden;
}
.login-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.6) !important;
}
.login-btn:active {
transform: translateY(0);
}
.login-btn-content {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-size: 16px;
font-weight: 500;
letter-spacing: 1px;
color: #fff;
}
.login-hint {
margin: 20px 0 0;
text-align: center;
font-size: 12px;
color: rgba(255, 255, 255, 0.4);
}
.login-hint a {
color: rgba(255, 255, 255, 0.6);
text-decoration: none;
border-bottom: 1px dashed rgba(255, 255, 255, 0.3);
}
.login-hint a:hover {
color: #fff;
border-bottom-color: #fff;
}
/* ============ 右侧固定关联链接 ============ */
.side-links {
position: fixed;
right: 32px;
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
gap: 14px;
z-index: 10;
}
.side-link {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.side-link:hover {
background: rgba(102, 126, 234, 0.25);
border-color: rgba(102, 126, 234, 0.6);
color: #fff;
transform: scale(1.1);
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4);
}
.side-link-icon {
display: flex;
align-items: center;
justify-content: center;
}
/* tooltip 显示名字 */
.side-link-name {
position: absolute;
right: 56px;
white-space: nowrap;
padding: 6px 12px;
border-radius: 6px;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(10px);
color: #fff;
font-size: 13px;
opacity: 0;
transform: translateX(10px);
pointer-events: none;
transition: all 0.25s ease;
}
.side-link:hover .side-link-name {
opacity: 1;
transform: translateX(0);
}
/* 移动端隐藏侧栏(屏幕太窄) */
@media (max-width: 768px) {
.side-links {
display: none;
}
}
/* ============ 左侧外部网站链接 ============ */
.external-links {
position: fixed;
left: 32px;
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
gap: 14px;
z-index: 10;
}
.external-link {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.external-link:hover {
background: rgba(118, 75, 162, 0.25);
border-color: rgba(240, 147, 251, 0.6);
color: #fff;
transform: scale(1.1);
box-shadow: 0 4px 20px rgba(240, 147, 251, 0.4);
}
.external-link-icon {
display: flex;
align-items: center;
justify-content: center;
}
/* tooltip 在右侧(跟左侧链接方向相反) */
.external-link-name {
position: absolute;
left: 56px;
white-space: nowrap;
padding: 6px 12px;
border-radius: 6px;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(10px);
color: #fff;
font-size: 13px;
opacity: 0;
transform: translateX(-10px);
pointer-events: none;
transition: all 0.25s ease;
}
.external-link:hover .external-link-name {
opacity: 1;
transform: translateX(0);
}
@media (max-width: 768px) {
.external-links {
display: none;
}
}
/* ============ Footer ============ */
.footer { .footer {
position: relative;
z-index: 1;
flex: 0 0 auto; flex: 0 0 auto;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 12px 24px; padding: 16px 32px;
border-top: 1px solid rgba(151, 151, 151, 0.2); color: rgba(255, 255, 255, 0.5);
font-size: 16px; font-size: 13px;
color: #666;
} }
.footer-left { .footer-left {
@@ -120,20 +550,27 @@ style.width = window.innerWidth * 0.7 + 'px'
} }
.separator { .separator {
color: #ccc; color: rgba(255, 255, 255, 0.25);
} }
.icp-link { .icp-link {
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
transition: color 0.2s;
} }
.icp-link:hover { .icp-link:hover {
color: #409eff; color: rgba(255, 255, 255, 0.85);
} }
.github-icon { .github-icon {
vertical-align: middle; vertical-align: middle;
cursor: pointer; cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
}
.github-icon:hover {
opacity: 1;
} }
</style> </style>
-7
View File
@@ -43,13 +43,6 @@ const routes: Array<RouteRecordRaw> = [
name: '日历' name: '日历'
} }
}, },
{
path: '/cloud',
component: () => import('@/view/cloud/index.vue'),
meta: {
name: '个人云盘'
}
},
{ {
path: '/market', path: '/market',
component: () => import('@/view/recommend/index.vue'), component: () => import('@/view/recommend/index.vue'),
+339 -102
View File
@@ -1,61 +1,137 @@
<template> <template>
<el-form :model="form" label-width="120px"> <!-- 顶部 header标题 + 主操作 -->
<el-form-item label="关键字搜索"> <div class="blog-header">
<el-col :span="7"> <div class="blog-header-left">
<el-input v-model="form.key_word" /> <span class="blog-icon">📚</span>
</el-col> <h2 class="blog-title">博客</h2>
<el-col :span="1"></el-col> <el-tag v-if="pagination.total > 0" type="info" size="small" round>
<el-col :span="10"> {{ pagination.total }}
<span>分类搜索</span> </el-tag>
<el-select v-model="form.btId" placeholder="选择分类"> </div>
<div class="blog-header-right">
<el-button type="primary" :icon="EditPen" @click="openBlogEditer">创作</el-button>
<el-dropdown @command="handleBatchCommand">
<el-button :icon="MoreFilled">
批量<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="publishAll">
<el-icon><Promotion /></el-icon> 重新发布全部
</el-dropdown-item>
<el-dropdown-item command="publishProfile">
<el-icon><User /></el-icon> 重新发布 profile
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<!-- 搜索筛选区 -->
<el-card class="filter-card" shadow="never">
<el-form :inline="true" :model="form" class="filter-form">
<el-form-item label="关键字">
<el-input
v-model="form.key_word"
placeholder="搜索标题或内容"
clearable
style="width: 220px"
@keyup.enter="onSearch"
/>
</el-form-item>
<el-form-item label="分类">
<el-select v-model="form.btId" placeholder="全部分类" clearable style="width: 160px">
<el-option v-for="item in btNameList" :key="item.btId" :label="item.btName" :value="item.btId" /> <el-option v-for="item in btNameList" :key="item.btId" :label="item.btName" :value="item.btId" />
</el-select> </el-select>
</el-col>
</el-form-item> </el-form-item>
<el-form-item label="创作时间"> <el-form-item label="时间">
<el-col :span="7"> <el-date-picker
<el-date-picker v-model="form.start_day" type="date" placeholder="开始时间" style="width: 100%" /> v-model="dateRange"
</el-col> type="daterange"
<el-col :span="2" class="text-center"> range-separator=""
<span class="text-gray-500">-</span> start-placeholder="开始"
</el-col> end-placeholder="结束"
<el-col :span="7"> value-format="YYYY-MM-DD"
<el-time-picker v-model="form.end_day" placeholder="结束时间" style="width: 100%" /> style="width: 260px"
</el-col> />
<el-col :span="6" style="text-align: center;"> </el-form-item>
<el-button type="primary" @click="onSearch">搜索</el-button> <el-form-item>
</el-col> <el-button type="primary" :icon="Search" @click="onSearch">搜索</el-button>
<el-button :icon="RefreshLeft" @click="onReset">重置</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-button @click="openBlogEditer">创作</el-button> </el-card>
<el-button @click="publishbyBids">重新发布Wiki</el-button>
<el-button @click="publishProfile">重新发布profile</el-button>
<div class="blog_list" v-for="item in data_list"> <!-- 加载态 -->
<h3 style="cursor: pointer; width: min-content;white-space:nowrap" @click="openBlog(item.bid)">{{ item.title }}</h3> <div v-if="isLoading" class="loading">
<div style="display: flex; justify-content: space-between; align-items: center"> <el-icon class="is-loading"><Loading /></el-icon>
<p style="font-size: 12px; height: 16px;line-height: 16px;overflow: hidden;text-overflow: ellipsis;">{{ <span>加载中...</span>
item.content </div>
}}</p>
<el-button size="small" type="success" @click="publishbyBid(item.bid)">重新发布 <!-- 空态 -->
<el-empty v-else-if="data_list.length === 0" description="暂无博客,点击右上方开始创作" />
<!-- 博客列表卡片 -->
<div v-else class="blog-list">
<el-card
v-for="item in data_list"
:key="item.bid"
class="blog-card"
shadow="hover"
@click="openBlog(item.bid)"
>
<div class="blog-card-header">
<h3 class="blog-card-title">{{ item.title }}</h3>
<div class="blog-card-actions" @click.stop>
<el-button size="small" :icon="View" @click="openBlog(item.bid)">查看</el-button>
<el-button size="small" type="success" :icon="Promotion" @click="publishbyBid(item.bid)">
发布
</el-button> </el-button>
</div> </div>
<!-- <span style="font-size: 12px;">{{ item.blog_prex }}</span> -->
</div> </div>
<div class="page_style">
<!-- <el-pagination small layout="prev, pager, next" :total="50" /> --> <p class="blog-card-preview">{{ item.content || '(无内容预览)' }}</p>
<el-pagination small background layout="prev, pager, next" :total="50" class="mt-4" />
<div class="blog-card-meta">
<el-tag v-if="item.btName" size="small" type="primary" effect="plain">
📁 {{ item.btName }}
</el-tag>
<span class="meta-item">📅 {{ item.createDate || '未知时间' }}</span>
<span v-if="readingMinutes(item.content)" class="meta-item">
{{ readingMinutes(item.content) }} 分钟
</span>
</div>
</el-card>
</div>
<!-- 分页 -->
<div v-if="data_list.length > 0" class="pagination-wrap">
<el-pagination
background
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
:page-size="pagination.pageSize"
:current-page="pagination.page"
:page-sizes="[10, 20, 50, 100]"
@size-change="onPageSizeChange"
@current-change="onPageChange"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Cookies from 'js-cookie' import { onMounted, reactive, ref, watch } from 'vue'
import { reactive } from 'vue' import {
import { getBlogs, getBtName, publish, publishAll } from '../../api/blogView' Search, RefreshLeft, EditPen, MoreFilled, ArrowDown,
import { publish_profile } from '../../api/profile' Promotion, User, View, Loading
import router from '../../router/index' } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { getBlogs, getBtName, publish, publishAll } from '@/api/blogView'
import { publish_profile } from '@/api/profile'
import router from '@/router'
// -- INTERFACE OR TYPE DEFINITION -- // -- TYPE --
interface Blog { interface Blog {
bid: string bid: string
btId: string btId: string
@@ -63,6 +139,7 @@ interface Blog {
title: string title: string
blog_prex: string blog_prex: string
content: string content: string
createDate?: string
} }
interface Blog_Type { interface Blog_Type {
btId: string btId: string
@@ -72,92 +149,252 @@ interface Blog_Type {
// -- REACTIVE OBJECT -- // -- REACTIVE OBJECT --
const form = reactive({ const form = reactive({
key_word: '', key_word: '',
btId: '', btId: '' as string | undefined,
start_day: '', start_day: '' as string | undefined,
end_day: '', end_day: '' as string | undefined,
page_number: 1,
page_size: 5,
total: 1
}) })
const dateRange = ref<[string, string] | null>(null)
const data_list = reactive<Blog[]>([]) const data_list = reactive<Blog[]>([])
const btNameList = reactive<Blog_Type[]>([]) const btNameList = reactive<Blog_Type[]>([])
// -- EVENT DEFINITION const isLoading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
// -- HELPERS --
const readingMinutes = (text?: string): number => {
if (!text) return 0
// 中文 ~300 字/分钟
const len = text.length
return Math.max(1, Math.round(len / 300))
}
const extractDateRange = () => {
if (dateRange.value && dateRange.value.length === 2) {
form.start_day = dateRange.value[0]
form.end_day = dateRange.value[1]
} else {
form.start_day = undefined
form.end_day = undefined
}
}
// -- EVENT DEFINITION --
const fetchBlogs = async () => {
isLoading.value = true
extractDateRange()
try {
const res = await getBlogs({
...form,
page: pagination.page,
page_size: pagination.pageSize
})
// 后端 ApiResponse 结构:{code, message, data: PageResponse | List}
const payload = res.data?.data ?? res.data
const items = Array.isArray(payload) ? payload : (payload?.items ?? [])
pagination.total = payload?.total ?? items.length
pagination.page = payload?.page ?? pagination.page
pagination.pageSize = payload?.pageSize ?? pagination.pageSize
data_list.splice(0, data_list.length)
items.forEach((item: Blog) => data_list.push(item))
} catch {
// 错误由 axios 拦截器处理
} finally {
isLoading.value = false
}
}
const fetchBtNames = async () => {
try {
const res = await getBtName()
const payload = res.data?.data ?? res.data
const items = Array.isArray(payload) ? payload : []
btNameList.splice(0, btNameList.length)
items.forEach((item: Blog_Type) => btNameList.push(item))
} catch {
// ignore
}
}
const onSearch = () => { const onSearch = () => {
getBlogs(form).then(response => { pagination.page = 1
data_list.splice(0, data_list.length); fetchBlogs()
response.data.forEach((item: Blog) => { }
data_list.push(item)
}) const onReset = () => {
}) form.key_word = ''
form.btId = ''
form.start_day = ''
form.end_day = ''
dateRange.value = null
pagination.page = 1
fetchBlogs()
}
const onPageChange = (p: number) => {
pagination.page = p
fetchBlogs()
}
const onPageSizeChange = (s: number) => {
pagination.pageSize = s
pagination.page = 1
fetchBlogs()
} }
const openBlog = (bid: string) => { const openBlog = (bid: string) => {
const { href } = router.resolve({ const { href } = router.resolve({ path: '/view', query: { bid } })
path: '/view', window.open(href, '_blank')
query: {
bid: bid
}
});
window.open(href, '_blank');
} }
const openBlogEditer = () => { const openBlogEditer = () => {
const { href } = router.resolve({ const { href } = router.resolve({ path: '/write/' })
path: '/write/' window.open(href, '_blank')
});
window.open(href, '_blank');
} }
const publishbyBids = () => { const publishbyBid = async (bid: string) => {
publishAll() await publish(bid)
ElMessage.success('发布指令已发送')
} }
const publishProfile = () => { const handleBatchCommand = async (cmd: string) => {
publish_profile() if (cmd === 'publishAll') {
} await publishAll()
ElMessage.success('批量发布已触发')
const publishbyBid = (bid: string) => { } else if (cmd === 'publishProfile') {
publish(bid) await publish_profile()
ElMessage.success('profile 重新发布已触发')
}
} }
/** /**
* AUTO INVOKE FUNCTION * AUTO INVOKE FUNCTION
*/ */
(() => { onMounted(() => {
fetchBtNames()
getBlogs(form).then(response => { fetchBlogs()
response.data.forEach((item: Blog) => { })
data_list.push(item)
})
})
getBtName().then(response => {
btNameList.push({ btId: "", btName: "选择分类" })
response.data.forEach((item: any) => {
const bts = {
btId: item.btId,
btName: item.btName
}
btNameList.push(bts)
})
})
})()
</script> </script>
<style> <style lang="scss" scoped>
.blog_list { .blog-header {
display: flex; display: flex;
flex-direction: column; justify-content: space-between;
border-bottom: 1px solid rgba(151, 151, 151, 0.3); align-items: center;
padding-top: 10px; margin-bottom: 16px;
padding-bottom: 10px; }
.blog-header-left {
display: flex;
align-items: center;
gap: 10px;
}
.blog-icon { font-size: 24px; }
.blog-title {
margin: 0;
font-size: 22px;
font-weight: 600;
color: #303133;
}
.blog-header-right {
display: flex;
gap: 8px;
} }
.page_style { .filter-card {
margin-top: 25px; margin-bottom: 16px;
background: #fafbfc;
}
.filter-form {
margin-bottom: -16px; /* 抵消 el-form-item 底部 margin */
}
.loading {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 40px 0;
color: #909399;
}
.blog-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px;
}
.blog-card {
cursor: pointer;
transition: all 0.2s ease;
border-radius: 10px;
:deep(.el-card__body) {
padding: 18px 20px;
}
}
.blog-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
margin-bottom: 10px;
}
.blog-card-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #303133;
flex: 1;
/* 单行省略 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.2s;
}
.blog-card:hover .blog-card-title { color: #409eff; }
.blog-card-actions {
display: flex;
gap: 6px;
flex-shrink: 0;
}
.blog-card-preview {
margin: 0 0 12px;
font-size: 14px;
line-height: 1.6;
color: #606266;
/* 两行省略 */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
min-height: 44px;
}
.blog-card-meta {
display: flex;
align-items: center; align-items: center;
gap: 16px;
font-size: 12px;
color: #909399;
flex-wrap: wrap;
}
.meta-item {
display: inline-flex;
align-items: center;
gap: 4px;
}
.pagination-wrap {
display: flex;
justify-content: flex-end;
margin-top: 20px;
padding: 8px 0;
} }
</style> </style>
+106 -30
View File
@@ -21,13 +21,37 @@
</template> </template>
</el-calendar> </el-calendar>
<span>待办事项</span> <div class="toolbar">
<el-table :data="eventDataList"> <span class="toolbar-label">待办事项</span>
<el-select v-model="pageSize" size="small" style="width: 110px">
<el-option label="10 条/页" :value="10" />
<el-option label="20 条/页" :value="20" />
<el-option label="50 条/页" :value="50" />
<el-option label="100 条/页" :value="100" />
</el-select>
</div>
<el-table :data="eventDataList" v-loading="isComponentLoading">
<el-table-column property="eventDate" label="Date" width="150" /> <el-table-column property="eventDate" label="Date" width="150" />
<el-table-column property="title" label="title" width="200" /> <el-table-column property="title" label="title" width="200" />
<el-table-column property="eventInfo" label="eventInfo" /> <el-table-column property="eventInfo" label="eventInfo" />
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button size="small" type="danger" @click="onDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table> </el-table>
<el-pagination
class="pagination"
background
layout="prev, pager, next, total"
:total="pagination.total"
:page-size="pageSize"
:current-page="pagination.page"
@current-change="onPageChange"
/>
<el-dialog v-model="dialogFormVisible" title="待办事项"> <el-dialog v-model="dialogFormVisible" title="待办事项">
<el-form :model="form"> <el-form :model="form">
<el-form-item label="日期" :label-width="formLabelWidth"> <el-form-item label="日期" :label-width="formLabelWidth">
@@ -59,14 +83,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { nextTick, onMounted, reactive, ref } from 'vue' import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { get_holiday } from '@/service/calendar' import { get_holiday } from '@/service/calendar'
import { toLocalDate } from '@/utils/date' import { toLocalDate } from '@/utils/date'
import { get_event, post_event, put_event, delete_event } from '@/api/calendar' import { get_event, post_event, delete_event } from '@/api/calendar'
import { Event } from '@/entity/index' import { Event } from '@/entity/index'
import { settingsStore } from '@/store/modules/settings' import { settingsStore } from '@/store/modules/settings'
// -- IMPORT -- // -- IMPORT --
const useSettingsStore = settingsStore() const useSettingsStore = settingsStore()
@@ -91,6 +115,16 @@ const isComponentLoading = ref(true)
const checkedalerts = ref([]) const checkedalerts = ref([])
const alerts = ['email', 'messageBox', '手机短信', '微信通知'] const alerts = ['email', 'messageBox', '手机短信', '微信通知']
// 分页
const pageSize = ref(10)
const pagination = reactive({ page: 1, total: 0 })
// 当前选中的月份(yyyy-MM
const monthStr = computed(() => {
const d = value.value
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
})
// -- EVENT DEFINITION // -- EVENT DEFINITION
const onclick = (date: Date) => { const onclick = (date: Date) => {
form.eventDate = toLocalDate(date) form.eventDate = toLocalDate(date)
@@ -115,44 +149,72 @@ const handleCommand = (command: string) => {
useSettingsStore.settings.isRouterAlive = true useSettingsStore.settings.isRouterAlive = true
useSettingsStore.settings.isLoading = false useSettingsStore.settings.isLoading = false
}) })
} else if (command === 'closeOther') {
// useSettingsStore.closeOther()
} else {
// useSettingsStore.closeLeftOrRight(command)
} }
} }
const onPageChange = (p: number) => {
pagination.page = p
reloadEvents()
}
const reloadEvents = async () => {
isComponentLoading.value = true
try {
const res = await get_event({
month: monthStr.value,
page: pagination.page,
pageSize: pageSize.value
})
// 后端 ApiResponse 结构:{code, message, data: {items, total, page, pageSize}}
const pageResp = res.data?.data ?? res.data
const items = Array.isArray(pageResp) ? pageResp : (pageResp?.items ?? [])
pagination.total = pageResp?.total ?? items.length
pagination.page = pageResp?.page ?? pagination.page
eventDataList.length = 0
eventDataObject.value = {}
items.forEach((record: Event) => {
eventDataList.push(record)
eventDataObject[record.eventDate] = record.title
})
} catch {
// 错误已由 axios 拦截器处理
} finally {
isComponentLoading.value = false
}
}
const onDelete = async (row: Event) => {
if (!row.id) return
try {
await ElMessageBox.confirm(`确认删除 "${row.title}"`, '提示', { type: 'warning' })
} catch {
return // 用户取消
}
await delete_event(row.id)
ElMessage.success('删除成功')
reloadEvents()
}
// 切换月份 / 改变 pageSize → 重新加载
watch([monthStr, pageSize], () => {
pagination.page = 1
reloadEvents()
}, { deep: true })
/** /**
* AUTO INVOKE FUNCTION * AUTO INVOKE FUNCTION
*/ */
onMounted(async () => { onMounted(async () => {
// 先显示日历(避免数据加载失败阻塞日历渲染)
isComponentActive.value = true isComponentActive.value = true
await get_holiday().then((resp) => { await get_holiday().then((resp) => {
// holidays-jp.github.io 直接返回 JSON 对象,不是 ApiResponse 结构
Object.keys(resp.data).forEach(element => { Object.keys(resp.data).forEach(element => {
holidays[element] = resp.data[element] holidays[element] = resp.data[element]
}) })
}).catch(() => { }).catch(() => { /* 节假日失败不影响 */ })
// 节假日数据失败不影响日历显示
})
await get_event().then((response) => { await reloadEvents()
// 后端 ApiResponse 结构:{code, message, data: [...]}
const list = response.data?.data ?? response.data
eventDataList.length = 0
if (Array.isArray(list)) {
list.forEach((record: Event) => {
eventDataList.push(record)
eventDataObject[record.eventDate] = record.title
})
}
}).catch(() => {
// 业务异常已经被 axios 拦截器 toast 了
})
isComponentLoading.value = false
}) })
@@ -162,8 +224,22 @@ onMounted(async () => {
.date-cell-holiday { .date-cell-holiday {
background: #dfd; background: #dfd;
} }
.date-cell-event { .date-cell-event {
background: yellow; background: yellow;
} }
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin: 16px 0 8px;
}
.toolbar-label {
font-size: 16px;
font-weight: 500;
}
.pagination {
margin: 16px 0;
justify-content: flex-end;
}
</style> </style>
-226
View File
@@ -1,226 +0,0 @@
<template>
<el-table :data="dirContexts" @cell-mouse-enter="rowMouseEnter" @cell-mouse-leave="rowMouseLeave"
:row-style="handleRowStyle" @row-contextmenu="rightClickRow" @row-click="leftClickRow" ref="multipleTableRef">
<el-table-column prop="name" width="200px">
</el-table-column>
<el-table-column prop="isDir" width="130px">
</el-table-column>
<el-table-column prop="size" width="130px">
</el-table-column>
<el-table-column prop="path" width="130px">
</el-table-column>
</el-table>
<Contextmenu auto-ajust-placement ref="contextmenu">
<ContextmenuItem v-if="isSelect" @click="createFolder">
<el-icon class="contextmenu-icon">
<FolderOpened />
</el-icon>
<label> _new folder</label>
</ContextmenuItem>
<ContextmenuItem v-if="isSelect" @click="createFolder">
<el-icon class="contextmenu-icon">
<FolderOpened />
</el-icon>
<label> _copy</label>
</ContextmenuItem>
<ContextmenuItem v-if="isSelect" @click="createFolder">
<el-icon class="contextmenu-icon">
<FolderOpened />
</el-icon>
<label> _rename</label>
</ContextmenuItem>
<ContextmenuItem v-if="isSelect" @click="createFolder">
<el-icon class="contextmenu-icon">
<FolderOpened />
</el-icon>
<label> _delete</label>
</ContextmenuItem>
<ContextmenuItem v-if="isSelect" @click="createFolder">
<el-icon class="contextmenu-icon">
<FolderOpened />
</el-icon>
<label> _delete</label>
</ContextmenuItem>
</Contextmenu>
<!-- aplayer -->
</template>
<script setup lang="ts">
import { defineAsyncComponent, reactive, toRaw } from 'vue';
import { getFileList } from '../../api/cloud'
import { Contextmenu, ContextmenuDivider, ContextmenuItem } from "v-contextmenu";
import { computed, getCurrentInstance, onBeforeUnmount, onMounted, ref } from "vue";
import { ElTable, type TableInstance } from 'element-plus'
// -- IMPORT --
const currentInstance = getCurrentInstance()
// -- INTERFACE OR TYPE DEFINITION --
interface DirContext {
size: number
isDir: number
name: string
path: string
}
interface KeyBoard {
isCtrl: boolean
isShift: boolean
}
// -- REACTIVE OBJECT --
const dirContexts = reactive<DirContext[]>([])
const multipleSelection = reactive<Set<DirContext>>(new Set())
const keyBoard = reactive<KeyBoard>({
isCtrl: false,
isShift: false
})
// -- REF OBJECT --
const multipleTableRef = ref<TableInstance>()
const hoverUnSelection = ref<DirContext>()
const isSelect = ref(true)
const selection = ref()
const isRightClick = ref(false)
// -- KEYBOARD AND MOUSE EVENT --
const rowMouseEnter = (row: any, column: any, cell: any, event: any) => {
if (!multipleSelection.has(row)) {
hoverUnSelection.value = row
}
}
const rowMouseLeave = (row: any, column: any, cell: any, event: any) => {
if (!multipleSelection.has(row)) {
hoverUnSelection.value = undefined
}
}
const leftClickRow = (row: any, column: any, event: any) => {
// CTRL KEY + LEFT CLICK
if (keyBoard.isCtrl) {
if (multipleSelection.has(row)) {
multipleSelection.delete(row)
} else {
multipleSelection.add(row)
}
// ONLY LEFT CLICK
} else {
multipleSelection.clear()
multipleSelection.add(row)
}
}
const rightClickRow = (row: any, column: any, event: any) => {
isRightClick.value = true
let contextmenuRef: any = currentInstance?.proxy?.$refs.contextmenu
if (!multipleSelection.has(row)) {
multipleSelection.clear()
multipleSelection.add(row)
}
event.preventDefault()
contextmenuRef.show({
top: event.clientY,
left: event.clientX
})
window.onclick = () => {
contextmenuRef.hide()
isRightClick.value = false
// contextMenuTargetBlank.value = false;
// contextMenuTargetFile.value = false;
};
}
const createFolder = () => {
}
// -- STYLE --
const handleRowStyle = ({ row, rowIndex }: any) => {
if (multipleSelection.has(row)) {
return {
"background": "#B4C7E7"
}
}
if (toRaw(hoverUnSelection.value)?.name == toRaw(row).name) {
return {
"background": "#DAE3F3"
}
}
}
/**
* AUTO INVOKE FUNCTION
*/
(() => {
// MOUSE KEYDOWM EVENT
document.onkeydown = (event) => {
switch (event.key) {
case 'Control':
keyBoard.isCtrl = true
break;
}
}
// MOUSE KEYUP EVENT
document.onkeyup = (event) => {
switch (event.key) {
case 'Control':
keyBoard.isCtrl = false
break
case 'Escape':
if (isRightClick.value) {
let contextmenuRef: any = currentInstance?.proxy?.$refs.contextmenu
contextmenuRef.hide()
isRightClick.value = false
} else {
multipleSelection.clear()
}
keyBoard.isCtrl = true
break
}
}
// INITIALIZE DATA QUERY
getFileList("").then(response => {
response.data.forEach((element: any) => {
const dirContext: DirContext = {
size: element.size,
isDir: element.isDir,
name: element.server_filename,
path: element.path
}
dirContexts.push(dirContext)
})
})
})()
</script>
<style>
.el-table--enable-row-hover .el-table__body tr:hover>td {
background-color: transparent !important;
}
</style>
-4
View File
@@ -83,10 +83,6 @@ export default defineConfig(({ mode, command }) => {
target: 'http://localhost:8088', target: 'http://localhost:8088',
changeOrigin: true, changeOrigin: true,
}, },
'^/cloud/api/.*': {
target: 'http://localhost:8089',
changeOrigin: true,
},
'^/wiki/api/.*': { '^/wiki/api/.*': {
target: 'http://localhost:8090', target: 'http://localhost:8090',
changeOrigin: true, changeOrigin: true,