Merge feature/merge-upstream: Modal 小屏 + 视频播放 + Docker Alpine(冲突以 feature 为准)
部署到群晖 / deploy (push) Failing after 3h9m39s
部署到群晖 / deploy (push) Failing after 3h9m39s
# Conflicts: # Dockerfile # Dockerfile-arm # dto/VideoPageRequestDTO.cs # model/AppConfig.cs # repository/DyCollectVideoRepository.cs # service/CommonService.cs # service/DyCollectVideoService.cs # service/DyHttpClientService.cs
This commit is contained in:
+740
-319
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,431 @@
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, watch, UnwrapRef, onMounted } from 'vue';
|
||||
import { FormInstance } from 'ant-design-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { useApiStore, useAccountStore } from '@/store';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { ExclamationCircleOutlined, SyncOutlined, SaveOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
interface UpSecUserIdItem {
|
||||
uper?: string;
|
||||
uid?: string;
|
||||
syncAll: boolean;
|
||||
}
|
||||
|
||||
type ConfigItem = {
|
||||
id?: string;
|
||||
userName: string; // 改为非可选,强制初始化
|
||||
cookies: string;
|
||||
savePath: string;
|
||||
favSavePath: string;
|
||||
secUserId: string;
|
||||
status: number;
|
||||
upSavePath: string;
|
||||
// imgSavePath: string;
|
||||
useSinglePath: boolean; // 非可选
|
||||
};
|
||||
|
||||
// 强制初始化所有字段为非undefined值
|
||||
const newConfig = (config?: ConfigItem): ConfigItem => {
|
||||
return {
|
||||
id: '0',
|
||||
userName: '', // 强制空字符串
|
||||
cookies: '',
|
||||
savePath: '',
|
||||
favSavePath: '',
|
||||
secUserId: '',
|
||||
status: 0,
|
||||
upSavePath: '',
|
||||
// imgSavePath: '',
|
||||
useSinglePath: false,
|
||||
};
|
||||
};
|
||||
|
||||
// 改用 ref 而非 reactive(解决深层响应式问题)
|
||||
const form = ref<ConfigItem>(newConfig());
|
||||
const formModel = ref<FormInstance>();
|
||||
const formLoading = ref(false);
|
||||
|
||||
// 重置表单:强制重置 + 清空校验状态
|
||||
const resetConfig = () => {
|
||||
form.value = newConfig();
|
||||
formModel.value?.resetFields(); // 必须调用,清空校验提示
|
||||
message.info('表单已重置为默认值');
|
||||
};
|
||||
|
||||
// 监听统一路径(改用 ref 的取值方式)
|
||||
watch(
|
||||
[() => form.value.savePath, () => form.value.useSinglePath],
|
||||
([newSavePath, useSinglePath]) => {
|
||||
if (useSinglePath && newSavePath) {
|
||||
form.value.favSavePath = newSavePath;
|
||||
form.value.upSavePath = newSavePath;
|
||||
// form.value.imgSavePath = newSavePath;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 校验规则:明确trigger + 不依赖组件自动触发
|
||||
const formRules = reactive({
|
||||
userName: [{ required: true, message: '请输入Cookie名称', trigger: 'change' }],
|
||||
savePath: [{ required: true, message: '请输入收藏存储路径', trigger: 'change' }],
|
||||
secUserId: [{ required: false, message: '请输入我的secUserId', trigger: 'change' }],
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
// 定义rowCount并暴露到模板(之前定义在onMounted里,模板访问不到)
|
||||
const rowCount = ref(6);
|
||||
|
||||
onMounted(() => {
|
||||
const accountStore = useAccountStore(); // 移到内部避免提前引用
|
||||
useApiStore()
|
||||
.AppisInit()
|
||||
.then((res) => {
|
||||
if (res.code == 0 && res.data) {
|
||||
if (accountStore.logged) {
|
||||
router.push('/dashboard');
|
||||
} else {
|
||||
router.push('/login');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
|
||||
// 提交:手动指定校验字段 + 强制取值
|
||||
const submitConfig = async () => {
|
||||
formLoading.value = true;
|
||||
try {
|
||||
// 手动获取表单实例,防止null
|
||||
const formInstance = formModel.value;
|
||||
if (!formInstance) {
|
||||
message.error('表单实例未初始化');
|
||||
formLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 手动校验(兜底方案)
|
||||
const checkResult = manualCheckForm();
|
||||
if (!checkResult.pass) {
|
||||
message.error(checkResult.msg);
|
||||
formLoading.value = false;
|
||||
return;
|
||||
}
|
||||
// 提交时强制解构,确保传递最新值
|
||||
const res = await useApiStore().DeskInitAsync({ ...form.value });
|
||||
if (res.code === 0) {
|
||||
message.success('配置保存成功,即将跳转登录页...');
|
||||
setTimeout(() => {
|
||||
router.push('/login');
|
||||
}, 500);
|
||||
} else {
|
||||
message.error(`保存失败:${res.message || '未知错误'}`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('校验失败:', err);
|
||||
const firstErr = err.errorFields?.[0]?.errors?.[0] || '请检查所有必填项';
|
||||
message.error(firstErr);
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 手动校验函数(兜底方案)
|
||||
const manualCheckForm = (): { pass: boolean; msg: string } => {
|
||||
if (!form.value.userName.trim()) {
|
||||
return { pass: false, msg: '请输入Cookie名称' };
|
||||
}
|
||||
if (!form.value.savePath.trim()) {
|
||||
return { pass: false, msg: '请输入收藏存储路径' };
|
||||
}
|
||||
return { pass: true, msg: '' };
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="init-config-page">
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<div class="logo-container">
|
||||
<img src="/logo.png" alt="抖小云Logo" class="logo-img" />
|
||||
</div>
|
||||
<h1 class="page-title">抖小云初始化配置信息</h1>
|
||||
<!-- <p class="page-desc">第一次运行,请花两分钟完成初始配置</p> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-card">
|
||||
<a-form ref="formModel" :model="form" :rules="formRules" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" layout="horizontal" class="config-form" @submit.prevent="submitConfig">
|
||||
|
||||
<!-- Cookie名称 -->
|
||||
<a-form-item label="Cookie名" name="userName">
|
||||
<a-input v-model:value="form.userName" placeholder="随便填个名称,作为唯一标识" @input="() => {}" />
|
||||
</a-form-item>
|
||||
|
||||
<!-- Cookie值:现在rows能正常生效了 -->
|
||||
<a-form-item label="Cookie值" name="cookies">
|
||||
<div class="cookie-wrapper">
|
||||
<a-textarea v-model:value="form.cookies" :rows="rowCount" placeholder="粘贴完整Cookie值,也可以先不填,保存配置后,后续可以在“抖音授权”修改" />
|
||||
<div class="cookie-tip">
|
||||
<a href="https://gitee.com/deathvicky/dysync.net" target="_blank" class="cookie-link">
|
||||
查看Cookie以及secUserId获取方式
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- secUserId -->
|
||||
<a-form-item label="我的secUserId" name="secUserId">
|
||||
<div class="input-with-tip">
|
||||
<a-input v-model:value="form.secUserId" placeholder="同步喜欢视频和关注博主视频必填" @input="() => {}" />
|
||||
<a-tooltip title="同步喜欢视频和关注博主视频必填">
|
||||
<ExclamationCircleOutlined style="color: #faad14; margin-left: 8px; font-size: 16px;" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 收藏路径 -->
|
||||
<a-form-item label="收藏存储路径" name="savePath">
|
||||
<div class="input-with-tip">
|
||||
<a-input v-model:value="form.savePath" placeholder="/app/collect" @input="() => {}" />
|
||||
<a-tooltip title="必填">
|
||||
<ExclamationCircleOutlined style="color: #faad14;font-size: 16px; margin-left: 8px; " />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 统一路径开关 -->
|
||||
<a-form-item label="统一存储路径" name="useSinglePath">
|
||||
<div class="switch-with-desc">
|
||||
<a-switch v-model:checked="form.useSinglePath" class="custom-large-switch" @change="() => {}" />
|
||||
<div class="switch-desc">
|
||||
<span class="main-desc">{{ form.useSinglePath ? '开启' : '关闭' }}</span>
|
||||
<span class="sub-desc">{{ form.useSinglePath ? '所有类型视频(收藏的、喜欢的、关注博主的)共用一个路径' : '各类型视频(收藏的、喜欢的、关注博主的)文件路径独立配置' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<!-- 其他路径 -->
|
||||
<a-form-item v-if="!form.useSinglePath" label="喜欢存储路径" name="favSavePath">
|
||||
<a-input v-model:value="form.favSavePath" placeholder="喜欢视频存储路径,不想同步就空着,后续可以在“抖音授权”修改" @input="() => {}" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="!form.useSinglePath" label="关注存储路径" name="upSavePath">
|
||||
<a-input v-model:value="form.upSavePath" placeholder="关注视频存储路径,不想同步就空着,后续可以在“抖音授权”修改" @input="() => {}" />
|
||||
</a-form-item>
|
||||
|
||||
<!-- <a-form-item v-if="!form.useSinglePath" label="图文存储路径" name="imgSavePath">
|
||||
<a-input v-model:value="form.imgSavePath" placeholder="图文视频存储路径,不想同步就空着,后续可以在“抖音授权”修改" @input="() => {}" />
|
||||
</a-form-item> -->
|
||||
|
||||
<!-- 同步状态开关 -->
|
||||
<a-form-item label="同步状态" name="status">
|
||||
<div class="switch-with-desc">
|
||||
<a-switch v-model:checked="form.status" :checked-value="1" :un-checked-value="0" class="custom-large-switch" />
|
||||
<div class="switch-desc">
|
||||
<span class="main-desc">{{ form.status === 1 ? '开启' : '关闭' }}</span>
|
||||
<span class="sub-desc">建议初始不开启,保存设置后,进入系统配置完成其他设置后再手动开启!</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<a-form-item :wrapper-col="{ span: 24 }" class="form-actions">
|
||||
<div class="btn-center-wrapper">
|
||||
<a-button type="primary" html-type="submit" :loading="formLoading" class="save-btn">
|
||||
<template #icon>
|
||||
<SaveOutlined />
|
||||
</template>保存配置
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.init-config-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f7fa;
|
||||
padding: 10px 50px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.page-header {
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.header-content {
|
||||
text-align: center;
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
}
|
||||
.logo-container {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.logo-img {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: contain;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
.page-desc {
|
||||
font-size: 16px;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
.config-card {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
padding: 20px;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.config-form {
|
||||
--ant-form-item-margin-bottom: 24px;
|
||||
}
|
||||
.input-with-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.switch-with-desc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.switch-desc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.switch-desc .main-desc {
|
||||
font-size: 15px;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
.switch-desc .sub-desc {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
:deep(.custom-large-switch) {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
.form-actions {
|
||||
margin-top: 30px !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.btn-center-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 16px;
|
||||
}
|
||||
.save-btn {
|
||||
padding: 0 40px;
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
background-color: #1677ff;
|
||||
border-color: #1677ff;
|
||||
}
|
||||
.save-btn:hover {
|
||||
background-color: #4096ff;
|
||||
border-color: #4096ff;
|
||||
}
|
||||
|
||||
/* 文本域恢复默认,让rows生效 */
|
||||
:deep(.ant-input-textarea) {
|
||||
font-size: 15px;
|
||||
height: auto !important; /* 取消固定高度 */
|
||||
min-height: auto !important;
|
||||
}
|
||||
|
||||
/* Cookie容器和链接样式 */
|
||||
.cookie-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
.cookie-tip {
|
||||
text-align: left;
|
||||
}
|
||||
.cookie-link {
|
||||
color: #1677ff;
|
||||
font-size: 13px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.cookie-link:hover {
|
||||
color: #4096ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.init-config-page {
|
||||
padding: 10px 16px;
|
||||
}
|
||||
.config-card {
|
||||
padding: 20px;
|
||||
}
|
||||
.logo-img {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
.switch-with-desc {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.switch-desc {
|
||||
gap: 4px;
|
||||
}
|
||||
:deep(.custom-large-switch) {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
:deep(.ant-input-textarea-input) {
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(150, 150, 150, 0.2) transparent;
|
||||
}
|
||||
:deep(.ant-input-textarea-input)::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
:deep(.ant-input-textarea-input)::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
:deep(.ant-input-textarea-input)::-webkit-scrollbar-thumb {
|
||||
background: rgba(150, 150, 150, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
:deep(.ant-input-disabled) {
|
||||
background-color: #f9fafb !important;
|
||||
color: #6b7280 !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,129 +0,0 @@
|
||||
<template>
|
||||
<a-card :bordered="false" :bodyStyle='{"padding-top":"0px","padding-bottom":"100px"}'>
|
||||
<a-form :model="formState" :label-col="labelCol" :rules="rules" :wrapper-col="wrapperCol" ref="formRef">
|
||||
<a-divider orientation="left"></a-divider>
|
||||
|
||||
<a-form-item has-feedback label="同步周期(分钟)" ref="Cron" name="Cron">
|
||||
<a-input v-model:value="formState.Cron" placeholder="1:数字-例如20-表示20分钟执行一次;2:cron表达式,根据表达式周期执行" />
|
||||
</a-form-item>
|
||||
<a-form-item label="在线Cron表达式">
|
||||
<a target="_blank" href="https://www.bejson.com/othertools/cron/">查看示例</a>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :wrapper-col="{ span: 10, offset: 3 }">
|
||||
<a-button type="primary" danger @click="onSubmit">进入系统</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, toRaw, ref, watch, createVNode, h } from 'vue';
|
||||
import type { UnwrapRef } from 'vue';
|
||||
import { Form } from 'ant-design-vue';
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import type { FormInstance } from 'ant-design-vue';
|
||||
import { useApiStore } from '@/store';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import { checkDomain, checksubDomainPrefix, checkPass, checkUserName } from '@/utils/regexHelper';
|
||||
const formRef = ref<FormInstance>();
|
||||
const SK = ref(null);
|
||||
interface FormState {
|
||||
cloudName: string;
|
||||
domainName: string;
|
||||
recordType: string;
|
||||
ipv6Prefix: string;
|
||||
domainRecord: string;
|
||||
AK: string;
|
||||
SK: string;
|
||||
DbType: number;
|
||||
ConnString: string;
|
||||
Cron: string;
|
||||
UserName: string;
|
||||
UswePwd: string;
|
||||
}
|
||||
interface CloudInfo {
|
||||
key: string;
|
||||
value: string;
|
||||
doc: string;
|
||||
}
|
||||
interface DbTypeInfo {
|
||||
key: number;
|
||||
value: string;
|
||||
conn: string;
|
||||
}
|
||||
interface showDBconn {
|
||||
value: boolean;
|
||||
}
|
||||
|
||||
const showDBconn: UnwrapRef<showDBconn> = reactive({
|
||||
value: false,
|
||||
});
|
||||
|
||||
const formState: UnwrapRef<FormState> = reactive({
|
||||
cloudName: 'aliyun',
|
||||
domainName: '',
|
||||
recordType: '',
|
||||
ipv6Prefix: '',
|
||||
domainRecord: '',
|
||||
AK: '',
|
||||
SK: '',
|
||||
DbType: 2,
|
||||
ConnString: '',
|
||||
Cron: '30',
|
||||
UserName: '',
|
||||
UswePwd: '',
|
||||
});
|
||||
|
||||
const rules: Record<string, Rule[]> = {
|
||||
Cron: [{ required: true, message: '请输入任务调度周期', trigger: 'change' }],
|
||||
};
|
||||
|
||||
watch(
|
||||
() => formState.DbType,
|
||||
() => {
|
||||
formRef.value.validateFields(['ConnString']);
|
||||
},
|
||||
{ flush: 'post' }
|
||||
);
|
||||
|
||||
//检查数据库连接
|
||||
import { message } from 'ant-design-vue';
|
||||
import router from '@/router';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
const isFirst = ref(false);
|
||||
|
||||
//提交初始化
|
||||
|
||||
const onSubmit = () => {
|
||||
// console.log('submit!', toRaw(formState));
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
console.log('values', formState, toRaw(formState));
|
||||
useApiStore()
|
||||
.apiInit(toRaw(formState))
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
message.success('初始化成功');
|
||||
setTimeout(() => {
|
||||
router.push('/login');
|
||||
}, 1000);
|
||||
} else {
|
||||
message.error(res.erro, 8);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('error', error);
|
||||
});
|
||||
};
|
||||
|
||||
const labelCol = { style: { width: '150px' } };
|
||||
const wrapperCol = { span: 4 };
|
||||
</script>
|
||||
<style scoped>
|
||||
.ant-card-body {
|
||||
padding: 5px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import Init from './Init.vue';
|
||||
|
||||
export default Init;
|
||||
+289
-22
@@ -3,54 +3,321 @@
|
||||
<login-box class="shadow-lg" @success="onLoginSuccess" @failure="onLoginFail" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import LoginBox from './LoginBox.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
// import http from '@/store/http';
|
||||
import { message, notification, Modal } from 'ant-design-vue'; // 引入 Modal 用于确认提示
|
||||
import { onMounted, ref, h } from 'vue';
|
||||
import { useApiStore } from '@/store';
|
||||
import { CopyOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function onLoginSuccess() {
|
||||
router.push('/dashboard');
|
||||
}
|
||||
import { message } from 'ant-design-vue';
|
||||
// 版本数据存储
|
||||
const currentTag = ref<string>('');
|
||||
const latestTag = ref<string>('未知版本');
|
||||
|
||||
function onLoginFail(status, res) {
|
||||
console.log(res);
|
||||
if (res.code === 0) {
|
||||
} else {
|
||||
message.error(res.erro, 5);
|
||||
// 复制版本号到剪贴板核心逻辑
|
||||
const copyToClipboard = async (content: string, type: string) => {
|
||||
if (!content || content === '未知版本') {
|
||||
message.warning(`${type}无有效内容可复制`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(content);
|
||||
message.success(`${type}已复制到剪贴板 ✅`);
|
||||
} else {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = content;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.left = '-9999px';
|
||||
textarea.style.top = '-9999px';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
message.success(`${type}已复制到剪贴板 ✅`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err);
|
||||
message.error('复制失败,请手动选中复制');
|
||||
}
|
||||
};
|
||||
|
||||
// 统一的关闭处理:标记缓存 + 关闭通知
|
||||
const handleNoticeClose = (noticeKey: string) => {
|
||||
console.log('版本提醒通知已关闭,后续不再提醒');
|
||||
// 标记为已提醒,存入缓存(确保版本通知和确认弹窗都不再出现)
|
||||
localStorage.setItem('maintain_notice_shown', 'true');
|
||||
// 关闭版本通知
|
||||
notification.close(noticeKey);
|
||||
};
|
||||
|
||||
// 关闭前的确认提示弹窗
|
||||
const showCloseConfirm = (noticeKey: string) => {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '关闭后该提醒将不再弹出,确定要关闭吗?',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
// 确认关闭:执行统一处理
|
||||
handleNoticeClose(noticeKey);
|
||||
message.success('提醒已关闭,后续不再弹出');
|
||||
},
|
||||
onCancel: () => {
|
||||
// 取消关闭:不执行任何操作,保留版本通知
|
||||
message.info('已取消关闭');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 打开右上角 notification 通知
|
||||
const openVersionNotice = () => {
|
||||
const noticeKey = `version_notice_${Date.now()}`;
|
||||
notification.open({
|
||||
message: '温馨提示一下',
|
||||
key: noticeKey,
|
||||
duration: 0,
|
||||
placement: 'topRight',
|
||||
// 通知描述内容
|
||||
description: h('div', { class: 'notice-content' }, [
|
||||
h('p', { class: 'notice-desc' }, '当前您使用的docker镜像为阿里云镜像,已停止维护。'),
|
||||
h('p', { class: 'notice-version-item' }, [
|
||||
h('strong', { class: 'notice-version-label' }, '当前版本:'),
|
||||
h('span', { style: { color: '#ff4d4f', fontWeight: '500' } }, currentTag.value.replace('[不再维护]', '')),
|
||||
h(CopyOutlined, {
|
||||
class: 'notice-copy-icon',
|
||||
title: '复制当前版本',
|
||||
onClick: () => copyToClipboard(currentTag.value.replace('[不再维护]', ''), '当前版本'),
|
||||
}),
|
||||
]),
|
||||
h('p', { class: 'notice-version-item' }, [
|
||||
h('strong', { class: 'notice-version-label' }, '最新版本:'),
|
||||
h('span', { style: { color: '#52c41a', fontWeight: '500' } }, latestTag.value),
|
||||
h(CopyOutlined, {
|
||||
class: 'notice-copy-icon',
|
||||
title: '复制最新版本',
|
||||
onClick: () => copyToClipboard(latestTag.value, '最新版本'),
|
||||
}),
|
||||
]),
|
||||
h('p', { class: 'notice-tip' }, '建议升级到最新版本!'),
|
||||
]),
|
||||
// 「我已知晓」按钮:点击触发确认弹窗
|
||||
btn: () =>
|
||||
h(
|
||||
'button',
|
||||
{
|
||||
class: 'notice-confirm-btn',
|
||||
onClick: () => {
|
||||
// 不直接关闭,先弹出确认提示
|
||||
showCloseConfirm(noticeKey);
|
||||
},
|
||||
},
|
||||
'我已知晓'
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
useApiStore()
|
||||
.AppisInit()
|
||||
.then((res) => {
|
||||
if (res.code == 0 && res.data) {
|
||||
} else {
|
||||
router.push('/init');
|
||||
}
|
||||
});
|
||||
|
||||
useApiStore()
|
||||
.CheckTag()
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
if (res.data.length > 0) {
|
||||
const tag = res.data[0];
|
||||
if (tag.indexOf('不再维护') !== -1) {
|
||||
const hasShown = localStorage.getItem('maintain_notice_shown');
|
||||
if (!hasShown) {
|
||||
currentTag.value = tag;
|
||||
latestTag.value = res.data.length >= 2 ? res.data[1] : '未知版本';
|
||||
openVersionNotice();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message.error(res.message);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
function onLoginSuccess() {
|
||||
if (isMobileBrowser()) router.push('/mobile');
|
||||
else router.push('/dashboard');
|
||||
}
|
||||
|
||||
const isMobileBrowser = (): boolean => {
|
||||
if (typeof navigator === 'undefined' || typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
const mobileUA = /android|iphone|ipod|blackberry|windows phone|iemobile|opera mini/i.test(userAgent);
|
||||
const isTablet = /ipad|tablet|playbook|kindle|android 3\.|android 4\.[0-3]/.test(userAgent);
|
||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
const isMobileScreen = window.innerWidth <= 768 && window.innerHeight <= 1024;
|
||||
|
||||
return (mobileUA && !isTablet && isTouchDevice) || (isMobileScreen && isTouchDevice);
|
||||
};
|
||||
|
||||
function onLoginFail(reason: string, fields: any) {
|
||||
console.log('登录失败:', reason, fields);
|
||||
message.error(reason || '登录失败,请重试', 5);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.login {
|
||||
height: 100vh;
|
||||
// 与深色登录框搭配的渐变背景
|
||||
min-height: -webkit-fill-available;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 20px 0;
|
||||
|
||||
// 可以添加一些装饰性背景元素
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
border-radius: 50%;
|
||||
background: rgba(54, 191, 250, 0.1);
|
||||
top: 20%;
|
||||
left: 15%;
|
||||
filter: blur(80px);
|
||||
top: 15%;
|
||||
left: 5%;
|
||||
filter: blur(60px);
|
||||
@media (min-width: 768px) {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
top: 20%;
|
||||
left: 15%;
|
||||
filter: blur(80px);
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
border-radius: 50%;
|
||||
background: rgba(54, 191, 250, 0.08);
|
||||
bottom: 10%;
|
||||
right: 10%;
|
||||
filter: blur(60px);
|
||||
bottom: 5%;
|
||||
right: 5%;
|
||||
filter: blur(40px);
|
||||
@media (min-width: 768px) {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
bottom: 10%;
|
||||
right: 10%;
|
||||
filter: blur(60px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@supports (bottom: env(safe-area-inset-bottom)) {
|
||||
.login {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-top: env(safe-area-inset-top);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.ant-notification {
|
||||
.ant-notification-notice {
|
||||
width: 500px !important;
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.notice-content {
|
||||
font-size: 14px !important;
|
||||
line-height: 1.8 !important;
|
||||
color: #334155 !important;
|
||||
width: 480px !important;
|
||||
|
||||
.notice-desc {
|
||||
margin-bottom: 12px !important;
|
||||
padding-left: 2px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.notice-version-item {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
margin: 8px 0 !important;
|
||||
padding: 6px 10px !important;
|
||||
background-color: #f8fafc !important;
|
||||
border-radius: 6px !important;
|
||||
transition: background-color 0.2s ease !important;
|
||||
|
||||
&:hover {
|
||||
background-color: #f1f5f9 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-version-label {
|
||||
color: #1e293b !important;
|
||||
width: 70px !important;
|
||||
flex-shrink: 0 !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
.notice-copy-icon {
|
||||
flex-shrink: 0 !important;
|
||||
color: #165dff !important;
|
||||
font-size: 16px !important;
|
||||
cursor: pointer !important;
|
||||
transition: all 0.2s ease !important;
|
||||
margin-left: 8px !important;
|
||||
|
||||
&:hover {
|
||||
color: #0d47a1 !important;
|
||||
transform: scale(1.1) !important;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-tip {
|
||||
margin-top: 12px !important;
|
||||
color: #64748b !important;
|
||||
padding-left: 2px !important;
|
||||
font-style: italic !important;
|
||||
font-size: 13px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-confirm-btn {
|
||||
background-color: #165dff !important;
|
||||
color: #ffffff !important;
|
||||
border: none !important;
|
||||
border-radius: 4px !important;
|
||||
padding: 4px 12px !important;
|
||||
font-size: 13px !important;
|
||||
cursor: pointer !important;
|
||||
transition: background-color 0.2s ease !important;
|
||||
margin-top: 12px !important;
|
||||
|
||||
&:hover {
|
||||
background-color: #0d47a1 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+206
-101
@@ -1,78 +1,103 @@
|
||||
<template>
|
||||
<div class="login-page min-h-screen flex items-center justify-center p-4 md:p-8">
|
||||
<ThemeProvider :color="{
|
||||
middle: { 'bg-base': '#f8fafc' },
|
||||
primary: { DEFAULT: '#4f46e5', hover: '#4338ca' }
|
||||
}">
|
||||
<!-- 登录卡片 -->
|
||||
<div class="login-box rounded-2xl shadow-lg p-8 md:p-10 max-w-md w-full border border-gray-200 transition-all duration-500 hover:shadow-xl transform hover:-translate-y-1">
|
||||
<!-- 顶部Logo区域 -->
|
||||
<div class="flex flex-col items-center mb-8">
|
||||
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mb-4">
|
||||
<img src="/public/logo.png">
|
||||
<ThemeProvider :color="{
|
||||
middle: { 'bg-base': '#fff' },
|
||||
primary: { DEFAULT: '#4f46e5', hover: '#4338ca' }
|
||||
}">
|
||||
<!-- 登录卡片 - 移动端全屏 + PC端保留原宽度 -->
|
||||
<div class="login-box rounded-2xl shadow-lg p-6 md:p-8 lg:p-10 max-w-md w-full border border-gray-200 transition-all duration-500 hover:shadow-xl transform hover:-translate-y-1 mx-auto" style="box-sizing: border-box; max-width: 90vw;">
|
||||
<!-- 顶部Logo区域 - 边框圈精准贴合Logo内容 -->
|
||||
<div class="flex flex-col items-center mb-6 md:mb-8">
|
||||
<div class="w-30 h-30 md:w-36 md:h-36 bg-primary/10 rounded-full flex items-center justify-center mb-4 md:mb-5">
|
||||
<div class="w-24 h-24 md:w-28 h-28 rounded-full flex items-center justify-center">
|
||||
<img src="/logo.png" alt="抖小云logo" class="w-18 h-18 md:w-20 h-20 object-contain" />
|
||||
</div>
|
||||
<h2 class="third-title text-2xl font-bold text-gray-800 tracking-tight">抖音同步小帮手</h2>
|
||||
</div>
|
||||
<h2 class="third-title text-xl md:text-2xl font-bold text-gray-800 tracking-tight">抖小云</h2>
|
||||
</div>
|
||||
|
||||
<a-form :model="form" :wrapperCol="{ span: 24 }" @finish="login" class="login-form w-full text-gray-700">
|
||||
<!-- 用户名输入框 -->
|
||||
<a-form-item :required="true" name="username" class="mb-4 md:mb-5" :validate-status="usernameStatus">
|
||||
<a-input v-model:value="form.username" autocomplete="new-username" placeholder="请输入用户名" class="login-input h-[46px] md:h-[50px] rounded-lg bg-gray-50 border-gray-200 text-gray-800 placeholder:text-gray-400 focus:border-primary focus:bg-white text-base transition-all duration-300 focus:ring-2 focus:ring-primary/20" style="padding: 0 12px;">
|
||||
<template #prefix>
|
||||
<svg class="w-4 h-4 md:w-5 md:h-5 text-gray-400 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<a-form-item :required="true" name="password" class="mb-2" :validate-status="passwordStatus">
|
||||
<a-input v-model:value="form.password" autocomplete="new-password" placeholder="请输入密码" class="login-input h-[46px] md:h-[50px] rounded-lg bg-gray-50 border-gray-200 text-gray-800 placeholder:text-gray-400 focus:border-primary focus:bg-white text-base transition-all duration-300 focus:ring-2 focus:ring-primary/20" :type="showPassword ? 'text' : 'password'" style="padding: 0 12px;">
|
||||
<template #prefix>
|
||||
<svg class="w-4 h-4 md:w-5 md:h-5 text-gray-400 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||
</svg>
|
||||
</template>
|
||||
<!-- 密码显示/隐藏图标 -->
|
||||
<template #suffix>
|
||||
<div class="w-8 h-8 flex items-center justify-center" @click="showPassword = !showPassword">
|
||||
<EyeOutlined v-if="showPassword" class="text-gray-400 hover:text-primary cursor-pointer transition-colors duration-300 text-lg" aria-label="隐藏密码" />
|
||||
<EyeInvisibleOutlined v-else class="text-gray-400 hover:text-primary cursor-pointer transition-colors duration-300 text-lg" aria-label="显示密码" />
|
||||
</div>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 记住密码 -->
|
||||
<div class="flex items-center justify-between mb-5 md:mb-6 px-1">
|
||||
<a-form-item class="mb-0">
|
||||
<a-checkbox v-model:checked="rememberPassword" class="text-gray-600 hover:text-primary transition-colors duration-300 cursor-pointer">
|
||||
<span class="text-xs md:text-sm">记住密码</span>
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item class="mb-0">
|
||||
<div class="text-gray-600 hover:text-primary transition-colors duration-300 cursor-pointer" @click="handleForgotPassword">
|
||||
<span class="text-xs md:text-sm">忘记密码</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<a-form :model="form" :wrapperCol="{ span: 24 }" @finish="login" class="login-form w-full text-gray-700">
|
||||
<!-- 用户名输入框 -->
|
||||
<a-form-item :required="true" name="username" class="mb-5" :validate-status="usernameStatus">
|
||||
<a-input v-model:value="form.username" autocomplete="new-username" placeholder="请输入用户名" class="login-input h-[50px] rounded-lg bg-gray-50 border-gray-200 text-gray-800 placeholder:text-gray-400 focus:border-primary focus:bg-white text-base transition-all duration-300 focus:ring-2 focus:ring-primary/20">
|
||||
<template #prefix>
|
||||
<svg class="w-5 h-5 text-gray-400 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<!-- 登录按钮 -->
|
||||
<a-button htmlType="submit" class="h-[48px] md:h-[52px] w-full rounded-lg transition-all duration-300 hover:bg-primary/90 bg-primary border-primary text-white text-base font-medium shadow-md hover:shadow-lg transform hover:-translate-y-0.5 active:translate-y-0" type="primary" :loading="loading" style="touch-action: manipulation; -webkit-tap-highlight-color: transparent;">
|
||||
<span v-if="!loading">登录</span>
|
||||
<span v-else>登录中...</span>
|
||||
</a-button>
|
||||
</a-form>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<a-form-item :required="true" name="password" class="mb-2" :validate-status="passwordStatus">
|
||||
<a-input v-model:value="form.password" autocomplete="new-password" placeholder="请输入密码" class="login-input h-[50px] rounded-lg bg-gray-50 border-gray-200 text-gray-800 placeholder:text-gray-400 focus:border-primary focus:bg-white text-base transition-all duration-300 focus:ring-2 focus:ring-primary/20" :type="showPassword ? 'text' : 'password'">
|
||||
<template #prefix>
|
||||
<svg class="w-5 h-5 text-gray-400 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||
</svg>
|
||||
</template>
|
||||
<!-- 密码显示/隐藏图标 -->
|
||||
<template #suffix>
|
||||
<EyeOutlined v-if="showPassword" @click="showPassword = !showPassword" class="text-gray-400 hover:text-primary cursor-pointer transition-colors duration-300 text-lg" aria-label="隐藏密码" />
|
||||
<EyeInvisibleOutlined v-else @click="showPassword = !showPassword" class="text-gray-400 hover:text-primary cursor-pointer transition-colors duration-300 text-lg" aria-label="显示密码" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 记住密码与忘记密码 -->
|
||||
<div class="flex items-center justify-between mb-6 px-1">
|
||||
<a-form-item class="mb-0">
|
||||
<a-checkbox v-model:checked="rememberPassword" class="text-gray-600 hover:text-primary transition-colors duration-300 cursor-pointer">
|
||||
<span class="text-sm">记住密码</span>
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
<!-- <a href="#" class="text-primary hover:text-primary/80 text-sm font-medium transition-colors duration-300" @click.prevent="handleForgotPassword">
|
||||
忘记密码?
|
||||
</a> -->
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<a-button htmlType="submit" class="h-[52px] w-full rounded-lg transition-all duration-300 hover:bg-primary/90 bg-primary border-primary text-white text-base font-medium shadow-md hover:shadow-lg transform hover:-translate-y-0.5 active:translate-y-0" type="primary" :loading="loading">
|
||||
<span v-if="!loading">登录</span>
|
||||
<span v-else>登录中...</span>
|
||||
</a-button>
|
||||
|
||||
<!-- 注册入口 -->
|
||||
<!-- <div class="text-center mt-6 text-gray-500 text-sm">
|
||||
还没有账号?
|
||||
<a href="#" class="text-primary hover:text-primary/80 font-medium transition-colors duration-300 ml-1" @click.prevent="handleRegister">
|
||||
立即注册
|
||||
</a>
|
||||
</div> -->
|
||||
</a-form>
|
||||
<!-- 忘记密码弹窗 -->
|
||||
<a-modal v-model:visible="forgotModalVisible" title="密码重置教程" width="50%" :max-width="500" centered :mask-closable="false" :footer="null" class="forgot-modal">
|
||||
<div class="p-2">
|
||||
<!-- 步骤说明 -->
|
||||
<div class="space-y-4 mb-6">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-primary text-white flex items-center justify-center text-sm mr-3 mt-0.5">1</div>
|
||||
<div class="flex-1 text-gray-700 text-sm leading-relaxed">在 db 目录下新建一个文本文件,命名为 <span class="px-2 py-0.5 bg-gray-100 text-primary font-medium rounded text-sm">pwd.txt</span></div>
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-primary text-white flex items-center justify-center text-sm mr-3 mt-0.5">2</div>
|
||||
<div class="flex-1 text-gray-700 text-sm leading-relaxed">打开文件写入你要设置的新密码,留空则自动重置为默认密码:<span class="px-2 py-0.5 bg-gray-100 text-primary font-medium rounded text-sm">douyin2026</span></div>
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-primary text-white flex items-center justify-center text-sm mr-3 mt-0.5">3</div>
|
||||
<div class="flex-1 text-gray-700 text-sm leading-relaxed">重启 docker服务或者飞牛安装的抖小云应用 ,使用新密码登录即可</div>
|
||||
</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</div>
|
||||
|
||||
<!-- 知道了按钮 -->
|
||||
<div class="flex justify-center">
|
||||
<a-button type="primary" class="h-10 px-8 rounded-lg" @click="forgotModalVisible = false">
|
||||
我知道了
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -88,8 +113,14 @@ const showPassword = ref(false);
|
||||
const rememberPassword = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
// 忘记密码弹窗显示状态
|
||||
const forgotModalVisible = ref(false);
|
||||
|
||||
// 表单状态(用于输入框验证反馈)
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
const usernameStatus = computed(() => (form.username ? 'success' : ''));
|
||||
const passwordStatus = computed(() => (form.password ? 'success' : ''));
|
||||
|
||||
@@ -98,11 +129,6 @@ export interface LoginFormProps {
|
||||
password: string;
|
||||
}
|
||||
|
||||
const form = reactive<LoginFormProps>({
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'success', fields: LoginFormProps): void;
|
||||
(e: 'failure', reason: string, fields: LoginFormProps): void;
|
||||
@@ -158,19 +184,20 @@ async function login(params: LoginFormProps) {
|
||||
try {
|
||||
const res = await accountStore.login(params.username, params.password);
|
||||
emit('success', params);
|
||||
console.log(res);
|
||||
message.success('登录成功!');
|
||||
} catch (e: any) {
|
||||
// emit('failure', e.message, e.data);
|
||||
message.error(e.data.erro || '登录失败,请重试');
|
||||
emit('failure', e.message || '登录失败', params);
|
||||
message.error(e.data?.erro || '登录失败,请重试');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 忘记密码处理
|
||||
// 忘记密码处理 - 打开弹窗
|
||||
function handleForgotPassword() {
|
||||
forgotModalVisible.value = true;
|
||||
emit('forgot-password');
|
||||
message.info('忘记密码功能即将上线');
|
||||
}
|
||||
|
||||
// 注册处理
|
||||
@@ -180,15 +207,85 @@ function handleRegister() {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 页面基础样式 */
|
||||
.login-page {
|
||||
/* 全局布局适配 - 替代原外层容器的样式 */
|
||||
:root {
|
||||
-webkit-text-size-adjust: 100%; /* 禁止iOS文本缩放 */
|
||||
-webkit-font-smoothing: antialiased; /* 优化字体渲染 */
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
min-height: 100vh;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
/* 全局居中布局 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* 登录卡片核心样式 */
|
||||
.login-box {
|
||||
box-sizing: border-box;
|
||||
touch-action: manipulation; /* 优化触摸性能 */
|
||||
/* 移动端全屏,PC端保留原max-w-md(默认28rem/448px)+ 90vw限制 */
|
||||
}
|
||||
|
||||
/* 移动端适配(<768px) */
|
||||
@media (max-width: 767.98px) {
|
||||
body {
|
||||
padding: 0;
|
||||
}
|
||||
.login-box {
|
||||
max-width: 100vw !important; /* 移动端全屏 */
|
||||
min-height: 100vh;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
padding: 20px 16px !important;
|
||||
padding-top: 20vh !important; /* 移动端垂直居中 */
|
||||
}
|
||||
.third-title {
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
::v-deep(.ant-input) {
|
||||
font-size: 14px; /* 移动端字体适配 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 小屏手机额外适配 */
|
||||
@media (max-width: 480px) {
|
||||
.login-box {
|
||||
padding-top: 15vh !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* PC端样式(保留原有宽度,仅调小最大宽度) */
|
||||
@media (min-width: 768px) {
|
||||
body {
|
||||
padding: 8px;
|
||||
}
|
||||
.login-box {
|
||||
/* 原宽度是 min(448px, 90vw),现调整为更小的数值,比如 380px(可根据需求修改) */
|
||||
max-width: min(340px, 90vw) !important;
|
||||
min-height: auto !important;
|
||||
border: 1px solid #e5e7eb !important;
|
||||
border-radius: 1rem !important;
|
||||
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1) !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 输入框聚焦动画优化 */
|
||||
::v-deep(.ant-input) {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
::v-deep(.ant-input:focus) {
|
||||
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2) !important;
|
||||
border-color: #4f46e5 !important;
|
||||
outline: none; /* 移除默认轮廓 */
|
||||
}
|
||||
|
||||
/* 复选框样式优化 */
|
||||
@@ -196,29 +293,26 @@ function handleRegister() {
|
||||
background-color: #4f46e5 !important;
|
||||
border-color: #4f46e5 !important;
|
||||
}
|
||||
|
||||
::v-deep(.ant-checkbox:hover .ant-checkbox-inner) {
|
||||
border-color: #4f46e5 !important;
|
||||
}
|
||||
::v-deep(.ant-checkbox) {
|
||||
transform: scale(0.9); /* 移动端复选框适当缩小 */
|
||||
}
|
||||
|
||||
/* 按钮样式优化 */
|
||||
::v-deep(.ant-btn-primary) {
|
||||
background-color: #4f46e5 !important;
|
||||
border-color: #4f46e5 !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
::v-deep(.ant-btn-primary:hover) {
|
||||
background-color: #4338ca !important;
|
||||
border-color: #4338ca !important;
|
||||
}
|
||||
|
||||
::v-deep(.ant-btn-primary:focus) {
|
||||
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.3) !important;
|
||||
}
|
||||
|
||||
/* 分割线样式优化 */
|
||||
::v-deep(.ant-divider) {
|
||||
background-color: #e5e7eb !important;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* 加载状态动画优化 */
|
||||
@@ -226,24 +320,35 @@ function handleRegister() {
|
||||
margin-right: 8px !important;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 375px) {
|
||||
.login-box {
|
||||
padding: 60px 20px !important;
|
||||
}
|
||||
/* 弹窗样式美化 */
|
||||
::v-deep(.forgot-modal .ant-modal-header) {
|
||||
border-radius: 12px 12px 0 0;
|
||||
background: #f9fafb;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
::v-deep(.forgot-modal .ant-modal-title) {
|
||||
color: #1f2937;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
::v-deep(.forgot-modal .ant-modal-body) {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
::v-deep(.forgot-modal .ant-modal-content) {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.third-title {
|
||||
font-size: 1.5rem !important;
|
||||
}
|
||||
/* 防止点击闪烁和触摸高亮 */
|
||||
* {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* 平滑滚动 */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* 防止点击闪烁 */
|
||||
* {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
overflow-x: hidden; /* 防止移动端横向滚动 */
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
+254
-40
@@ -1,79 +1,293 @@
|
||||
<template>
|
||||
<a-form layout="inline" style="margin-top:5px;margin-bottom:5px;">
|
||||
<a-form layout="inline" style="margin-top:5px;margin-bottom:5px;align-items: center;">
|
||||
|
||||
<!-- 日期控制 -->
|
||||
<a-form-item>
|
||||
<a-date-picker v-model:value="dateValue" format="YYYYMMDD" :locale="locale" @change="datePickChange" />
|
||||
<div class="date-control-group">
|
||||
<a-button type="text" @click="handleDateMinus" class="date-btn"><left-outlined /></a-button>
|
||||
<span class="current-date-text">{{ currentDateText }}</span>
|
||||
<a-button type="text" @click="handleDatePlus" class="date-btn" :disabled="isToday"><right-outlined /></a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<!-- debug/error单选按钮 -->
|
||||
<a-form-item>
|
||||
<a-radio-group v-model:value="typeValue" button-style="solid" @change="typeChange">
|
||||
<a-radio-group class="log-type-radio-group" v-model:value="typeValue" @change="typeChange" button-style="solid">
|
||||
<a-radio-button value="debug">debug</a-radio-button>
|
||||
<a-radio-button value="error">error</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<!-- 复制按钮 - 同一行最右侧 -->
|
||||
<a-form-item style="margin-left: auto;">
|
||||
<a-button type="text" size="small" @click="copyLogs" class="copy-btn">
|
||||
<copy-outlined />
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<div class="container">
|
||||
<a-card title="" :bordered="true">
|
||||
<pre>{{ logs }}</pre>
|
||||
<a-card title="" :bordered="true" :class="{ 'log-error': typeValue === 'error' }">
|
||||
<pre class="card-width-pre">{{ logs }}</pre>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, reactive, ref, watch, onMounted } from 'vue';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useApiStore } from '@/store';
|
||||
import type { UnwrapRef } from 'vue';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import locale from 'ant-design-vue/es/date-picker/locale/zh_CN';
|
||||
type RangeValue = [Dayjs, Dayjs];
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
dayjs.locale('zh-cn');
|
||||
const dateValue = ref<Dayjs>(dayjs(Date()));
|
||||
|
||||
const currentDate = ref<Dayjs>(dayjs());
|
||||
const typeValue = ref<string>('debug');
|
||||
const iframeUrl = ref<string>('');
|
||||
const dateValue1 = ref<string>();
|
||||
const logs = ref<string>('');
|
||||
dateValue1.value = dayjs(Date()).format('YYYYMMDD');
|
||||
iframeUrl.value = `type=${typeValue.value}&date=${dateValue1.value}`;
|
||||
const datePickChange = (e, dateStr) => {
|
||||
dateValue1.value = dateStr;
|
||||
iframeUrl.value = `type=${typeValue.value}&date=${dateValue1.value}`;
|
||||
console.log(iframeUrl);
|
||||
|
||||
const currentDateText = computed(() => {
|
||||
return currentDate.value.format('YYYYMMDD');
|
||||
});
|
||||
|
||||
const isToday = computed(() => {
|
||||
return currentDate.value.isSame(dayjs(), 'day');
|
||||
});
|
||||
|
||||
const handleDateMinus = () => {
|
||||
currentDate.value = currentDate.value.subtract(1, 'day');
|
||||
updateIframeUrlAndLoadLogs();
|
||||
};
|
||||
|
||||
const handleDatePlus = () => {
|
||||
if (isToday.value) return;
|
||||
currentDate.value = currentDate.value.add(1, 'day');
|
||||
updateIframeUrlAndLoadLogs();
|
||||
};
|
||||
|
||||
const updateIframeUrlAndLoadLogs = () => {
|
||||
iframeUrl.value = `${typeValue.value}/${currentDateText.value}`;
|
||||
loadLogs();
|
||||
};
|
||||
|
||||
const typeChange = (e) => {
|
||||
console.log(e.target);
|
||||
iframeUrl.value = `type=${e.target.value}&date=${dateValue1.value}`;
|
||||
loadLogs();
|
||||
const typeChange = (e: any) => {
|
||||
typeValue.value = e.target.value;
|
||||
updateIframeUrlAndLoadLogs();
|
||||
};
|
||||
const mIfrm = ref<any>(null);
|
||||
onMounted(() => {
|
||||
// console.log(mIfrm.value);
|
||||
loadLogs();
|
||||
});
|
||||
|
||||
const formatLogTime = (logContent: string) => {
|
||||
const timeRegex = /(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\.\d+ \+08:00/g;
|
||||
return logContent.replace(timeRegex, '$1');
|
||||
};
|
||||
|
||||
const loadLogs = () => {
|
||||
useApiStore()
|
||||
.apiGetLogs(iframeUrl.value)
|
||||
.then((log) => {
|
||||
// console.log(log);
|
||||
const lines = log.split('\n'); // 将文本按换行符分割为行数组
|
||||
const reversedLines = lines.reverse(); // 对行数组进行倒序操作
|
||||
const reversedText = reversedLines.join('\n'); // 将行数组重新连接为一个字符串
|
||||
|
||||
log = formatLogTime(log);
|
||||
const lines = log.split('\n');
|
||||
const reversedLines = lines.reverse();
|
||||
const reversedText = reversedLines.join('\n');
|
||||
logs.value = reversedText;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('加载日志失败:', err);
|
||||
logs.value = '日志加载失败,请稍后重试';
|
||||
});
|
||||
};
|
||||
|
||||
// 复制日志内容的核心函数
|
||||
const copyLogs = async () => {
|
||||
try {
|
||||
// 空内容判断
|
||||
if (!logs.value || logs.value === '日志加载失败,请稍后重试') {
|
||||
message.warning('暂无可复制的日志内容');
|
||||
return;
|
||||
}
|
||||
// 使用浏览器剪贴板API复制内容
|
||||
await navigator.clipboard.writeText(logs.value);
|
||||
message.success('日志内容复制成功!');
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err);
|
||||
// 降级方案:兼容不支持Clipboard API的浏览器
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = logs.value;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
// 提示反馈
|
||||
message.success('日志内容复制成功!');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
updateIframeUrlAndLoadLogs();
|
||||
});
|
||||
</script>
|
||||
<style lang='less' scoped>
|
||||
html {
|
||||
|
||||
<style lang='less' scoped>
|
||||
// 基础样式:仅清理默认margin/padding,不设置全屏
|
||||
html,
|
||||
body {
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// 表单容器:正常宽度,保留少量间距
|
||||
:deep(.ant-form) {
|
||||
width: 100%;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// 日志容器:自适应高度,无固定上限,保留上下留白
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// max-height: 400px;
|
||||
iframe {
|
||||
.word-wrap {
|
||||
color: white !important;
|
||||
padding: 0 8px;
|
||||
margin: 16px 0; // 上下留白,远离屏幕边缘
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
// pre样式:自适应高度,内容过多时内部滚动
|
||||
.card-width-pre {
|
||||
width: 100% !important;
|
||||
min-width: 100%;
|
||||
padding: 8px;
|
||||
margin: 0 !important;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
// 核心:最小高度保证容器不塌陷,最大高度限制不超视口一半
|
||||
min-height: 200px;
|
||||
max-height: calc(67vh); // 最多占视口一半,绝对不会满屏
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
// 滚动条美化(保留)
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
.date-control-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
.date-btn {
|
||||
width: 36px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.current-date-text {
|
||||
font-size: 14px;
|
||||
color: #1f2329;
|
||||
font-weight: 500;
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
// 复制按钮样式
|
||||
.copy-btn {
|
||||
// height: 32px; /* 和日期按钮/单选按钮高度一致 */
|
||||
// padding: 0 16px;
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
// card-body样式:恢复默认内边距,作为pre的宽度基准
|
||||
:deep(.ant-card-body) {
|
||||
padding: 4px; // 恢复合理内边距,作为pre的宽度容器
|
||||
height: 100%; // 全屏继承容器高度
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden; // 新增:隐藏卡片内溢出,确保pre独占滚动区域
|
||||
}
|
||||
|
||||
// error状态下pre文字颜色
|
||||
.log-error {
|
||||
.card-width-pre {
|
||||
color: #f5222d !important;
|
||||
}
|
||||
}
|
||||
|
||||
// radio-button样式(保留之前的修正版)
|
||||
:deep(.log-type-radio-group) {
|
||||
.ant-radio-button-wrapper:first-child {
|
||||
&.ant-radio-button-wrapper-checked {
|
||||
background-color: #52c41a !important;
|
||||
border-color: #52c41a !important;
|
||||
color: #fff !important;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: #389e0d !important;
|
||||
border-color: #389e0d !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.ant-radio-button-wrapper-checked):hover {
|
||||
border-color: #73d13d !important;
|
||||
color: #52c41a !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-radio-button-wrapper:last-child {
|
||||
&.ant-radio-button-wrapper-checked {
|
||||
background-color: #f5222d !important;
|
||||
border-color: #f5222d !important;
|
||||
color: #fff !important;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: #cf1322 !important;
|
||||
border-color: #cf1322 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.ant-radio-button-wrapper-checked):hover {
|
||||
border-color: #ff4d4f !important;
|
||||
color: #f5222d !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-radio-button-wrapper {
|
||||
&:first-child {
|
||||
border-radius: 6px 0 0 6px !important;
|
||||
}
|
||||
&:last-child {
|
||||
border-radius: 0 6px 6px 0 !important;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: -1px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-radio-button-wrapper-checked {
|
||||
&:first-child {
|
||||
&::before {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
// 调整form布局,让复制按钮靠右
|
||||
:deep(.ant-form-inline) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,173 +0,0 @@
|
||||
<template>
|
||||
<a-drawer v-model:visible="visible" class="custom-class" title="邮件配置" placement="right" @after-visible-change="afterVisibleChange" :maskClosable="true">
|
||||
<a-tabs v-model:activeKey="activeKey">
|
||||
<a-tab-pane key="1">
|
||||
<template #tab>
|
||||
<span>
|
||||
<!-- <BellOutlined /> -->
|
||||
邮件STMP配置
|
||||
</span>
|
||||
</template>
|
||||
<a-form ref="emailFromRef" :model="EmailFormData" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish" @finishFailed="onFinishFailed">
|
||||
<a-form-item label="是否开启" ref="Open">
|
||||
<a-checkbox v-model:checked="EmailFormData.Open">解析结果邮件通知</a-checkbox>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="EmailFormData.Open" label="Stmp地址" ref="Stmp" name="Stmp" :rules="[{ required: EmailFormData.Open, message: '请输入stmp服务地址!',validator:validateStmp }]">
|
||||
<a-input v-model:value="EmailFormData.Stmp" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="EmailFormData.Open" label="Stmp端口" ref="Port" name="Port" :rules="[{ required: EmailFormData.Open, message: '请输入stmp服务端口!',validator:validatePort }]">
|
||||
<a-input v-model:value="EmailFormData.Port" placeholder="默认465,yeah-587" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="EmailFormData.Open" label="收件Email" type='email' ref="From" name="From" :rules="[{ required: EmailFormData.Open, message: '请输入收件Email地址!',validator:validateEmail }]">
|
||||
<a-input v-model:value="EmailFormData.From" />
|
||||
<span style="color:#888">此处收件人亦是发件人</span>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="EmailFormData.Open" label="Stmp授权码" ref="Code" name="Code" :rules="[{ required: EmailFormData.Open, message: '请输入Stmp授权码!' }]">
|
||||
<a-input v-model:value="EmailFormData.Code" />
|
||||
<span style="color:#888">邮箱设置开启stmp服务会提示</span>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
|
||||
<a-button type="primary" html-type="submit">确认</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-drawer>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { message } from 'ant-design-vue';
|
||||
import { defineComponent, ref, onMounted, reactive, watch } from 'vue';
|
||||
import type { FormInstance } from 'ant-design-vue';
|
||||
import { useApiStore, useAccountStore } from '@/store';
|
||||
import http from '@/store/http';
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import { checkEmail, checkDomain, checkPort } from '@/utils/regexHelper';
|
||||
const activeKey = ref<string>('1');
|
||||
const visible = ref<boolean>(false);
|
||||
|
||||
function showEmail(vis: boolean) {
|
||||
visible.value = vis;
|
||||
if (vis) loadStmpInfo();
|
||||
}
|
||||
const emailFromRef = ref<FormInstance>();
|
||||
const afterVisibleChange = (bool: boolean) => {
|
||||
if (!bool) {
|
||||
emailFromRef.value.resetFields();
|
||||
}
|
||||
};
|
||||
|
||||
const loadStmpInfo = () => {
|
||||
// useApiStore()
|
||||
// .apiGetStmpConfig()
|
||||
// .then((res) => {
|
||||
// if (res.code === 0) {
|
||||
// EmailFormData.Open = res.data.open;
|
||||
// EmailFormData.Stmp = res.data.stmp;
|
||||
// EmailFormData.Port = res.data.port;
|
||||
// EmailFormData.Code = res.data.code;
|
||||
// EmailFormData.From = res.data.from;
|
||||
// EmailFormData.Id = res.data.id;
|
||||
// }
|
||||
// });
|
||||
};
|
||||
|
||||
interface EmailFormState {
|
||||
Open: boolean;
|
||||
Stmp: string;
|
||||
Port: number;
|
||||
Code: string;
|
||||
From: string;
|
||||
Id: string;
|
||||
To: string;
|
||||
}
|
||||
const EmailFormData = reactive<EmailFormState>({
|
||||
Open: false,
|
||||
Stmp: 'smtp.qq.com',
|
||||
Port: 465,
|
||||
Code: '',
|
||||
From: '',
|
||||
Id: '',
|
||||
To: '',
|
||||
});
|
||||
const onFinish = (values: EmailFormState) => {
|
||||
EmailFormData.To = EmailFormData.From;
|
||||
// useApiStore()
|
||||
// .apiSaveStmpConfig(EmailFormData)
|
||||
// .then((res) => {
|
||||
// console.log(res);
|
||||
// if (res.code === 0) {
|
||||
// message.success('修改配置成功');
|
||||
// visible.value = false;
|
||||
// } else {
|
||||
// message.error(res.erro, 8);
|
||||
// }
|
||||
// });
|
||||
};
|
||||
|
||||
//验证邮箱
|
||||
const validateEmail = async (_rule: Rule, value: string) => {
|
||||
if (checkEmail(value)) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject('请输入正确的邮箱地址');
|
||||
}
|
||||
};
|
||||
|
||||
//验证stmp服务
|
||||
const validateStmp = async (_rule: Rule, value: string) => {
|
||||
if (checkDomain(value)) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject('请输入正确的stmp-server地址');
|
||||
}
|
||||
};
|
||||
//验证端口
|
||||
const validatePort = async (_rule: Rule, value: string) => {
|
||||
if (checkPort(value)) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject('请输入正确的端口');
|
||||
}
|
||||
};
|
||||
const onFinishFailed = (errorInfo: any) => {
|
||||
console.log('Failed:', errorInfo);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => EmailFormData.Open,
|
||||
() => {
|
||||
emailFromRef.value.validateFields(['Stmp', 'Port', 'From', 'Code']);
|
||||
},
|
||||
{ flush: 'post' }
|
||||
);
|
||||
|
||||
// 将updateMessage方法暴露给父组件调用
|
||||
defineExpose({
|
||||
showEmail,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.avatar-uploader > .ant-upload {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
.ant-upload-picture-card-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
.ant-upload-select-picture-card i {
|
||||
font-size: 32px;
|
||||
color: #999;
|
||||
margin-top: 50px !important;
|
||||
}
|
||||
.ant-upload-select-picture-card {
|
||||
margin-top: 50px !important;
|
||||
}
|
||||
.ant-upload-select-picture-card .ant-upload-text {
|
||||
margin-top: 8px;
|
||||
color: #666;
|
||||
}
|
||||
.ant-tabs-content {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +1,6 @@
|
||||
<template>
|
||||
<a-drawer v-model:visible="visible" class="custom-class" title="个人设置" placement="right" @after-visible-change="afterVisibleChange" :maskClosable="true">
|
||||
<a-tabs v-model:activeKey="activeKey">
|
||||
<!-- <a-tab-pane key="1" style="height:100%">
|
||||
<template #tab>
|
||||
<span>
|
||||
<user-outlined />
|
||||
修改头像
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<a-upload style="" v-model:file-list="fileList" name="file" list-type="picture-card" class="avatar-uploader" :show-upload-list="false" :action="uploadAction" :before-upload="beforeUpload" @change="handleChange" accept=".jpg, .jpeg, .png">
|
||||
<img v-if="imageUrl" :src="imageUrl" alt="avatar" style="height: 112px; width: 112px; border-radius: 50%;" />
|
||||
<div v-else>
|
||||
<loading-outlined v-if="loading"></loading-outlined>
|
||||
<plus-outlined v-else></plus-outlined>
|
||||
<div class="ant-upload-text">选择图片</div>
|
||||
</div>
|
||||
</a-upload>
|
||||
<div style="margin-top:20px;">
|
||||
上传成功即修改成功
|
||||
</div>
|
||||
</a-tab-pane> -->
|
||||
<a-tab-pane key="2">
|
||||
<template #tab>
|
||||
<span>
|
||||
@@ -60,7 +40,6 @@ import { checkPass } from '@/utils/regexHelper';
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
|
||||
const activeKey = ref<string>('2');
|
||||
const uploadAction = ref<string>('');
|
||||
const visible = ref<boolean>(false);
|
||||
|
||||
function show(vis: boolean) {
|
||||
@@ -122,7 +101,6 @@ const loadUserInfo = () => {
|
||||
if (res.code === 0) {
|
||||
if (res.data.avatar != null && res.data.avatar !== '') imageUrl.value = `/upload/${res.data.avatar}`;
|
||||
passwordFormData.UserId = res.data.id;
|
||||
uploadAction.value = `/api/auth/UpdateUserAvatar?Uid=${res.data.id}`;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -150,7 +128,7 @@ const onFinish = (values: PassFormState) => {
|
||||
visible.value = false;
|
||||
http.removeAuthorization();
|
||||
} else {
|
||||
message.error(res.erro, 8);
|
||||
message.error(res.message, 8);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import MyPersonal from './MyPersonal.vue';
|
||||
import EmailSet from './EmailSet.vue';
|
||||
// import EmailSet from './EmailSet.vue';
|
||||
|
||||
export { MyPersonal, EmailSet };
|
||||
export { MyPersonal };
|
||||
|
||||
+893
-122
File diff suppressed because it is too large
Load Diff
+1524
-395
File diff suppressed because it is too large
Load Diff
@@ -1,745 +0,0 @@
|
||||
<template>
|
||||
<div class="stats-dashboard">
|
||||
<div class="dashboard-container">
|
||||
<!-- 页面标题 -->
|
||||
|
||||
<!-- 核心统计概览 -->
|
||||
<section class="stats-overview">
|
||||
<div class="stat-card primary-card">
|
||||
<div class="stat-header">
|
||||
<span class="stat-meta">总视频数</span>
|
||||
<div class="stat-icon video-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="23 7 16 12 23 17 23 7"></polygon>
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-value">{{ totalVideos }}</div>
|
||||
<div class="stat-trend">
|
||||
<!-- <span class="trend-up">较上月 +5.2%</span> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card secondary-card">
|
||||
<div class="stat-header">
|
||||
<span class="stat-meta">占用空间</span>
|
||||
<div class="stat-icon size-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 12H2v8h20v-8z" />
|
||||
<path d="M6 18h.01" />
|
||||
<path d="M10 18h.01" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-value">{{ fileSizeTotal }} G</div>
|
||||
<div class="stat-trend">
|
||||
<!-- <span class="trend-up">较上月 +12.8%</span> -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 次级统计卡片组 -->
|
||||
<section class="stats-grid">
|
||||
<!-- 我喜欢的数量卡片 -->
|
||||
<div class="stat-card mini-card">
|
||||
<div class="stat-header">
|
||||
<span class="stat-meta">我喜欢的</span>
|
||||
<div class="stat-icon like-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-value">{{ favoriteCount }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 我收藏的数量卡片 -->
|
||||
<div class="stat-card mini-card">
|
||||
<div class="stat-header">
|
||||
<span class="stat-meta">我收藏的</span>
|
||||
<div class="stat-icon collect-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-value">{{ collectCount }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 作者总数卡片 -->
|
||||
<div class="stat-card mini-card">
|
||||
<div class="stat-header">
|
||||
<span class="stat-meta">作者总数</span>
|
||||
<div class="stat-icon author-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-value">{{ totalAuthors }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类总数卡片 -->
|
||||
<div class="stat-card mini-card">
|
||||
<div class="stat-header">
|
||||
<span class="stat-meta">分类总数</span>
|
||||
<div class="stat-icon cate-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="5" width="20" height="14" rx="2" />
|
||||
<path d="M17 5v4" />
|
||||
<path d="M7 5v4" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-value">{{ categoryTotal }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 详细分类统计 -->
|
||||
<section class="detailed-stats">
|
||||
<div class="stats-header">
|
||||
<h2>详细统计</h2>
|
||||
<div class="tab-controls">
|
||||
<button class="tab-btn" :class="{ active: currentTab === 'author' }" @click="currentTab = 'author'">
|
||||
视频作者
|
||||
</button>
|
||||
<button class="tab-btn" :class="{ active: currentTab === 'type' }" @click="currentTab = 'type'">
|
||||
视频分类
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition name="stats-fade" mode="out-in">
|
||||
<!-- 作者统计 -->
|
||||
<div v-if="currentTab === 'author'" key="author-view" class="stats-content">
|
||||
<div class="authors-grid">
|
||||
<div class="author-card" v-for="(author, index) in authors" :key="index">
|
||||
<div class="author-avatar">
|
||||
<img :src="author.icon" alt="作者头像" />
|
||||
</div>
|
||||
<div class="author-info">
|
||||
<h3 class="author-name">{{ author.name }}</h3>
|
||||
<p class="author-stats">作品数: {{ author.count }}</p>
|
||||
</div>
|
||||
<div class="author-progress">
|
||||
<div class="progress-bar" :style="{ width: `${(author.count / totalVideos) * 100}%` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类统计 -->
|
||||
<div v-else key="category-view" class="stats-content">
|
||||
<div class="categories-grid">
|
||||
<div class="category-card" v-for="(category, index) in categories" :key="index" :style="{ '--category-color': category.color }">
|
||||
<div class="category-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 12 12" fill="white">
|
||||
<use :xlink:href="`#${category.icon}`"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="category-info">
|
||||
<h3 class="category-name">{{ category.name }}</h3>
|
||||
<p class="category-stats">作品数: {{ category.count }}</p>
|
||||
</div>
|
||||
<div class="category-percentage">
|
||||
{{ Math.round((category.count / totalVideos) * 100) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- SVG图标定义 -->
|
||||
<svg style="display: none;">
|
||||
<!-- 所有图标定义保持不变 -->
|
||||
<symbol id="cup" viewBox="0 0 12 12">
|
||||
<path d="M18 4h2v16h-2zM4 4h14v2H4zM4 8h10v2H4zM4 12h10v2H4zM4 16h6v2H4zM4 20h6v2H4z" />
|
||||
</symbol>
|
||||
<!-- 其他图标省略,保持原样 -->
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useApiStore } from '@/store';
|
||||
|
||||
// 类型接口
|
||||
interface Author {
|
||||
name: string;
|
||||
count: number;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
interface Category {
|
||||
name: string;
|
||||
count: number;
|
||||
color: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
// 状态管理
|
||||
const totalVideos = ref<number>(0);
|
||||
const totalAuthors = ref<number>(0);
|
||||
const categoryTotal = ref<number>(0);
|
||||
const fileSizeTotal = ref<number>(0);
|
||||
const favoriteCount = ref<number>(0);
|
||||
const collectCount = ref<number>(0);
|
||||
const categories = ref<Category[]>([]);
|
||||
const authors = ref<Author[]>([]);
|
||||
const currentTab = ref<string>('author');
|
||||
|
||||
// 组件名称
|
||||
defineOptions({
|
||||
name: 'StatsDashboard',
|
||||
});
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
loadDashboardData();
|
||||
});
|
||||
|
||||
const loadDashboardData = async () => {
|
||||
try {
|
||||
const res = await useApiStore().VideoStatics();
|
||||
totalVideos.value = res.data.videoCount;
|
||||
totalAuthors.value = res.data.authorCount;
|
||||
categoryTotal.value = res.data.categoryCount;
|
||||
favoriteCount.value = res.data.favoriteCount;
|
||||
collectCount.value = res.data.collectCount;
|
||||
categories.value = res.data.categories;
|
||||
fileSizeTotal.value = res.data.viedoSizeTotal; // 修复拼写错误
|
||||
authors.value = res.data.authors;
|
||||
|
||||
// 分类图标和颜色随机分配
|
||||
const cates = getRandomElements(categoriessss.value, categories.value.length);
|
||||
const colors = getRandomElements(colorArray, categories.value.length);
|
||||
categories.value.forEach((item, index) => {
|
||||
item.icon = cates[index];
|
||||
item.color = colors[index];
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('加载仪表盘数据失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 随机获取数组元素方法
|
||||
const getRandomElements = (arr: any[], n: number) => {
|
||||
if (n <= 0) return [];
|
||||
if (n >= arr.length) return [...arr];
|
||||
return [...arr].sort(() => Math.random() - 0.5).slice(0, n);
|
||||
};
|
||||
|
||||
// 图标列表和颜色数组
|
||||
const categoriessss = ref<any[]>([
|
||||
'cup',
|
||||
'spoon',
|
||||
'fork',
|
||||
'knife',
|
||||
'plate',
|
||||
'bottle',
|
||||
'toothbrush',
|
||||
'comb',
|
||||
'mirror',
|
||||
'soap',
|
||||
'mobile',
|
||||
'tablet',
|
||||
'camera',
|
||||
'headphones',
|
||||
'speaker',
|
||||
'tv',
|
||||
'watch',
|
||||
'charger',
|
||||
]);
|
||||
|
||||
const colorArray = [
|
||||
'#3A7CA5',
|
||||
'#D9A566',
|
||||
'#6B4226',
|
||||
'#829399',
|
||||
'#A63446',
|
||||
'#4F6367',
|
||||
'#FE5F55',
|
||||
'#C7EFCF',
|
||||
'#78C0E0',
|
||||
'#49BEAA',
|
||||
'#49DCB1',
|
||||
'#FF9999',
|
||||
'#66B2FF',
|
||||
'#99FF99',
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 白天模式默认样式(无dark-mode类时生效) */
|
||||
.stats-dashboard {
|
||||
min-height: 100vh;
|
||||
background-color: #ffffff; /* 白天背景:白色 */
|
||||
color: #333333; /* 白天文本:深灰 */
|
||||
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
/* 加宽整体宽度并减少两侧留白 */
|
||||
.dashboard-container {
|
||||
max-width: 1400px; /* 从1200px加宽至1600px */
|
||||
margin: 0 auto;
|
||||
padding: 0 15px; /* 两侧留白从20px减至15px */
|
||||
}
|
||||
|
||||
/* 响应式适配:避免宽屏溢出 */
|
||||
@media (max-width: 1700px) {
|
||||
.dashboard-container {
|
||||
max-width: 95%; /* 中等屏幕用百分比控制宽度 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.dashboard-header {
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #e0e0e0; /* 白天边框:浅灰 */
|
||||
}
|
||||
|
||||
.dashboard-header h1 {
|
||||
font-size: 28px;
|
||||
margin: 0 0 10px 0;
|
||||
color: #333333; /* 白天标题:深灰 */
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 16px;
|
||||
color: #666666; /* 白天副标题:中灰 */
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 统计卡片样式(增强白天阴影) */
|
||||
.stat-card {
|
||||
background: #f5f5f5; /* 白天卡片背景:浅灰 */
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); /* 增强阴影 */
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15); /* 增强hover阴影 */
|
||||
}
|
||||
|
||||
.stat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.stat-meta {
|
||||
font-size: 14px;
|
||||
color: #666666; /* 白天元数据:中灰 */
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.video-icon {
|
||||
background-color: rgba(76, 175, 80, 0.2);
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.size-icon {
|
||||
background-color: rgba(33, 150, 243, 0.2);
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.like-icon {
|
||||
background-color: rgba(233, 30, 99, 0.2);
|
||||
color: #e91e63;
|
||||
}
|
||||
|
||||
.collect-icon {
|
||||
background-color: rgba(255, 152, 0, 0.2);
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
.author-icon {
|
||||
background-color: rgba(156, 39, 176, 0.2);
|
||||
color: #9c27b0;
|
||||
}
|
||||
|
||||
.cate-icon {
|
||||
background-color: rgba(0, 188, 212, 0.2);
|
||||
color: #00bcd4;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 10px 0;
|
||||
color: #333333;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.trend-up {
|
||||
color: #4caf50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trend-up::before {
|
||||
content: '↑';
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* 概览区域布局 */
|
||||
.stats-overview {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.stats-overview {
|
||||
grid-template-columns: 2fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.primary-card {
|
||||
border-top: 4px solid #4caf50;
|
||||
}
|
||||
|
||||
.secondary-card {
|
||||
border-top: 4px solid #2196f3;
|
||||
}
|
||||
|
||||
/* 次级统计网格布局 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.mini-card .stat-value {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* 详细统计区域 */
|
||||
.detailed-stats {
|
||||
background: #f5f5f5;
|
||||
border-radius: 12px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.stats-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.stats-header h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.tab-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #666666;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #4caf50;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tab-btn:hover:not(.active) {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* 统计内容区域 */
|
||||
.stats-content {
|
||||
animation: fadeIn 0.5s ease;
|
||||
}
|
||||
|
||||
.authors-grid,
|
||||
.categories-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.authors-grid,
|
||||
.categories-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.authors-grid,
|
||||
.categories-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* 作者卡片(增强白天阴影) */
|
||||
.author-card {
|
||||
background: #eeeeee;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); /* 新增阴影 */
|
||||
}
|
||||
|
||||
.author-card:hover {
|
||||
transform: translateX(5px);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.12); /* 增强hover阴影 */
|
||||
}
|
||||
|
||||
.author-avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.author-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.author-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 16px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.author-stats {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.author-progress {
|
||||
height: 6px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: #4caf50;
|
||||
border-radius: 3px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
/* 分类卡片(增强白天阴影) */
|
||||
.category-card {
|
||||
background: #eeeeee;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); /* 新增阴影 */
|
||||
}
|
||||
|
||||
.category-card:hover {
|
||||
transform: translateX(5px);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.12); /* 增强hover阴影 */
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--category-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.category-name {
|
||||
margin: 0 0 3px 0;
|
||||
font-size: 16px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.category-stats {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.category-percentage {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--category-color);
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.stats-fade-enter-from,
|
||||
.stats-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.stats-fade-enter-active,
|
||||
.stats-fade-leave-active {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* 晚上模式样式 */
|
||||
html.dark-mode .stats-dashboard {
|
||||
background-color: #1a1a2e;
|
||||
color: #eaeaea;
|
||||
}
|
||||
|
||||
html.dark-mode .dashboard-header {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
html.dark-mode .dashboard-header h1 {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
html.dark-mode .header-subtitle {
|
||||
color: #b0b0c3;
|
||||
}
|
||||
|
||||
html.dark-mode .stat-card {
|
||||
background: rgba(30, 30, 50, 0.8);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
html.dark-mode .stat-card:hover {
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
html.dark-mode .stat-meta {
|
||||
color: #b0b0c3;
|
||||
}
|
||||
|
||||
html.dark-mode .stat-value {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
html.dark-mode .detailed-stats {
|
||||
background: rgba(30, 30, 50, 0.8);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
html.dark-mode .stats-header h2 {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
html.dark-mode .tab-btn {
|
||||
color: #b0b0c3;
|
||||
}
|
||||
|
||||
html.dark-mode .tab-btn:hover:not(.active) {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
html.dark-mode .author-card {
|
||||
background: rgba(40, 40, 65, 0.6);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
html.dark-mode .author-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
html.dark-mode .author-stats {
|
||||
color: #b0b0c3;
|
||||
}
|
||||
|
||||
html.dark-mode .author-progress {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
html.dark-mode .category-card {
|
||||
background: rgba(40, 40, 65, 0.6);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
html.dark-mode .category-name {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
html.dark-mode .category-stats {
|
||||
color: #b0b0c3;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
+979
-1242
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user