995 lines
27 KiB
Vue
995 lines
27 KiB
Vue
<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> 退出登录
|
||
</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>
|