✨ 优化 2026 主题移动端与详情体验
This commit is contained in:
@@ -8,7 +8,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
font-family: 'DingTalk', sans-serif !important;
|
font-family: 'DingTalk', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.native-date-input {
|
.native-date-input {
|
||||||
@@ -32,7 +45,7 @@
|
|||||||
background:
|
background:
|
||||||
linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(234, 243, 245, 0.9)),
|
linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(234, 243, 245, 0.9)),
|
||||||
radial-gradient(circle at 14% 10%, rgba(122, 159, 171, 0.18), transparent 30%);
|
radial-gradient(circle at 14% 10%, rgba(122, 159, 171, 0.18), transparent 30%);
|
||||||
box-shadow: 0 14px 34px rgba(128, 151, 155, 0.22);
|
box-shadow: 0 10px 26px rgba(128, 151, 155, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-2026-card::before {
|
.theme-2026-card::before {
|
||||||
@@ -58,7 +71,7 @@
|
|||||||
|
|
||||||
.theme-2026-card-hover:hover {
|
.theme-2026-card-hover:hover {
|
||||||
border-color: rgba(116, 153, 165, 0.86);
|
border-color: rgba(116, 153, 165, 0.86);
|
||||||
box-shadow: 0 20px 42px rgba(114, 143, 150, 0.32);
|
box-shadow: 0 16px 34px rgba(114, 143, 150, 0.26);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .theme-2026-card {
|
.dark .theme-2026-card {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<div class="flex min-h-full items-center justify-center p-4">
|
<div class="flex min-h-full items-center justify-center p-4">
|
||||||
<div
|
<div
|
||||||
ref="modalRef"
|
ref="modalRef"
|
||||||
class="relative transform overflow-hidden rounded-[30px] shadow-xl transition-all"
|
class="relative flex max-h-[calc(100vh-2rem)] transform flex-col overflow-hidden rounded-[30px] shadow-xl transition-all"
|
||||||
:class="[
|
:class="[
|
||||||
sizeClasses,
|
sizeClasses,
|
||||||
isDarkMode ? 'bg-gray-800' : 'bg-white'
|
isDarkMode ? 'bg-gray-800' : 'bg-white'
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<!-- 头部 -->
|
<!-- 头部 -->
|
||||||
<div
|
<div
|
||||||
v-if="$slots.header || title"
|
v-if="$slots.header || title"
|
||||||
class="flex items-center justify-between px-6 py-4 border-b"
|
class="flex shrink-0 items-center justify-between border-b px-4 py-3 sm:px-6 sm:py-4"
|
||||||
:class="[isDarkMode ? 'border-gray-700' : 'border-gray-200']"
|
:class="[isDarkMode ? 'border-gray-700' : 'border-gray-200']"
|
||||||
>
|
>
|
||||||
<slot name="header">
|
<slot name="header">
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
<button
|
<button
|
||||||
v-if="closable"
|
v-if="closable"
|
||||||
@click="$emit('close')"
|
@click="$emit('close')"
|
||||||
class="rounded-md p-2 transition-colors"
|
class="flex h-11 w-11 shrink-0 items-center justify-center rounded-lg transition-colors"
|
||||||
:class="[
|
:class="[
|
||||||
isDarkMode
|
isDarkMode
|
||||||
? 'text-gray-400 hover:text-gray-300 hover:bg-gray-700'
|
? 'text-gray-400 hover:text-gray-300 hover:bg-gray-700'
|
||||||
@@ -49,14 +49,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 内容 -->
|
<!-- 内容 -->
|
||||||
<div class="px-6 py-4">
|
<div class="min-h-0 flex-1 overflow-y-auto px-4 py-4 sm:px-6">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 底部 -->
|
<!-- 底部 -->
|
||||||
<div
|
<div
|
||||||
v-if="$slots.footer"
|
v-if="$slots.footer"
|
||||||
class="flex items-center justify-end space-x-3 px-6 py-4 border-t"
|
class="flex shrink-0 items-center justify-end space-x-3 border-t px-4 py-3 sm:px-6 sm:py-4"
|
||||||
:class="[isDarkMode ? 'border-gray-700 bg-gray-900/50' : 'border-gray-200 bg-gray-50']"
|
:class="[isDarkMode ? 'border-gray-700 bg-gray-900/50' : 'border-gray-200 bg-gray-50']"
|
||||||
>
|
>
|
||||||
<slot name="footer"></slot>
|
<slot name="footer"></slot>
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="theme-2026-card theme-2026-card-hover group rounded-[30px] p-6 shadow-md transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
|
class="theme-2026-card theme-2026-card-hover group rounded-[24px] p-4 shadow-md transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg sm:rounded-[30px] sm:p-6"
|
||||||
:class="[
|
:class="[
|
||||||
isDarkMode
|
isDarkMode
|
||||||
? 'hover:bg-gray-800 hover:shadow-black/20'
|
? 'hover:bg-gray-800 hover:shadow-black/20'
|
||||||
: 'hover:shadow-gray-200/80'
|
: 'hover:shadow-gray-200/80'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div class="min-w-0">
|
||||||
<p class="text-sm" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-600']">
|
<p class="truncate text-xs sm:text-sm" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-600']">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</p>
|
</p>
|
||||||
<h3 class="text-2xl font-bold mt-1" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
|
<h3 class="mt-1 truncate text-xl font-bold tabular-nums sm:text-2xl" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
|
||||||
{{ value }}
|
{{ value }}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="rounded-full p-3 transition-transform duration-300 group-hover:scale-105"
|
class="shrink-0 rounded-full p-2.5 transition-transform duration-300 group-hover:scale-105 sm:p-3"
|
||||||
:class="iconBgClass"
|
:class="iconBgClass"
|
||||||
>
|
>
|
||||||
<component :is="icon" class="w-6 h-6" :class="iconClass" />
|
<component :is="icon" class="h-5 w-5 sm:h-6 sm:w-6" :class="iconClass" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm mt-2" :class="descriptionClass">
|
<p class="mt-2 line-clamp-1 text-xs sm:text-sm" :class="descriptionClass">
|
||||||
<slot name="description"></slot>
|
<slot name="description"></slot>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -439,7 +439,9 @@ export function useAdminFiles() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
const detailPolicyActionOptions = computed<DetailPolicyActionOption[]>(() =>
|
const detailPolicyActionOptions = computed<DetailPolicyActionOption[]>(() =>
|
||||||
createPolicyActionOptions(Boolean(selectedFileDetail.value?.isPermanentFile))
|
createPolicyActionOptions(Boolean(selectedFileDetail.value?.isPermanentFile)).filter(
|
||||||
|
(option) => !option.disabled
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const batchPolicyActionOptions = computed<BatchPolicyActionOption[]>(
|
const batchPolicyActionOptions = computed<BatchPolicyActionOption[]>(
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export default {
|
|||||||
admin: {
|
admin: {
|
||||||
logout: 'Logout',
|
logout: 'Logout',
|
||||||
dashboard: {
|
dashboard: {
|
||||||
|
adminLabel: 'FileCodeBox Admin',
|
||||||
title: 'Dashboard',
|
title: 'Dashboard',
|
||||||
totalFiles: 'Total Files',
|
totalFiles: 'Total Files',
|
||||||
storageSpace: 'Storage Space',
|
storageSpace: 'Storage Space',
|
||||||
@@ -536,6 +537,8 @@ export default {
|
|||||||
statusInsight: 'Status Insight',
|
statusInsight: 'Status Insight',
|
||||||
nextAction: 'Next Action',
|
nextAction: 'Next Action',
|
||||||
lifecycle: 'Lifecycle',
|
lifecycle: 'Lifecycle',
|
||||||
|
expandLifecycle: 'Show all lifecycle records ({count})',
|
||||||
|
collapseLifecycle: 'Collapse lifecycle records',
|
||||||
permanent: 'Permanent',
|
permanent: 'Permanent',
|
||||||
createdAt: 'Created Time',
|
createdAt: 'Created Time',
|
||||||
remainingDownloads: 'Remaining Downloads',
|
remainingDownloads: 'Remaining Downloads',
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export default {
|
|||||||
admin: {
|
admin: {
|
||||||
logout: '退出登录',
|
logout: '退出登录',
|
||||||
dashboard: {
|
dashboard: {
|
||||||
|
adminLabel: 'FileCodeBox 管理后台',
|
||||||
title: '仪表盘',
|
title: '仪表盘',
|
||||||
totalFiles: '总文件数',
|
totalFiles: '总文件数',
|
||||||
storageSpace: '存储空间',
|
storageSpace: '存储空间',
|
||||||
@@ -533,6 +534,8 @@ export default {
|
|||||||
statusInsight: '状态洞察',
|
statusInsight: '状态洞察',
|
||||||
nextAction: '建议动作',
|
nextAction: '建议动作',
|
||||||
lifecycle: '生命周期',
|
lifecycle: '生命周期',
|
||||||
|
expandLifecycle: '展开全部生命周期记录(共 {count} 条)',
|
||||||
|
collapseLifecycle: '收起生命周期记录',
|
||||||
permanent: '永久有效',
|
permanent: '永久有效',
|
||||||
createdAt: '创建时间',
|
createdAt: '创建时间',
|
||||||
remainingDownloads: '剩余次数',
|
remainingDownloads: '剩余次数',
|
||||||
|
|||||||
+29
-12
@@ -36,30 +36,47 @@ export const copyToClipboard = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showManualCopyDialog = () => {
|
||||||
|
window.prompt('复制下面内容', text)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 优先使用 Clipboard API
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
if (document.hasFocus() && navigator.clipboard && navigator.clipboard.writeText) {
|
try {
|
||||||
await navigator.clipboard.writeText(text)
|
await navigator.clipboard.writeText(text)
|
||||||
showCopyMessage(successMsg, 'success')
|
showCopyMessage(successMsg, 'success')
|
||||||
return true
|
return true
|
||||||
|
} catch {
|
||||||
|
// Continue to the DOM fallback.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 后备方案:使用传统的复制方法
|
|
||||||
const textarea = document.createElement('textarea')
|
const textarea = document.createElement('textarea')
|
||||||
textarea.value = text
|
textarea.value = text
|
||||||
textarea.style.position = 'fixed'
|
textarea.setAttribute('readonly', '')
|
||||||
textarea.style.opacity = '0'
|
textarea.style.cssText =
|
||||||
|
'position:fixed;left:0;top:0;width:1px;height:1px;opacity:0;z-index:-1;'
|
||||||
document.body.appendChild(textarea)
|
document.body.appendChild(textarea)
|
||||||
|
textarea.focus()
|
||||||
textarea.select()
|
textarea.select()
|
||||||
const success = document.execCommand('copy')
|
textarea.setSelectionRange(0, textarea.value.length)
|
||||||
document.body.removeChild(textarea)
|
let success = false
|
||||||
|
try {
|
||||||
|
success = document.execCommand('copy')
|
||||||
|
} finally {
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
}
|
||||||
if (success) {
|
if (success) {
|
||||||
showCopyMessage(successMsg, 'success')
|
showCopyMessage(successMsg, 'success')
|
||||||
return true
|
return true
|
||||||
} else {
|
|
||||||
throw new Error('execCommand copy failed')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showManualCopyDialog()
|
||||||
|
showCopyMessage('已打开手动复制窗口', 'error')
|
||||||
|
return false
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('复制失败:', err)
|
console.error('复制失败:', err)
|
||||||
|
showManualCopyDialog()
|
||||||
showCopyMessage(errorMsg, 'error')
|
showCopyMessage(errorMsg, 'error')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-6">
|
<div class="p-4 sm:p-6">
|
||||||
<div class="mb-6 flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
|
<div class="mb-6 flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm" :class="[mutedTextClass]">FileCodeBox Admin</p>
|
<p class="text-sm" :class="[mutedTextClass]">{{ t('admin.dashboard.adminLabel') }}</p>
|
||||||
<h2 class="text-2xl font-bold" :class="[primaryTextClass]">
|
<h2 class="text-2xl font-bold" :class="[primaryTextClass]">
|
||||||
{{ t('admin.dashboard.title') }}
|
{{ t('admin.dashboard.title') }}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
<div class="grid grid-cols-2 gap-3 sm:gap-4 xl:grid-cols-4">
|
||||||
<StatCard
|
<StatCard
|
||||||
:title="t('admin.dashboard.totalFiles')"
|
:title="t('admin.dashboard.totalFiles')"
|
||||||
:value="dashboardData.totalFiles"
|
:value="dashboardData.totalFiles"
|
||||||
@@ -78,9 +78,9 @@
|
|||||||
</StatCard>
|
</StatCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="dashboardData.hasExtendedStats" class="mt-6 grid grid-cols-1 gap-6 xl:grid-cols-3">
|
<div v-if="dashboardData.hasExtendedStats" class="mt-6 grid grid-cols-1 gap-6 2xl:grid-cols-3">
|
||||||
<section
|
<section
|
||||||
class="theme-2026-card theme-2026-card-hover rounded-[30px] p-5 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg xl:col-span-2"
|
class="theme-2026-card theme-2026-card-hover rounded-[30px] p-5 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg 2xl:col-span-2"
|
||||||
:class="[panelClass, cardHoverClass]"
|
:class="[panelClass, cardHoverClass]"
|
||||||
>
|
>
|
||||||
<div class="mb-5 flex items-center justify-between">
|
<div class="mb-5 flex items-center justify-between">
|
||||||
@@ -119,12 +119,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-5">
|
<div class="mt-6 grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-5">
|
||||||
<button
|
<button
|
||||||
v-for="action in healthActions"
|
v-for="action in healthActions"
|
||||||
:key="action.key"
|
:key="action.key"
|
||||||
type="button"
|
type="button"
|
||||||
class="theme-2026-card-hover group flex min-h-28 flex-col justify-between rounded-[30px] border p-4 text-left shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
class="theme-2026-card-hover group flex min-h-24 flex-col justify-between rounded-[24px] border p-4 text-left shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md sm:rounded-[30px]"
|
||||||
:class="getHealthActionClass(action.tone)"
|
:class="getHealthActionClass(action.tone)"
|
||||||
@click="openHealthQueue(action.health)"
|
@click="openHealthQueue(action.health)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-6">
|
<div class="p-4 sm:p-6">
|
||||||
<div class="mb-6 flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
|
<div class="mb-6 flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-2xl font-bold" :class="[primaryTextClass]">
|
<h2 class="text-2xl font-bold" :class="[primaryTextClass]">
|
||||||
@@ -11,22 +11,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
<div class="mb-5 grid grid-cols-2 gap-3 sm:mb-6 sm:gap-4 xl:grid-cols-4">
|
||||||
<div
|
<div
|
||||||
v-for="card in summaryCards"
|
v-for="card in summaryCards"
|
||||||
:key="card.label"
|
:key="card.label"
|
||||||
class="theme-2026-card theme-2026-card-hover group rounded-[30px] border p-4 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
class="theme-2026-card theme-2026-card-hover group rounded-[24px] border p-3.5 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg sm:rounded-[30px] sm:p-4"
|
||||||
:class="[panelClass, cardHoverClass]"
|
:class="[panelClass, cardHoverClass]"
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between gap-3">
|
<div class="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm" :class="[mutedTextClass]">{{ card.label }}</p>
|
<p class="text-xs sm:text-sm" :class="[mutedTextClass]">{{ card.label }}</p>
|
||||||
<p class="mt-2 text-2xl font-semibold" :class="[primaryTextClass]">
|
<p class="mt-1.5 truncate text-xl font-semibold tabular-nums sm:mt-2 sm:text-2xl" :class="[primaryTextClass]">
|
||||||
{{ card.value }}
|
{{ card.value }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="rounded-lg p-2 transition-transform duration-200 group-hover:scale-105"
|
class="rounded-lg p-2 transition-transform duration-200 group-hover:scale-105 max-sm:p-1.5"
|
||||||
:class="[card.iconClass]"
|
:class="[card.iconClass]"
|
||||||
>
|
>
|
||||||
<component :is="card.icon" class="h-5 w-5" />
|
<component :is="card.icon" class="h-5 w-5" />
|
||||||
@@ -39,14 +39,14 @@
|
|||||||
class="theme-2026-card theme-2026-card-hover mb-6 rounded-[30px] border p-4 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
class="theme-2026-card theme-2026-card-hover mb-6 rounded-[30px] border p-4 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
||||||
:class="[panelClass, cardHoverClass]"
|
:class="[panelClass, cardHoverClass]"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-base font-semibold" :class="[primaryTextClass]">条件筛选</h3>
|
<h3 class="text-base font-semibold" :class="[primaryTextClass]">条件筛选</h3>
|
||||||
<p class="mt-1 text-xs" :class="[mutedTextClass]">
|
<p class="mt-1 text-xs" :class="[mutedTextClass]">
|
||||||
{{ hasActiveFilters ? '已应用筛选条件' : '默认收起,需要时展开' }}
|
{{ hasActiveFilters ? '已应用筛选条件' : '默认收起,需要时展开' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
<div class="flex flex-wrap items-center gap-2 sm:justify-end">
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="hasActiveFilters"
|
v-if="hasActiveFilters"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -269,10 +269,10 @@
|
|||||||
v-if="tableData.length > 0"
|
v-if="tableData.length > 0"
|
||||||
class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
|
class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
|
||||||
>
|
>
|
||||||
<label class="inline-flex items-center gap-2 text-sm" :class="[primaryTextClass]">
|
<label class="inline-flex min-h-11 cursor-pointer items-center gap-2 text-sm sm:min-h-0" :class="[primaryTextClass]">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
class="h-5 w-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||||
:checked="isAllCurrentPageSelected"
|
:checked="isAllCurrentPageSelected"
|
||||||
:disabled="isBatchActionRunning"
|
:disabled="isBatchActionRunning"
|
||||||
:indeterminate="isCurrentPagePartiallySelected"
|
:indeterminate="isCurrentPagePartiallySelected"
|
||||||
@@ -287,7 +287,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div v-if="hasSelectedFiles" class="flex flex-wrap items-center gap-2">
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="hasSelectedFiles"
|
v-if="hasSelectedFiles"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -407,7 +407,7 @@
|
|||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
class="h-5 w-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||||
:checked="selectedFileIds.has(file.id)"
|
:checked="selectedFileIds.has(file.id)"
|
||||||
:disabled="isBatchActionRunning"
|
:disabled="isBatchActionRunning"
|
||||||
:aria-label="t('fileManage.selectFile', { name: file.displayName })"
|
:aria-label="t('fileManage.selectFile', { name: file.displayName })"
|
||||||
@@ -873,70 +873,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section
|
|
||||||
class="theme-2026-card-hover space-y-3 rounded-[30px] border px-4 py-4 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
|
||||||
:class="[subtleSectionClass, cardHoverClass]"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
||||||
<div>
|
|
||||||
<h4 class="text-sm font-semibold" :class="[primaryTextClass]">
|
|
||||||
{{ t('fileManage.metadataInfo') }}
|
|
||||||
</h4>
|
|
||||||
<p class="mt-1 text-xs" :class="[mutedTextClass]">
|
|
||||||
{{ t('fileManage.metadataHint') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<BaseButton
|
|
||||||
size="sm"
|
|
||||||
:loading="isDetailMetadataSaving"
|
|
||||||
:disabled="isDetailLoading"
|
|
||||||
@click="updateDetailMetadata"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<CheckIcon class="mr-2 h-4 w-4" />
|
|
||||||
</template>
|
|
||||||
{{ t('fileManage.saveMetadata') }}
|
|
||||||
</BaseButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-3 lg:grid-cols-[minmax(0,1fr)_minmax(220px,0.55fr)]">
|
|
||||||
<label class="space-y-1">
|
|
||||||
<span class="text-xs font-medium" :class="[mutedTextClass]">
|
|
||||||
{{ t('fileManage.metadataNote') }}
|
|
||||||
</span>
|
|
||||||
<textarea
|
|
||||||
v-model="detailMetadataForm.note"
|
|
||||||
rows="4"
|
|
||||||
maxlength="2000"
|
|
||||||
class="w-full resize-y rounded-lg border px-3 py-2 text-sm focus:border-transparent focus:ring-2 focus:ring-indigo-500"
|
|
||||||
:class="[fieldClass]"
|
|
||||||
:placeholder="t('fileManage.metadataNotePlaceholder')"
|
|
||||||
></textarea>
|
|
||||||
</label>
|
|
||||||
<label class="space-y-1">
|
|
||||||
<span class="text-xs font-medium" :class="[mutedTextClass]">
|
|
||||||
{{ t('fileManage.metadataTags') }}
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
v-model="detailMetadataForm.tagsText"
|
|
||||||
type="text"
|
|
||||||
class="w-full rounded-lg border px-3 py-2 text-sm focus:border-transparent focus:ring-2 focus:ring-indigo-500"
|
|
||||||
:class="[fieldClass]"
|
|
||||||
:placeholder="t('fileManage.metadataTagsPlaceholder')"
|
|
||||||
/>
|
|
||||||
<span class="block text-xs" :class="[mutedTextClass]">
|
|
||||||
{{
|
|
||||||
selectedFileDetail.metadataUpdatedAt
|
|
||||||
? t('fileManage.metadataUpdatedAt', {
|
|
||||||
time: formatMetadataUpdatedAt(selectedFileDetail.metadataUpdatedAt)
|
|
||||||
})
|
|
||||||
: t('fileManage.metadataNeverUpdated')
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
<section
|
||||||
class="theme-2026-card-hover rounded-[30px] border px-4 py-4 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
class="theme-2026-card-hover rounded-[30px] border px-4 py-4 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
||||||
:class="getInsightPanelClass(selectedFileDetail.statusInsightSeverity)"
|
:class="getInsightPanelClass(selectedFileDetail.statusInsightSeverity)"
|
||||||
@@ -1046,15 +982,32 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-if="selectedFileDetail.detailTimeline.length > 0" class="space-y-3">
|
<section v-if="selectedFileDetail.detailTimeline.length > 0" class="space-y-3">
|
||||||
<h4 class="text-sm font-semibold" :class="[primaryTextClass]">
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
{{ t('fileManage.lifecycle') }}
|
<h4 class="text-sm font-semibold" :class="[primaryTextClass]">
|
||||||
</h4>
|
{{ t('fileManage.lifecycle') }}
|
||||||
|
</h4>
|
||||||
|
<button
|
||||||
|
v-if="selectedFileDetail.detailTimeline.length > 3"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex min-h-11 items-center self-start rounded-lg border px-3 py-2 text-sm font-medium transition-colors sm:min-h-0 sm:self-auto"
|
||||||
|
:class="detailActionClass"
|
||||||
|
@click="isLifecycleExpanded = !isLifecycleExpanded"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
isLifecycleExpanded
|
||||||
|
? t('fileManage.collapseLifecycle')
|
||||||
|
: t('fileManage.expandLifecycle', {
|
||||||
|
count: selectedFileDetail.detailTimeline.length
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="rounded-[30px] border px-4 py-4 shadow-md"
|
class="rounded-[30px] border px-4 py-4 shadow-md"
|
||||||
:class="[isDarkMode ? 'border-gray-700 bg-gray-700/30' : 'border-gray-200 bg-white']"
|
:class="[isDarkMode ? 'border-gray-700 bg-gray-700/30' : 'border-gray-200 bg-white']"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in selectedFileDetail.detailTimeline"
|
v-for="(item, index) in visibleDetailTimeline"
|
||||||
:key="`${item.key}-${index}`"
|
:key="`${item.key}-${index}`"
|
||||||
class="grid grid-cols-[auto_minmax(0,1fr)] gap-3"
|
class="grid grid-cols-[auto_minmax(0,1fr)] gap-3"
|
||||||
>
|
>
|
||||||
@@ -1064,7 +1017,7 @@
|
|||||||
:class="getTimelineDotClass(item.severity)"
|
:class="getTimelineDotClass(item.severity)"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="index < selectedFileDetail.detailTimeline.length - 1"
|
v-if="index < visibleDetailTimeline.length - 1"
|
||||||
class="my-1 w-px flex-1"
|
class="my-1 w-px flex-1"
|
||||||
:class="[isDarkMode ? 'bg-gray-600' : 'bg-gray-200']"
|
:class="[isDarkMode ? 'bg-gray-600' : 'bg-gray-200']"
|
||||||
/>
|
/>
|
||||||
@@ -1091,6 +1044,70 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="theme-2026-card-hover space-y-3 rounded-[30px] border px-4 py-4 shadow-md transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg"
|
||||||
|
:class="[subtleSectionClass, cardHoverClass]"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-semibold" :class="[primaryTextClass]">
|
||||||
|
{{ t('fileManage.metadataInfo') }}
|
||||||
|
</h4>
|
||||||
|
<p class="mt-1 text-xs" :class="[mutedTextClass]">
|
||||||
|
{{ t('fileManage.metadataHint') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<BaseButton
|
||||||
|
size="sm"
|
||||||
|
:loading="isDetailMetadataSaving"
|
||||||
|
:disabled="isDetailLoading"
|
||||||
|
@click="updateDetailMetadata"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<CheckIcon class="mr-2 h-4 w-4" />
|
||||||
|
</template>
|
||||||
|
{{ t('fileManage.saveMetadata') }}
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-3 lg:grid-cols-[minmax(0,1fr)_minmax(220px,0.55fr)]">
|
||||||
|
<label class="space-y-1">
|
||||||
|
<span class="text-xs font-medium" :class="[mutedTextClass]">
|
||||||
|
{{ t('fileManage.metadataNote') }}
|
||||||
|
</span>
|
||||||
|
<textarea
|
||||||
|
v-model="detailMetadataForm.note"
|
||||||
|
rows="3"
|
||||||
|
maxlength="2000"
|
||||||
|
class="w-full resize-y rounded-lg border px-3 py-2 text-sm focus:border-transparent focus:ring-2 focus:ring-indigo-500"
|
||||||
|
:class="[fieldClass]"
|
||||||
|
:placeholder="t('fileManage.metadataNotePlaceholder')"
|
||||||
|
></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="space-y-1">
|
||||||
|
<span class="text-xs font-medium" :class="[mutedTextClass]">
|
||||||
|
{{ t('fileManage.metadataTags') }}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
v-model="detailMetadataForm.tagsText"
|
||||||
|
type="text"
|
||||||
|
class="w-full rounded-lg border px-3 py-2 text-sm focus:border-transparent focus:ring-2 focus:ring-indigo-500"
|
||||||
|
:class="[fieldClass]"
|
||||||
|
:placeholder="t('fileManage.metadataTagsPlaceholder')"
|
||||||
|
/>
|
||||||
|
<span class="block text-xs" :class="[mutedTextClass]">
|
||||||
|
{{
|
||||||
|
selectedFileDetail.metadataUpdatedAt
|
||||||
|
? t('fileManage.metadataUpdatedAt', {
|
||||||
|
time: formatMetadataUpdatedAt(selectedFileDetail.metadataUpdatedAt)
|
||||||
|
})
|
||||||
|
: t('fileManage.metadataNeverUpdated')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="space-y-3">
|
<section class="space-y-3">
|
||||||
<h4 class="text-sm font-semibold" :class="[primaryTextClass]">
|
<h4 class="text-sm font-semibold" :class="[primaryTextClass]">
|
||||||
{{ t('fileManage.storageInfo') }}
|
{{ t('fileManage.storageInfo') }}
|
||||||
@@ -1572,6 +1589,11 @@ const {
|
|||||||
} = useAdminFiles()
|
} = useAdminFiles()
|
||||||
|
|
||||||
const isFilterPanelOpen = ref(false)
|
const isFilterPanelOpen = ref(false)
|
||||||
|
const isLifecycleExpanded = ref(false)
|
||||||
|
const visibleDetailTimeline = computed(() => {
|
||||||
|
const rows = selectedFileDetail.value?.detailTimeline || []
|
||||||
|
return isLifecycleExpanded.value ? rows : rows.slice(0, 3)
|
||||||
|
})
|
||||||
const filterBodyVisible = computed(() => isFilterPanelOpen.value || hasActiveFilters.value)
|
const filterBodyVisible = computed(() => isFilterPanelOpen.value || hasActiveFilters.value)
|
||||||
const isHistoryLoading = ref(false)
|
const isHistoryLoading = ref(false)
|
||||||
const historyRows = ref<AnalyticsFileRow[]>([])
|
const historyRows = ref<AnalyticsFileRow[]>([])
|
||||||
@@ -2051,6 +2073,13 @@ const getTimelineDotClass = (severity: AdminFileInsightSeverity) => {
|
|||||||
return classes[severity]
|
return classes[severity]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => selectedFileDetail.value?.id,
|
||||||
|
() => {
|
||||||
|
isLifecycleExpanded.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.query.health,
|
() => route.query.health,
|
||||||
async () => {
|
async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user