feat(desktop): square file cards and context-menu file actions
This commit is contained in:
@@ -413,6 +413,44 @@ async fn api_delete_share(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn api_create_direct_link(
|
||||||
|
state: tauri::State<'_, ApiState>,
|
||||||
|
base_url: String,
|
||||||
|
file_path: String,
|
||||||
|
file_name: Option<String>,
|
||||||
|
expiry_days: Option<i32>,
|
||||||
|
) -> Result<BridgeResponse, String> {
|
||||||
|
let mut body = Map::new();
|
||||||
|
body.insert("file_path".to_string(), Value::String(file_path));
|
||||||
|
|
||||||
|
if let Some(name) = file_name {
|
||||||
|
if !name.trim().is_empty() {
|
||||||
|
body.insert("file_name".to_string(), Value::String(name.trim().to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(days) = expiry_days {
|
||||||
|
if days > 0 {
|
||||||
|
body.insert("expiry_days".to_string(), Value::Number(days.into()));
|
||||||
|
} else {
|
||||||
|
body.insert("expiry_days".to_string(), Value::Null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body.insert("expiry_days".to_string(), Value::Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
request_with_optional_csrf(
|
||||||
|
&state.client,
|
||||||
|
Method::POST,
|
||||||
|
&base_url,
|
||||||
|
"/api/direct-link/create",
|
||||||
|
Some(Value::Object(body)),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
@@ -436,7 +474,8 @@ pub fn run() {
|
|||||||
api_get_download_url,
|
api_get_download_url,
|
||||||
api_get_my_shares,
|
api_get_my_shares,
|
||||||
api_create_share,
|
api_create_share,
|
||||||
api_delete_share
|
api_delete_share,
|
||||||
|
api_create_direct_link
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||||
|
|
||||||
@@ -70,13 +70,11 @@ const shares = ref<ShareItem[]>([]);
|
|||||||
|
|
||||||
const transferTasks = ref<{ id: string; name: string; speed: string; progress: number; status: string }[]>([]);
|
const transferTasks = ref<{ id: string; name: string; speed: string; progress: number; status: string }[]>([]);
|
||||||
const sharesLoading = ref(false);
|
const sharesLoading = ref(false);
|
||||||
|
const contextMenu = reactive({
|
||||||
const shareCreate = reactive({
|
visible: false,
|
||||||
creating: false,
|
x: 0,
|
||||||
expiryPreset: "never" as "never" | "1" | "7" | "30" | "custom",
|
y: 0,
|
||||||
customDays: 7,
|
item: null as FileItem | null,
|
||||||
enablePassword: false,
|
|
||||||
password: "",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const toast = reactive({
|
const toast = reactive({
|
||||||
@@ -239,16 +237,6 @@ function getShareExpireLabel(value: string | null | undefined) {
|
|||||||
return expired ? `已过期 · ${text}` : text;
|
return expired ? `已过期 · ${text}` : text;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveShareExpiryDays() {
|
|
||||||
if (shareCreate.expiryPreset === "never") return null;
|
|
||||||
if (shareCreate.expiryPreset === "custom") {
|
|
||||||
const days = Number(shareCreate.customDays);
|
|
||||||
if (!Number.isFinite(days) || days < 1 || days > 365) return null;
|
|
||||||
return Math.floor(days);
|
|
||||||
}
|
|
||||||
return Number(shareCreate.expiryPreset);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function copyText(text: string, successMessage: string) {
|
async function copyText(text: string, successMessage: string) {
|
||||||
try {
|
try {
|
||||||
if (navigator.clipboard?.writeText) {
|
if (navigator.clipboard?.writeText) {
|
||||||
@@ -340,37 +328,18 @@ async function loadShares(silent = false) {
|
|||||||
if (!silent) sharesLoading.value = false;
|
if (!silent) sharesLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createShareFromSelected() {
|
async function createShareForItem(current: FileItem) {
|
||||||
const current = selectedFile.value;
|
|
||||||
if (!current) {
|
|
||||||
showToast("请先在文件视图选中一个文件或文件夹", "info");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const expiryDays = resolveShareExpiryDays();
|
|
||||||
if (shareCreate.expiryPreset === "custom" && !expiryDays) {
|
|
||||||
showToast("自定义有效期需要 1-365 天", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const password = shareCreate.enablePassword ? shareCreate.password.trim() : "";
|
|
||||||
if (shareCreate.enablePassword && !password) {
|
|
||||||
showToast("请填写分享密码", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
shareCreate.creating = true;
|
|
||||||
const response = await invokeBridge("api_create_share", {
|
const response = await invokeBridge("api_create_share", {
|
||||||
baseUrl: appConfig.baseUrl,
|
baseUrl: appConfig.baseUrl,
|
||||||
shareType: current.isDirectory || current.type === "directory" ? "directory" : "file",
|
shareType: current.isDirectory || current.type === "directory" ? "directory" : "file",
|
||||||
filePath: buildItemPath(current),
|
filePath: buildItemPath(current),
|
||||||
fileName: current.displayName || current.name,
|
fileName: current.displayName || current.name,
|
||||||
password: shareCreate.enablePassword ? password : null,
|
password: null,
|
||||||
expiryDays,
|
expiryDays: null,
|
||||||
});
|
});
|
||||||
shareCreate.creating = false;
|
|
||||||
|
|
||||||
if (response.ok && response.data?.success) {
|
if (response.ok && response.data?.success) {
|
||||||
showToast("分享创建成功", "success");
|
showToast("分享创建成功", "success");
|
||||||
shareCreate.password = "";
|
|
||||||
await loadShares(true);
|
await loadShares(true);
|
||||||
const shareUrl = String(response.data.share_url || "");
|
const shareUrl = String(response.data.share_url || "");
|
||||||
if (shareUrl) {
|
if (shareUrl) {
|
||||||
@@ -382,6 +351,31 @@ async function createShareFromSelected() {
|
|||||||
showToast(response.data?.message || "创建分享失败", "error");
|
showToast(response.data?.message || "创建分享失败", "error");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createDirectLinkForItem(current: FileItem) {
|
||||||
|
if (current.isDirectory || current.type === "directory") {
|
||||||
|
showToast("文件夹不支持生成直链", "info");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await invokeBridge("api_create_direct_link", {
|
||||||
|
baseUrl: appConfig.baseUrl,
|
||||||
|
filePath: buildItemPath(current),
|
||||||
|
fileName: current.displayName || current.name,
|
||||||
|
expiryDays: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok && response.data?.success) {
|
||||||
|
showToast("直链创建成功", "success");
|
||||||
|
const directUrl = String(response.data.direct_url || "");
|
||||||
|
if (directUrl) {
|
||||||
|
await copyText(directUrl, "直链已复制");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(response.data?.message || "创建直链失败", "error");
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteShare(share: ShareItem) {
|
async function deleteShare(share: ShareItem) {
|
||||||
const confirmed = window.confirm(`确认删除分享 ${share.share_code} 吗?`);
|
const confirmed = window.confirm(`确认删除分享 ${share.share_code} 吗?`);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
@@ -523,8 +517,8 @@ async function createFolder() {
|
|||||||
showToast(response.data?.message || "创建文件夹失败", "error");
|
showToast(response.data?.message || "创建文件夹失败", "error");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renameSelected() {
|
async function renameSelected(target?: FileItem | null) {
|
||||||
const current = selectedFile.value;
|
const current = target || selectedFile.value;
|
||||||
if (!current) {
|
if (!current) {
|
||||||
showToast("请先选中文件或文件夹", "info");
|
showToast("请先选中文件或文件夹", "info");
|
||||||
return;
|
return;
|
||||||
@@ -545,8 +539,8 @@ async function renameSelected() {
|
|||||||
showToast(response.data?.message || "重命名失败", "error");
|
showToast(response.data?.message || "重命名失败", "error");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteSelected() {
|
async function deleteSelected(target?: FileItem | null) {
|
||||||
const current = selectedFile.value;
|
const current = target || selectedFile.value;
|
||||||
if (!current) {
|
if (!current) {
|
||||||
showToast("请先选中文件或文件夹", "info");
|
showToast("请先选中文件或文件夹", "info");
|
||||||
return;
|
return;
|
||||||
@@ -567,8 +561,8 @@ async function deleteSelected() {
|
|||||||
showToast(response.data?.message || "删除失败", "error");
|
showToast(response.data?.message || "删除失败", "error");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadSelected() {
|
async function downloadSelected(target?: FileItem | null) {
|
||||||
const current = selectedFile.value;
|
const current = target || selectedFile.value;
|
||||||
if (!current) {
|
if (!current) {
|
||||||
showToast("请先选中文件", "info");
|
showToast("请先选中文件", "info");
|
||||||
return;
|
return;
|
||||||
@@ -617,6 +611,59 @@ async function jumpToPath(nextPath: string) {
|
|||||||
await loadFiles(nextPath);
|
await loadFiles(nextPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeContextMenu() {
|
||||||
|
contextMenu.visible = false;
|
||||||
|
contextMenu.item = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openContextMenu(event: MouseEvent, item: FileItem) {
|
||||||
|
selectFile(item);
|
||||||
|
contextMenu.item = item;
|
||||||
|
const maxX = window.innerWidth - 220;
|
||||||
|
const maxY = window.innerHeight - 260;
|
||||||
|
contextMenu.x = Math.max(8, Math.min(event.clientX, maxX));
|
||||||
|
contextMenu.y = Math.max(8, Math.min(event.clientY, maxY));
|
||||||
|
contextMenu.visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeContextAction(action: "open" | "download" | "rename" | "delete" | "share" | "direct") {
|
||||||
|
const item = contextMenu.item;
|
||||||
|
closeContextMenu();
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
if (action === "open") {
|
||||||
|
await openItem(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === "download") {
|
||||||
|
await downloadSelected(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === "rename") {
|
||||||
|
await renameSelected(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === "delete") {
|
||||||
|
await deleteSelected(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === "share") {
|
||||||
|
await createShareForItem(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await createDirectLinkForItem(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGlobalClick() {
|
||||||
|
if (contextMenu.visible) closeContextMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGlobalKey(event: KeyboardEvent) {
|
||||||
|
if (event.key === "Escape" && contextMenu.visible) {
|
||||||
|
closeContextMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(nav, async (next) => {
|
watch(nav, async (next) => {
|
||||||
if (next === "shares" && authenticated.value) {
|
if (next === "shares" && authenticated.value) {
|
||||||
await loadShares();
|
await loadShares();
|
||||||
@@ -624,8 +671,15 @@ watch(nav, async (next) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
window.addEventListener("click", handleGlobalClick);
|
||||||
|
window.addEventListener("keydown", handleGlobalKey);
|
||||||
await restoreSession();
|
await restoreSession();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener("click", handleGlobalClick);
|
||||||
|
window.removeEventListener("keydown", handleGlobalKey);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -731,9 +785,6 @@ onMounted(async () => {
|
|||||||
/>
|
/>
|
||||||
<button class="action-btn" @click="runGlobalSearch">搜索</button>
|
<button class="action-btn" @click="runGlobalSearch">搜索</button>
|
||||||
<button class="action-btn" @click="createFolder">新建文件夹</button>
|
<button class="action-btn" @click="createFolder">新建文件夹</button>
|
||||||
<button class="action-btn" @click="renameSelected">重命名</button>
|
|
||||||
<button class="action-btn danger" @click="deleteSelected">删除</button>
|
|
||||||
<button class="solid-btn" @click="downloadSelected">下载</button>
|
|
||||||
<button class="action-btn" @click="loadFiles(pathState.currentPath)">刷新</button>
|
<button class="action-btn" @click="loadFiles(pathState.currentPath)">刷新</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="nav === 'shares'">
|
<template v-else-if="nav === 'shares'">
|
||||||
@@ -762,6 +813,7 @@ onMounted(async () => {
|
|||||||
:class="{ selected: selectedFileName === item.name }"
|
:class="{ selected: selectedFileName === item.name }"
|
||||||
@click="selectFile(item)"
|
@click="selectFile(item)"
|
||||||
@dblclick="openItem(item)"
|
@dblclick="openItem(item)"
|
||||||
|
@contextmenu.prevent="openContextMenu($event, item)"
|
||||||
>
|
>
|
||||||
<div class="file-icon-glyph">{{ fileIcon(item) }}</div>
|
<div class="file-icon-glyph">{{ fileIcon(item) }}</div>
|
||||||
<div class="file-name" :title="item.displayName || item.name">{{ item.displayName || item.name }}</div>
|
<div class="file-name" :title="item.displayName || item.name">{{ item.displayName || item.name }}</div>
|
||||||
@@ -796,50 +848,7 @@ onMounted(async () => {
|
|||||||
<template v-else-if="nav === 'shares'">
|
<template v-else-if="nav === 'shares'">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<h3>我的分享</h3>
|
<h3>我的分享</h3>
|
||||||
<span>创建、查看、复制与删除</span>
|
<span>仅展示已分享文件及操作</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="share-create-toolbar">
|
|
||||||
<div class="share-target">
|
|
||||||
当前选中:
|
|
||||||
<strong>{{ selectedFile ? (selectedFile.displayName || selectedFile.name) : "未选择文件/文件夹" }}</strong>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="share-create-controls">
|
|
||||||
<label class="compact-field">
|
|
||||||
有效期
|
|
||||||
<select v-model="shareCreate.expiryPreset">
|
|
||||||
<option value="never">永久</option>
|
|
||||||
<option value="1">1 天</option>
|
|
||||||
<option value="7">7 天</option>
|
|
||||||
<option value="30">30 天</option>
|
|
||||||
<option value="custom">自定义</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label v-if="shareCreate.expiryPreset === 'custom'" class="compact-field">
|
|
||||||
天数
|
|
||||||
<input v-model.number="shareCreate.customDays" type="number" min="1" max="365" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="inline-check">
|
|
||||||
<input v-model="shareCreate.enablePassword" type="checkbox" />
|
|
||||||
启用密码
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<input
|
|
||||||
v-if="shareCreate.enablePassword"
|
|
||||||
v-model="shareCreate.password"
|
|
||||||
type="text"
|
|
||||||
class="password-input"
|
|
||||||
maxlength="32"
|
|
||||||
placeholder="输入访问密码"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button class="solid-btn" :disabled="shareCreate.creating" @click="createShareFromSelected">
|
|
||||||
{{ shareCreate.creating ? "创建中..." : "创建分享" }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="sharesLoading" class="empty-tip">正在加载分享列表...</div>
|
<div v-if="sharesLoading" class="empty-tip">正在加载分享列表...</div>
|
||||||
@@ -904,6 +913,24 @@ onMounted(async () => {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="contextMenu.visible" class="context-mask" @click="closeContextMenu"></div>
|
||||||
|
<div
|
||||||
|
v-if="contextMenu.visible && contextMenu.item"
|
||||||
|
class="context-menu"
|
||||||
|
:style="{ left: `${contextMenu.x}px`, top: `${contextMenu.y}px` }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<button class="context-item" @click="executeContextAction('open')">
|
||||||
|
{{ contextMenu.item.isDirectory ? "打开文件夹" : "打开/下载" }}
|
||||||
|
</button>
|
||||||
|
<button v-if="!contextMenu.item.isDirectory" class="context-item" @click="executeContextAction('download')">下载文件</button>
|
||||||
|
<button class="context-item" @click="executeContextAction('rename')">重命名</button>
|
||||||
|
<button class="context-item danger" @click="executeContextAction('delete')">删除</button>
|
||||||
|
<div class="context-divider"></div>
|
||||||
|
<button class="context-item" @click="executeContextAction('share')">生成分享链接</button>
|
||||||
|
<button v-if="!contextMenu.item.isDirectory" class="context-item" @click="executeContextAction('direct')">生成直链</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="toast.visible" class="toast" :class="toast.type">{{ toast.message }}</div>
|
<div v-if="toast.visible" class="toast" :class="toast.type">{{ toast.message }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1321,7 +1348,7 @@ select:focus {
|
|||||||
.icon-grid {
|
.icon-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
|
||||||
align-content: start;
|
align-content: start;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -1337,8 +1364,11 @@ select:focus {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
min-height: 122px;
|
width: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-card:hover {
|
.file-card:hover {
|
||||||
@@ -1353,7 +1383,7 @@ select:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.file-icon-glyph {
|
.file-icon-glyph {
|
||||||
font-size: 30px;
|
font-size: 28px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
@@ -1423,71 +1453,6 @@ select:focus {
|
|||||||
background: linear-gradient(90deg, #1d6fff, #57a2ff);
|
background: linear-gradient(90deg, #1d6fff, #57a2ff);
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-create-toolbar {
|
|
||||||
border: 1px solid #d8e1ee;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #f8fbff;
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.share-target {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #4d6683;
|
|
||||||
}
|
|
||||||
|
|
||||||
.share-target strong {
|
|
||||||
color: #213247;
|
|
||||||
}
|
|
||||||
|
|
||||||
.share-create-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compact-field {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #4d6683;
|
|
||||||
min-width: 92px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compact-field select,
|
|
||||||
.compact-field input {
|
|
||||||
height: 34px;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-check {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #4d6683;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-check input[type="checkbox"] {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-input {
|
|
||||||
width: 160px;
|
|
||||||
height: 34px;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.share-list {
|
.share-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -1557,6 +1522,50 @@ select:focus {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.context-mask {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 90;
|
||||||
|
width: 210px;
|
||||||
|
border: 1px solid #d1dced;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 12px 24px rgba(25, 52, 86, 0.2);
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-item {
|
||||||
|
width: 100%;
|
||||||
|
height: 34px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #2a4664;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-item:hover {
|
||||||
|
background: #eef4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-item.danger {
|
||||||
|
color: #b53f3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #e5edf7;
|
||||||
|
margin: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-panel h3 {
|
.detail-panel h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|||||||
Reference in New Issue
Block a user