Files
vue-tickets/src/views/UserView.vue
2026-08-08 15:06:28 +08:00

995 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="user-view-container">
<!-- 顶层框 -->
<div class="user-header-card">
<div class="header-main">
<div class="welcome-container">
<div class="welcome-section">
<h2 class="greeting">{{ welcomeMessage }},{{ currentUser.username }}</h2>
<p class="sub-text">欢迎回到你的工作堆,开启牛马的一天。</p>
</div>
</div>
</div>
<div class="header-action">
<el-button type="danger" plain @click="handleLogout">
<el-icon>
<SwitchButton />
</el-icon>&nbsp;退出登录
</el-button>
</div>
</div>
<!-- 状态标签 -->
<div class="tabs-toolbar-row">
<div class="status-tabs-wrapper">
<el-radio-group v-model="currentStatus" @change="handleStatusChange" class="custom-radio-group">
<el-radio-button label="未分配">
<el-icon>
<Notification />
</el-icon>
待分配任务
</el-radio-button>
<el-radio-button label="进行中">
<el-icon>
<Timer />
</el-icon>
我的进行中
</el-radio-button>
<el-radio-button label="已完成">
<el-icon>
<CircleCheck />
</el-icon>
我的已完成
</el-radio-button>
<el-radio-button label="已完结">
<el-icon>
<Checked />
</el-icon>
我的已完结
</el-radio-button>
</el-radio-group>
</div>
<!-- 搜索框 -->
<div class="search-row">
<SearchInput v-model="searchKeyword" placeholder="搜索网站" @search="handleSearch" />
<el-button v-if="searchKeyword" :icon="Close" circle size="small" @click="handleClearSearch" />
</div>
</div>
<div class="table-card">
<!-- 表格 -->
<el-table :data="paginatedTickets" v-loading="loading" class="custom-table">
<template #empty>
<el-empty :image-size="120" :description="emptyText">
<el-button type="primary" plain @click="fetchTickets">刷新列表</el-button>
</el-empty>
</template>
<el-table-column prop="id" label="ID" width="70" align="center" />
<el-table-column prop="website" label="网站名称" min-width="150">
<template #default="{ row }">
<el-link type="primary" :href="formatUrl(row.website)" target="_blank" rel="noopener noreferrer">
{{ row.website }}
</el-link>
</template>
</el-table-column>
<el-table-column label="数据量" width="150" align="center">
<template #default="scope">
<div class="cell-content-card">
<div class="data-display-area">
<span v-if="scope.row.datanum && scope.row.datanum !== '0'" class="display-val">{{ scope.row.datanum
}}</span>
<span v-else class="no-data-placeholder">-</span>
</div>
<el-popover v-if="currentStatus !== '未分配'" placement="top" :width="200" trigger="click"
@show="prepareEdit(scope.row, 'datanum')">
<template #reference>
<el-button type="primary" size="small" link class="edit-link-btn">
{{
scope.row.datanum && scope.row.datanum !== "0"
? "[ 修改数据 ]"
: "[ 输入数据 ]"
}}
</el-button>
</template>
<div class="popover-content">
<div class="pop-header">录入站点数据量</div>
<el-input v-model="editValue" placeholder="请输入数值" size="small" clearable />
<div class="pop-footer">
<el-button type="primary" size="small" @click="submitEdit(scope.row, 'datanum')">确认提交</el-button>
</div>
</div>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column v-if="currentStatus === '进行中'" label="任务进度" width="150" align="center">
<template #default="scope">
<div class="cell-content-card">
<div class="data-display-area" style="width: 100%; padding: 0 10px">
<el-progress :percentage="Number(scope.row.progress || 0)"
:status="Number(scope.row.progress) === 100 ? 'success' : ''" :stroke-width="12" striped
striped-flow />
</div>
<el-popover v-if="currentStatus === '进行中'" placement="top" :width="240" trigger="click"
@show="prepareEdit(scope.row, 'progress')">
<template #reference>
<el-button type="primary" size="small" link class="edit-link-btn">
[ 更新进度 ]
</el-button>
</template>
<div class="popover-content">
<div class="pop-header">调整当前进度 (%)</div>
<el-radio-group v-model="editValue" size="small" class="progress-radio-group">
<el-radio-button :label="0">0</el-radio-button>
<el-radio-button :label="30">30</el-radio-button>
<el-radio-button :label="50">50</el-radio-button>
<el-radio-button :label="70">70</el-radio-button>
<el-radio-button :label="99">99</el-radio-button>
</el-radio-group>
<div class="pop-footer">
<el-button type="primary" size="small" @click="submitEdit(scope.row, 'progress')">确定</el-button>
</div>
</div>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column prop="operator_name" label="运营" width="120" align="center" />
<el-table-column label="备注" min-width="150" align="center">
<template #default="scope">
<div class="cell-content-card">
<div class="data-display-area">
<div v-if="scope.row.remark" class="display-remark" :title="scope.row.remark">
{{ scope.row.remark }}
</div>
<span v-else class="no-data-placeholder">-</span>
</div>
<el-popover v-if="currentStatus !== '未分配'" placement="top" :width="250" trigger="click"
@show="prepareEdit(scope.row, 'remark')">
<template #reference>
<el-button type="warning" size="small" link class="edit-link-btn">
{{ scope.row.remark ? "[ 修改备注 ]" : "[ 输入备注 ]" }}
</el-button>
</template>
<div class="popover-content">
<div class="pop-header">录入工单备注</div>
<el-input v-model="editValue" type="textarea" :rows="3" placeholder="请输入备注" size="small" />
<div class="pop-footer">
<el-button type="primary" size="small" @click="submitEdit(scope.row, 'remark')">提交保存</el-button>
</div>
</div>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column :label="['已完成', '已完结'].includes(currentStatus) ? '锁定日期' : '创建日期'" width="140" align="center">
<template #default="scope">
<span class="pure-date">
{{
formatDate(
["已完成", "已完结"].includes(currentStatus)
? scope.row.lock_time
: scope.row.create_time
)
}}
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="190" fixed="right" align="center">
<template #default="scope">
<el-button v-if="Number(scope.row.status) === 0" type="primary" size="small" :disabled="!realStaffInfo"
@click="handleAccept(scope.row)">立即接单</el-button>
<template v-if="Number(scope.row.status) === 1 && realStaffInfo">
<el-button type="success" size="small" @click="handleFinish(scope.row)">完成任务</el-button>
<el-button type="warning" plain size="small" @click="handleCancel(scope.row)">取消接单</el-button>
</template>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :total="allTickets.length"
:page-sizes="[15, 30, 50]" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
@current-change="handleCurrentChange" background />
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from "vue";
import { useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
import {
User,
SwitchButton,
Timer,
Notification,
CircleCheck,
Checked,
Loading,
Close,
} from "@element-plus/icons-vue";
import { ticketApi, userApi } from "@/api/ticket";
import { resetRouter } from "@/router";
import { formatUrl, formatDate } from "@/utils/format";
import SearchInput from "@/components/common/SearchInput.vue";
import { openSocket, closeSocket, onTicketChanged, onReconnect } from "@/utils/socket";
const router = useRouter();
const loading = ref(false);
const allTickets = ref([]);
const currentStatus = ref("未分配");
const editValue = ref("");
const searchKeyword = ref("");
const currentPage = ref(1);
const pageSize = ref(15);
const realStaffInfo = ref(null);
const emptyText = computed(() => `暂无${currentStatus.value}的工单`);
// js计算时间
const welcomeMessage = computed(() => {
const hour = new Date().getHours();
if (hour < 6) return "凌晨好";
if (hour < 9) return "早上好";
if (hour < 12) return "上午好";
if (hour < 14) return "中午好";
if (hour < 18) return "下午好";
return "晚上好";
});
// 获取当前用户信息
const currentUser = computed(() => {
const info = sessionStorage.getItem("user_info") || localStorage.getItem("user_info");
return info ? JSON.parse(info) : { id: null, username: "未知", role: "未知" };
});
// 获取工单信息
// silent: 静默刷新(WS 推送触发),不显示 loading 遮罩
const fetchTickets = async (silent = false) => {
if (!silent) loading.value = true; // 显示加载中状态
// 优化:不再获取所有工单,而是根据当前视图的状态和用户ID进行筛选
try {
const statusMap = {
未分配: 0,
进行中: 1,
已完成: 2,
已完结: 3,
};
const statusValue = statusMap[currentStatus.value]
const params = { // 传递后端参数
status: statusValue,
limit: 1000,
};
// 如果不是未分配,则只拉取当前员工相关的工单
if (currentStatus.value !== "未分配") {
// 判断是否是员工
if (currentUser.value.role !== "user") {
allTickets.value = []; // 如果不是员工,则清空列表
loading.value = false;
return;
}
params.user_id = currentUser.value.id;
}
// 针对员工搜索框
if (searchKeyword.value) {
params.search = searchKeyword.value;
}
const res = await ticketApi.getTickets(params);
if (res.code === 200) {
allTickets.value = res.data.data || [];
} else {
allTickets.value = [];
ElMessage.error(res.msg || "获取列表失败");
}
} catch (e) {
allTickets.value = [];
ElMessage.error("获取列表失败");
} finally {
if (!silent) loading.value = false;
}
};
const paginatedTickets = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
return allTickets.value.slice(start, end);
});
const handleSizeChange = (val) => {
pageSize.value = val;
currentPage.value = 1;
};
const handleCurrentChange = (val) => {
currentPage.value = val;
};
const handleStatusChange = async () => {
currentPage.value = 1;
await fetchTickets();
};
const handleSearch = async () => {
currentPage.value = 1;
await fetchTickets();
};
const handleClearSearch = async () => {
searchKeyword.value = "";
currentPage.value = 1;
await fetchTickets();
};
const prepareEdit = (row, field) => {
editValue.value = row[field] || "";
};
const submitEdit = async (row, field) => {
try {
const payload = {
datanum: field === "datanum" ? String(editValue.value) : String(row.datanum || ""),
remark: field === "remark" ? editValue.value : row.remark || "",
progress:
field === "progress" ? Number(editValue.value) : Number(row.progress || 0),
};
const res = await ticketApi.updateTicket(row.id, payload);
if (res.code === 200 || res.code === true) {
row[field] = editValue.value;
ElMessage.success("更新成功");
document.body.click();
}
} catch (e) {
ElMessage.error("操作失败");
}
};
// 点击接受工单
const handleAccept = (row) => {
ElMessageBox.confirm(`确认接手工单 #${row.id}?`).then(async () => {
try {
const res = await ticketApi.updateTicket(row.id, {
user_id: currentUser.value.id,
status: 1,
progress: 0,
});
if (res.code === 200 || res.code === true) {
ElMessage.success("接单成功");
fetchTickets();
}
} catch (e) {
ElMessage.error("操作失败");
}
});
};
// 点击完成
const handleFinish = (row) => {
ElMessageBox.confirm("确认已完成任务?").then(async () => {
try {
const currentTimestamp = Math.floor(Date.now() / 1000);
// 关键修改:在提交时增加 progress: 100
const res = await ticketApi.updateTicket(row.id, {
status: 2,
lock_time: String(currentTimestamp),
progress: 100,
});
if (res.code === 200 || res.code === true) {
ElMessage.success("任务已完成,进度已更新为100%");
fetchTickets();
}
} catch (e) {
ElMessage.error("操作失败");
}
});
};
// 新增:取消接单
const handleCancel = (row) => {
ElMessageBox.confirm(
`确认要取消接手此工单 #${row.id} 吗?工单将退回未分配列表。`,
"取消确认",
{ type: "warning" }
).then(async () => {
try {
const res = await ticketApi.updateTicket(row.id, {
user_id: null,
status: 0,
progress: 0,
});
if (res.code === 200 || res.code === true) {
ElMessage.success("已取消接单,工单已退回!");
fetchTickets();
} else {
ElMessage.error(res.msg || "操作失败");
}
} catch (e) {
ElMessage.error("操作失败");
}
});
};
// 退出登录(已完成)
const handleLogout = () => {
ElMessageBox.confirm("确定退出当前账号?", "提示", { type: "warning" }).then(async () => {
try {
await userApi.logout();
} catch (e) { }
localStorage.removeItem("user_info");
sessionStorage.removeItem("user_info");
resetRouter();
router.push({ name: "login" });
});
};
let stopTicketWatch = null;
let stopReconnectWatch = null;
onMounted(async () => {
try {
const res = await ticketApi.getUserStaffs();
if (res.code === 200 || res.code === true) {
const match = (res.data || []).find((s) => s.username === currentUser.value.username);
if (match) realStaffInfo.value = match;
}
} catch (e) { }
await fetchTickets();
// 实时刷新:后台新增/更新工单时静默刷新列表;断线重连后补拉一次
openSocket();
stopTicketWatch = onTicketChanged(() => fetchTickets(true));
stopReconnectWatch = onReconnect(() => fetchTickets(true));
});
onBeforeUnmount(() => {
stopTicketWatch && stopTicketWatch();
stopReconnectWatch && stopReconnectWatch();
closeSocket();
});
</script>
<style scoped>
/* ============================================
ENERGY WORKBENCH — Gamified Employee Desk
============================================ */
.user-view-container {
padding: 12px 12px 0;
min-height: 0;
background: none;
--glow-indigo: rgba(99, 102, 241, 0.35);
--glow-amber: rgba(245, 158, 11, 0.3);
--glow-green: rgba(34, 197, 94, 0.3);
--glow-rose: rgba(244, 63, 94, 0.25);
}
/* ---- Header Card (Gradient Hero) ---- */
.user-header-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
border: none;
border-radius: 16px;
padding: 28px 32px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
box-shadow:
0 4px 16px rgba(102, 126, 234, 0.2),
0 8px 32px rgba(118, 75, 162, 0.15);
position: relative;
overflow: hidden;
}
.user-header-card::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(circle at 20% 50%, rgba(255, 255, 255, 0.08) 0%, transparent 50%),
radial-gradient(circle at 80% 50%, rgba(255, 255, 255, 0.04) 0%, transparent 50%);
pointer-events: none;
}
.header-main {
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
z-index: 1;
}
.welcome-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.greeting {
margin: 0;
color: #fff;
font-size: 22px;
line-height: 1.3;
font-weight: 700;
letter-spacing: -0.02em;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.sub-text {
margin: 4px 0 0;
color: rgba(255, 255, 255, 0.75);
font-size: 13px;
font-weight: 400;
letter-spacing: 0.01em;
}
.identity-badges {
margin-top: 8px;
display: flex;
align-items: center;
position: relative;
z-index: 1;
}
:deep(.identity-badges .el-tag) {
display: inline-flex !important;
align-items: center !important;
gap: 5px !important;
background: rgba(255, 255, 255, 0.18) !important;
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.25) !important;
color: #fff !important;
font-weight: 600 !important;
padding: 4px 14px !important;
border-radius: 20px !important;
}
:deep(.identity-badges .el-tag .el-icon) {
color: #fbbf24 !important;
}
.header-action {
position: relative;
z-index: 1;
}
.header-action :deep(.el-button) {
background: rgba(255, 255, 255, 0.15) !important;
border: 1px solid rgba(255, 255, 255, 0.25) !important;
color: #fff !important;
backdrop-filter: blur(8px);
border-radius: 24px !important;
padding: 8px 18px !important;
}
.header-action :deep(.el-button:hover) {
background: rgba(255, 255, 255, 0.25) !important;
}
/* ---- Tabs + Search Row ---- */
.tabs-toolbar-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
gap: 16px;
}
.search-row {
display: flex;
align-items: center;
gap: 8px;
}
.search-row :deep(.search-input) {
width: 200px;
}
/* ---- Tab Switcher ---- */
.status-tabs-wrapper {
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 14px;
padding: 5px;
display: inline-block;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.custom-radio-group :deep(.el-radio-button__inner) {
border: none !important;
border-radius: 10px !important;
margin: 0 1px;
padding: 10px 18px;
display: flex;
align-items: center;
gap: 6px;
font-weight: 500;
font-size: 13px;
color: #6b6d7a;
background: none;
transition: all 0.2s;
}
.custom-radio-group :deep(.el-radio-button__inner:hover) {
color: #667eea;
}
.custom-radio-group :deep(.is-active .el-radio-button__inner) {
background: linear-gradient(135deg, #667eea, #764ba2);
color: #fff;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.35);
}
/* ---- Table Card ---- */
.table-card {
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 16px;
padding: 20px 24px;
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.03),
0 4px 20px rgba(0, 0, 0, 0.04);
}
.custom-table {
overflow: hidden;
border-radius: var(--radius-md);
}
:deep(.el-table__header th) {
background: #f8f9fc;
color: #5b5d68;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.4px;
text-transform: uppercase;
border-bottom: 2px solid #eef0f4 !important;
}
:deep(.el-table__body tr) {
transition: all 0.15s;
}
:deep(.el-table__body tr:hover > td) {
background: linear-gradient(90deg, rgba(102, 126, 234, 0.03), rgba(118, 75, 162, 0.02)) !important;
}
:deep(.el-table__body tr) {
border-bottom: 1px solid #f5f5f8;
}
/* Staggered row tints */
:deep(.el-table__body tr:nth-child(odd) td) {
background: #fdfdfe;
}
:deep(.el-table__body tr:nth-child(even) td) {
background: #fafbfd;
}
.cell-content-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 60px;
}
.data-display-area {
min-height: 24px;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.display-val {
font-weight: 700;
color: #667eea;
font-size: 15px;
font-family: "Courier New", monospace;
}
.display-remark {
font-size: 12px;
color: #5b5d68;
max-width: 180px;
line-height: 1.4;
background: #f8f9fc;
padding: 4px 12px;
border-radius: 6px;
text-align: center;
}
.no-data-placeholder {
color: #c0c4cc;
font-size: 12px;
}
.pure-date {
color: #6b6d7a;
font-weight: 500;
font-family: "Courier New", monospace;
font-size: 12px;
}
.edit-link-btn {
font-size: 12px;
height: auto;
padding: 0;
opacity: 0.6;
color: #667eea;
transition: all 0.15s;
}
.edit-link-btn:hover {
opacity: 1;
color: #4f46e5;
font-weight: 600;
}
/* ---- Pagination ---- */
.pagination-container {
margin-top: 18px;
display: flex;
justify-content: flex-end;
padding-top: 16px;
border-top: 1px solid #f0f1f4;
}
/* ---- Progress ---- */
:deep(.el-progress-bar__outer) {
background: #eef0f4;
border-radius: 10px;
overflow: hidden;
}
:deep(.el-progress-bar__inner) {
background: linear-gradient(90deg, #667eea, #764ba2) !important;
}
:deep(.el-progress-bar__innerText) {
display: flex !important;
align-items: center !important;
justify-content: center !important;
height: 100% !important;
line-height: 1 !important;
vertical-align: middle !important;
}
:deep(.el-progress-bar__innerText svg),
:deep(.el-progress-bar__innerText .el-icon) {
margin: 0 !important;
position: relative !important;
top: -1px !important;
width: 0.8em;
height: 0.8em;
}
:deep(.el-progress) {
display: flex;
align-items: center;
}
.progress-radio-group {
display: flex;
justify-content: center;
margin: 10px 0;
}
.progress-radio-group :deep(.el-radio-button__inner) {
padding: 8px 10px !important;
}
/* ---- Table Overrides ---- */
:deep(.el-table__row) {
height: 100px;
}
:deep(.el-table .cell) {
overflow: visible !important;
}
/* ---- Popover (Inline Edit Flyout) ---- */
:deep(.el-popover) {
border-radius: 16px !important;
border: none !important;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.1),
0 2px 8px rgba(0, 0, 0, 0.06) !important;
padding: 20px !important;
}
.popover-content {
display: flex;
flex-direction: column;
gap: 4px;
}
.pop-header {
font-size: 13px;
font-weight: 700;
margin-bottom: 4px;
color: #1c1c28;
text-align: left;
letter-spacing: -0.01em;
display: flex;
align-items: center;
gap: 6px;
}
.pop-header::before {
content: "";
width: 3px;
height: 14px;
background: linear-gradient(180deg, #667eea, #764ba2);
border-radius: 2px;
}
:deep(.popover-content .el-input__wrapper) {
border-radius: 10px !important;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08) inset !important;
transition: box-shadow 0.2s;
}
:deep(.popover-content .el-input__wrapper:hover) {
box-shadow: 0 0 0 1px rgba(102, 126, 234, 0.3) inset !important;
}
:deep(.popover-content .el-input__wrapper.is-focus) {
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.15), 0 0 0 1px #667eea inset !important;
}
:deep(.popover-content .el-textarea__inner) {
border-radius: 10px !important;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08) inset !important;
transition: box-shadow 0.2s;
font-size: 13px;
}
:deep(.popover-content .el-textarea__inner:focus) {
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.15), 0 0 0 1px #667eea inset !important;
}
.pop-footer {
margin-top: 10px;
text-align: right;
}
.pop-footer :deep(.el-button--primary) {
background: linear-gradient(135deg, #667eea, #764ba2) !important;
border: none !important;
border-radius: 10px !important;
padding: 7px 20px !important;
font-weight: 600 !important;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
transition: all 0.2s;
}
.pop-footer :deep(.el-button--primary:hover) {
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
transform: translateY(-1px);
}
/* Progress radio buttons in popover */
:deep(.popover-content .el-radio-button__inner) {
border-radius: 8px !important;
padding: 6px 12px !important;
}
:deep(.popover-content .el-radio-button__inner:hover) {
color: #667eea !important;
}
:deep(.popover-content .is-active .el-radio-button__inner) {
background: linear-gradient(135deg, #667eea, #764ba2) !important;
border-color: transparent !important;
color: #fff !important;
box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
}
/* ---- Action Buttons Glow ---- */
:deep(.el-table .el-button--primary) {
transition: all 0.2s;
}
:deep(.el-table .el-button--primary:hover) {
box-shadow: 0 0 12px var(--glow-indigo);
}
:deep(.el-table .el-button--success:hover) {
box-shadow: 0 0 12px var(--glow-green);
}
:deep(.el-table .el-button--warning:hover) {
box-shadow: 0 0 12px var(--glow-amber);
}
/* ---- Logout Dropdown ---- */
:deep(.el-dropdown-menu) {
border-radius: 14px !important;
border: none !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 2px 8px rgba(0, 0, 0, 0.06) !important;
padding: 6px !important;
}
:deep(.el-dropdown-menu__item) {
border-radius: 10px !important;
padding: 9px 16px !important;
font-size: 13px !important;
font-weight: 500 !important;
transition: all 0.15s !important;
}
:deep(.el-dropdown-menu__item:hover) {
background: linear-gradient(135deg, rgba(244, 63, 94, 0.06), rgba(244, 63, 94, 0.02)) !important;
color: #e11d48 !important;
}
/* ---- Message Box (Logout Confirm) ---- */
:deep(.el-message-box) {
border-radius: 18px !important;
border: none !important;
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.12), 0 4px 16px rgba(0, 0, 0, 0.06) !important;
padding: 28px !important;
}
:deep(.el-message-box__title) {
font-size: 16px !important;
font-weight: 700 !important;
color: #1c1c28 !important;
}
:deep(.el-message-box__message) {
font-size: 13px !important;
color: #6b6d7a !important;
line-height: 1.6 !important;
}
:deep(.el-message-box__btns) {
padding-top: 12px !important;
}
:deep(.el-message-box__btns .el-button) {
border-radius: 10px !important;
padding: 9px 22px !important;
font-weight: 500 !important;
font-size: 13px !important;
}
:deep(.el-message-box__btns .el-button--primary) {
background: linear-gradient(135deg, #667eea, #764ba2) !important;
border: none !important;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3) !important;
}
:deep(.el-message-box__btns .el-button--primary:hover) {
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4) !important;
}
</style>