更新若干功能
This commit is contained in:
1 parent
892ba5886d
commit
79d5ae6b66
45 files changed
+6755
-38
No files matched your search
@@ -73,6 +73,14 @@ export function changeBusinessStage(id, stage) {
|
||||
return request({ url: `/backend/crm/business/${id}/stage`, method: "post", data: { stage } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 商机状态流转:1跟进中 / 3已废弃。
|
||||
* 「已转化项目(2)」不走本接口,须用 convertBusinessToProject 完成(会锁定商机)。
|
||||
*/
|
||||
export function changeBusinessStatus(id, status) {
|
||||
return request({ url: `/backend/crm/business/${id}/status`, method: "post", data: { status } });
|
||||
}
|
||||
|
||||
/* --------------------------------- 项目 --------------------------------- */
|
||||
|
||||
export function getProjectList(params) {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// OA 工作日报
|
||||
// 响应拦截器只返回 {code, data, msg} 包装,调用方需自行取 res.data
|
||||
|
||||
// 当前用户身份与可见范围:{ user_id, org_id, is_admin, can_view_team, window_start, window_end, backfill_days }
|
||||
export function getDailyReportProfile() {
|
||||
return request({
|
||||
url: '/backend/oa/dailyreport/profile',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 我的日报列表:params = { page, pageSize, start_date, end_date }
|
||||
// data = { list, total, summary: { month_total, month_submitted, today_status, today_report_id, window_start, window_end } }
|
||||
export function getMyDailyReports(params) {
|
||||
return request({
|
||||
url: '/backend/oa/dailyreport/mine/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 保存日报:{ date, today_done, tomorrow_plan, issues, attachments, submit }
|
||||
// submit=true 提交,false 存草稿;同一人同一天只会有一条记录
|
||||
export function saveDailyReport(data) {
|
||||
return request({
|
||||
url: '/backend/oa/dailyreport/save',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 日报详情:{ report, comments, can_edit, can_delete, can_comment }
|
||||
export function getDailyReportDetail(id) {
|
||||
return request({
|
||||
url: `/backend/oa/dailyreport/detail/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteDailyReport(id) {
|
||||
return request({
|
||||
url: `/backend/oa/dailyreport/delete/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 团队汇总:params = { date, org_id, keyword, page, pageSize }
|
||||
// data = { date, list, total, stats: { total, submitted, missing, rate } }
|
||||
export function getTeamDailyReports(params) {
|
||||
return request({
|
||||
url: '/backend/oa/dailyreport/team/status',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 团队筛选部门:data = { list: [{ id, parent_id, name, member_count }] }
|
||||
export function getTeamDailyReportOrgs() {
|
||||
return request({
|
||||
url: '/backend/oa/dailyreport/team/orgs',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 评论 / 回复:{ report_id, content, reply_to_id }
|
||||
export function addDailyReportComment(data) {
|
||||
return request({
|
||||
url: '/backend/oa/dailyreport/comment',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteDailyReportComment(id) {
|
||||
return request({
|
||||
url: `/backend/oa/dailyreport/comment/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
// 平台「小程序配置」中已启用的小程序(含二维码地址,公开接口无需登录)
|
||||
export function getSiteMiniApps() {
|
||||
return request({
|
||||
url: '/site/miniapp',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<el-aside
|
||||
:width="width"
|
||||
:class="['common-aside', { 'is-collapsed': isCollapse }]"
|
||||
:class="[
|
||||
'common-aside',
|
||||
{ 'is-collapsed': isCollapse, 'has-footer': miniApps.length > 0 },
|
||||
]"
|
||||
>
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-spinner">
|
||||
@@ -164,6 +167,26 @@
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
</el-menu>
|
||||
|
||||
<!-- 底部:平台「小程序配置」里的小程序码,吸附侧栏底部 -->
|
||||
<div v-if="miniApps.length" class="aside-footer">
|
||||
<div class="mini-qrcode" :class="{ 'is-collapsed': isCollapse }">
|
||||
<el-image
|
||||
:src="miniApps[0].qrcodeSrc"
|
||||
:preview-src-list="miniQrcodePreview"
|
||||
:initial-index="0"
|
||||
preview-teleported
|
||||
hide-on-click-modal
|
||||
fit="contain"
|
||||
class="mini-qrcode-img"
|
||||
:title="miniApps[0].name || '小程序码'"
|
||||
/>
|
||||
<div v-if="!isCollapse" class="mini-qrcode-name">
|
||||
{{ miniApps[0].name || "小程序"
|
||||
}}<span v-if="miniApps.length > 1"> 等 {{ miniApps.length }} 个</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-aside>
|
||||
</template>
|
||||
|
||||
@@ -172,6 +195,7 @@ import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { Document, Warning } from "@element-plus/icons-vue";
|
||||
import { useAllDataStore, useMenuStore } from "@/stores";
|
||||
import { getSiteMiniApps } from "@/api/site";
|
||||
|
||||
const emit = defineEmits(["menu-click"]);
|
||||
|
||||
@@ -195,6 +219,37 @@ const activeBgColor = ref("#3973FF");
|
||||
|
||||
const currentModuleId = ref(null);
|
||||
|
||||
// ---------- 底部小程序码 ----------
|
||||
// 来源:平台端「系统设置 → 小程序配置」中已启用且上传了二维码的小程序,
|
||||
// 通过公开接口 /site/miniapp 读取;相对地址按接口基址补全。
|
||||
const miniApps = ref([]);
|
||||
const miniQrcodePreview = computed(() =>
|
||||
miniApps.value.map((app) => app.qrcodeSrc),
|
||||
);
|
||||
|
||||
const resolveAssetUrl = (url) => {
|
||||
const value = String(url || "").trim().replace(/\\/g, "/");
|
||||
if (!value) return "";
|
||||
if (/^(https?:\/\/|data:|blob:)/i.test(value)) return value;
|
||||
const base = String(import.meta.env.VITE_API_BASE_URL || "").replace(
|
||||
/\/$/,
|
||||
"",
|
||||
);
|
||||
return `${base}/${value.replace(/^\/+/, "")}`;
|
||||
};
|
||||
|
||||
const fetchMiniApps = async () => {
|
||||
try {
|
||||
const res = await getSiteMiniApps();
|
||||
if (res.code !== 200) return;
|
||||
miniApps.value = (res.data?.list || [])
|
||||
.filter((app) => String(app.qrcode_url || "").trim())
|
||||
.map((app) => ({ ...app, qrcodeSrc: resolveAssetUrl(app.qrcode_url) }));
|
||||
} catch (error) {
|
||||
// 侧栏二维码属于辅助信息,加载失败静默忽略,不影响菜单
|
||||
}
|
||||
};
|
||||
|
||||
const findMenuItem = (menus, targetIndex) => {
|
||||
for (const menu of menus) {
|
||||
if (menu.path === targetIndex) {
|
||||
@@ -445,6 +500,8 @@ watch(
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
fetchMiniApps();
|
||||
|
||||
if (!menuStore.menus || menuStore.menus.length === 0) {
|
||||
setTimeout(() => {
|
||||
fetchMenus();
|
||||
@@ -462,6 +519,8 @@ onUnmounted(() => {
|
||||
<style scoped lang="less">
|
||||
.common-aside {
|
||||
height: 100%;
|
||||
// 底部二维码高度:菜单区据此预留滚动空间(无二维码时为 0)
|
||||
--aside-footer-height: 0px;
|
||||
transition:
|
||||
width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
background-color 0.3s ease;
|
||||
@@ -517,11 +576,13 @@ h3 {
|
||||
// 菜单样式
|
||||
:deep(.el-menu) {
|
||||
border-right: none;
|
||||
// 标题已移到 el-menu 之外,这里只减掉标题区高度
|
||||
// 标题已移到 el-menu 之外,这里只减掉标题区高度;
|
||||
// 底部再预留 footer 高度,保证最后一项能滚到二维码上方
|
||||
height: calc(100% - 60px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 16px 8px;
|
||||
padding-bottom: calc(16px + var(--aside-footer-height));
|
||||
background: transparent;
|
||||
|
||||
// 自定义滚动条样式
|
||||
@@ -707,6 +768,84 @@ h3 {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
// 底部小程序码:绝对定位吸附侧栏底部(Main.vue 将 el-aside 强制为 display:block,
|
||||
// 因此不用 flex 布局,避免样式被覆盖后失效)
|
||||
.common-aside.has-footer {
|
||||
--aside-footer-height: 148px;
|
||||
}
|
||||
|
||||
.common-aside.is-collapsed.has-footer {
|
||||
--aside-footer-height: 72px;
|
||||
}
|
||||
|
||||
.aside-footer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
height: var(--aside-footer-height);
|
||||
box-sizing: border-box;
|
||||
padding: 12px 8px 14px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.16);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
html.dark & {
|
||||
border-top-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.mini-qrcode {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.mini-qrcode-img {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
padding: 4px;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18);
|
||||
transition: width 0.3s ease, height 0.3s ease;
|
||||
|
||||
// el-image 的 img 不自带尺寸,需显式铺满容器(fit=contain 保留比例)
|
||||
:deep(.el-image__inner) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.mini-qrcode-name {
|
||||
max-width: 180px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
html.dark & {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠态只留二维码,尺寸收紧避免撑破 64px 窄栏
|
||||
.mini-qrcode.is-collapsed {
|
||||
.mini-qrcode-img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 2px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
// 注意:手机端(≤768px)侧栏的抽屉式布局由 Main.vue 统一控制(fixed 定位 + transform 动画),
|
||||
// 此处不要再对 .common-aside 设置 width,避免覆盖父级布局逻辑。
|
||||
|
||||
@@ -7,11 +7,37 @@
|
||||
@update:model-value="handleClose"
|
||||
@opened="loadAll"
|
||||
>
|
||||
<!-- 标题 + 右上角阶段流转(与合同 / 项目详情一致) -->
|
||||
<!-- 标题 + 右上角状态 / 阶段流转(与合同 / 项目详情一致) -->
|
||||
<template #header>
|
||||
<div class="drawer-header">
|
||||
<span class="drawer-title">{{ business?.business_name || "商机详情" }}</span>
|
||||
<div v-if="business" class="drawer-actions">
|
||||
<!-- 状态流转:跟不下去的商机可废弃,废弃后可恢复为跟进中 -->
|
||||
<el-dropdown
|
||||
v-if="statusOptionsExcept(business.status).length"
|
||||
trigger="click"
|
||||
@command="(cmd) => handleChangeStatus(cmd)"
|
||||
>
|
||||
<el-button
|
||||
link
|
||||
type="info"
|
||||
size="small"
|
||||
:disabled="Number(business.locked) === 1 || Number(business.status) === 2"
|
||||
>
|
||||
状态流转<el-icon style="margin-left: 2px"><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="i in statusOptionsExcept(business.status)"
|
||||
:key="i.value"
|
||||
:command="i.value"
|
||||
>
|
||||
{{ i.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dropdown
|
||||
v-if="stageOptionsExcept(business.stage).length"
|
||||
trigger="click"
|
||||
@@ -21,7 +47,11 @@
|
||||
link
|
||||
type="warning"
|
||||
size="small"
|
||||
:disabled="Number(business.locked) === 1 || Number(business.status) === 2"
|
||||
:disabled="
|
||||
Number(business.locked) === 1 ||
|
||||
Number(business.status) === 2 ||
|
||||
Number(business.status) === 3
|
||||
"
|
||||
>
|
||||
阶段流转<el-icon style="margin-left: 2px"><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
@@ -42,6 +72,14 @@
|
||||
</template>
|
||||
|
||||
<div v-if="business" class="biz-detail">
|
||||
<el-alert
|
||||
v-if="Number(business.status) === 3"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="该商机已废弃,可点击右上角「状态流转」恢复为「跟进中」后继续推进"
|
||||
style="margin-bottom: 12px"
|
||||
/>
|
||||
<el-tabs v-model="activeTab" tab-position="left" class="detail-tabs">
|
||||
<!-- 基本信息 -->
|
||||
<el-tab-pane label="基本信息" name="basic">
|
||||
@@ -98,8 +136,13 @@
|
||||
import { ref, reactive, watch } from "vue";
|
||||
import { ArrowDown } from "@element-plus/icons-vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { changeBusinessStage } from "@/api/crmPipeline";
|
||||
import { BUSINESS_STAGE_OPTIONS, businessStageText } from "../../dict";
|
||||
import { changeBusinessStage, changeBusinessStatus } from "@/api/crmPipeline";
|
||||
import {
|
||||
BUSINESS_STAGE_OPTIONS,
|
||||
BUSINESS_STATUS_OPTIONS,
|
||||
businessStageText,
|
||||
businessStatusText,
|
||||
} from "../../dict";
|
||||
import DetailBasic from "./detail_basic.vue";
|
||||
import DetailContact from "./detail_contact.vue";
|
||||
import DetailFollow from "./detail_follow.vue";
|
||||
@@ -139,6 +182,29 @@ function handleClose() {
|
||||
emit("update:visible", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 可流转的状态:排除当前状态与「已转化项目」。
|
||||
* 已转化必须走「转项目」流程(会锁定商机),不能只改状态。
|
||||
*/
|
||||
function statusOptionsExcept(cur) {
|
||||
return BUSINESS_STATUS_OPTIONS.filter(
|
||||
(i) => String(i.value) !== String(cur) && String(i.value) !== "2"
|
||||
);
|
||||
}
|
||||
|
||||
/** 状态流转:仅更新状态字段,成功后同步本地展示并通知列表刷新 */
|
||||
async function handleChangeStatus(status) {
|
||||
if (!props.business?.id) return;
|
||||
try {
|
||||
await changeBusinessStatus(props.business.id, Number(status));
|
||||
ElMessage.success(`状态已更新为「${businessStatusText(status)}」`);
|
||||
props.business.status = Number(status);
|
||||
emit("refresh");
|
||||
} catch (e) {
|
||||
ElMessage.error(e.message || "状态更新失败");
|
||||
}
|
||||
}
|
||||
|
||||
/** 可流转的阶段:排除当前阶段 */
|
||||
function stageOptionsExcept(cur) {
|
||||
return BUSINESS_STAGE_OPTIONS.filter((i) => String(i.value) !== String(cur));
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
<el-table-column label="下次联系时间" width="160" align="center">
|
||||
<template #default="{ row }">{{ formatDateTime(row.next_contact_time) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||
<el-table-column label="操作" width="280" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" :disabled="row.locked === 1" @click="openEdit(row)">
|
||||
编辑
|
||||
@@ -91,11 +91,31 @@
|
||||
link
|
||||
type="warning"
|
||||
size="small"
|
||||
:disabled="row.locked === 1 || row.status === 2"
|
||||
:disabled="row.locked === 1 || row.status === 2 || row.status === 3"
|
||||
@click="openConvert(row)"
|
||||
>
|
||||
转项目
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 3"
|
||||
link
|
||||
type="success"
|
||||
size="small"
|
||||
:disabled="row.locked === 1"
|
||||
@click="handleRestore(row)"
|
||||
>
|
||||
恢复
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
link
|
||||
type="info"
|
||||
size="small"
|
||||
:disabled="row.locked === 1 || row.status === 2"
|
||||
@click="handleDiscard(row)"
|
||||
>
|
||||
废弃
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@@ -138,7 +158,7 @@
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Search, Refresh } from "@element-plus/icons-vue";
|
||||
import { getBusinessList, deleteBusiness } from "@/api/crmPipeline";
|
||||
import { getBusinessList, deleteBusiness, changeBusinessStatus } from "@/api/crmPipeline";
|
||||
import BusinessEdit from "./components/edit.vue";
|
||||
import BusinessConvert from "./components/convert.vue";
|
||||
import BusinessDetail from "./components/detail.vue";
|
||||
@@ -228,6 +248,36 @@ function handleConvertSuccess() {
|
||||
ElMessage.success("商机已转化为项目,可在项目管理中继续跟进");
|
||||
}
|
||||
|
||||
/** 废弃商机:保留全部过程数据,仅把状态置为已废弃,可随时恢复 */
|
||||
async function handleDiscard(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定废弃商机「${row.business_name}」吗?废弃后可随时恢复为「跟进中」。`,
|
||||
"废弃确认",
|
||||
{ type: "warning" }
|
||||
);
|
||||
await changeBusinessStatus(row.id, 3);
|
||||
ElMessage.success("商机已废弃");
|
||||
fetchList();
|
||||
} catch (e) {
|
||||
if (e !== "cancel") ElMessage.error(e.message || "废弃失败");
|
||||
}
|
||||
}
|
||||
|
||||
/** 恢复商机:把已废弃的商机恢复为跟进中 */
|
||||
async function handleRestore(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定将商机「${row.business_name}」恢复为「跟进中」吗?`, "恢复确认", {
|
||||
type: "info",
|
||||
});
|
||||
await changeBusinessStatus(row.id, 1);
|
||||
ElMessage.success("商机已恢复为跟进中");
|
||||
fetchList();
|
||||
} catch (e) {
|
||||
if (e !== "cancel") ElMessage.error(e.message || "恢复失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除商机「${row.business_name}」吗?删除后不可恢复。`, "删除确认", {
|
||||
|
||||
@@ -63,6 +63,14 @@
|
||||
title="该线索已转化为商机并锁定,后续请在商机管理中操作;商机详情会按阶段展示线索 + 商机的联系人、跟进记录与附件"
|
||||
style="margin-bottom: 12px"
|
||||
/>
|
||||
<el-alert
|
||||
v-if="Number(clue.status) === 3"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="该线索已废弃,可点击右上角「状态流转」恢复为「跟进中」后继续跟进"
|
||||
style="margin-bottom: 12px"
|
||||
/>
|
||||
<el-tabs v-model="activeTab" tab-position="left" class="detail-tabs">
|
||||
<!-- 基本信息 -->
|
||||
<el-tab-pane label="基本信息" name="basic">
|
||||
|
||||
@@ -84,18 +84,38 @@
|
||||
<el-table-column label="创建时间" width="160" align="center">
|
||||
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<!-- 编辑 / 删除已收口到「线索详情」内,列表只保留转化 -->
|
||||
<!-- 编辑 / 删除已收口到「线索详情」内,列表保留转化 / 废弃 -->
|
||||
<el-button
|
||||
link
|
||||
type="warning"
|
||||
size="small"
|
||||
:disabled="row.locked === 1 || row.status === 2"
|
||||
:disabled="row.locked === 1 || row.status === 2 || row.status === 3"
|
||||
@click="openConvert(row)"
|
||||
>
|
||||
转化
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 3"
|
||||
link
|
||||
type="success"
|
||||
size="small"
|
||||
:disabled="row.locked === 1"
|
||||
@click="handleRestore(row)"
|
||||
>
|
||||
恢复
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
link
|
||||
type="info"
|
||||
size="small"
|
||||
:disabled="row.locked === 1 || row.status === 2"
|
||||
@click="handleDiscard(row)"
|
||||
>
|
||||
废弃
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="暂无线索" :image-size="80" /></template>
|
||||
@@ -130,7 +150,7 @@
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Search, Refresh } from "@element-plus/icons-vue";
|
||||
import { getClueList, deleteClue } from "@/api/crmPipeline";
|
||||
import { getClueList, deleteClue, changeClueStatus } from "@/api/crmPipeline";
|
||||
import ClueEdit from "./components/edit.vue";
|
||||
import ClueConvert from "./components/convert.vue";
|
||||
import ClueDetail from "./components/detail.vue";
|
||||
@@ -218,6 +238,36 @@ function handleConvertSuccess() {
|
||||
ElMessage.success("线索已转化,可在商机管理中继续跟进");
|
||||
}
|
||||
|
||||
/** 废弃线索:保留全部过程数据,仅把状态置为已废弃,可随时恢复 */
|
||||
async function handleDiscard(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定废弃线索「${row.clue_name}」吗?废弃后可随时恢复为「跟进中」。`,
|
||||
"废弃确认",
|
||||
{ type: "warning" }
|
||||
);
|
||||
await changeClueStatus(row.id, 3);
|
||||
ElMessage.success("线索已废弃");
|
||||
fetchList();
|
||||
} catch (e) {
|
||||
if (e !== "cancel") ElMessage.error(e.message || "废弃失败");
|
||||
}
|
||||
}
|
||||
|
||||
/** 恢复线索:把已废弃的线索恢复为跟进中 */
|
||||
async function handleRestore(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定将线索「${row.clue_name}」恢复为「跟进中」吗?`, "恢复确认", {
|
||||
type: "info",
|
||||
});
|
||||
await changeClueStatus(row.id, 1);
|
||||
ElMessage.success("线索已恢复为跟进中");
|
||||
fetchList();
|
||||
} catch (e) {
|
||||
if (e !== "cancel") ElMessage.error(e.message || "恢复失败");
|
||||
}
|
||||
}
|
||||
|
||||
/** 详情内编辑:收起详情后打开编辑弹窗(详情数据即当前行) */
|
||||
function handleDetailEdit() {
|
||||
detailVisible.value = false;
|
||||
|
||||
@@ -176,11 +176,11 @@ export const BUSINESS_STAGE_OPTIONS = [
|
||||
{ label: "输单", value: "6" },
|
||||
];
|
||||
|
||||
/** 商机状态 */
|
||||
/** 商机状态:跳过一半无法继续推进的商机可废弃(可恢复为跟进中) */
|
||||
export const BUSINESS_STATUS_OPTIONS = [
|
||||
{ label: "跟进中", value: "1" },
|
||||
{ label: "已转化项目", value: "2" },
|
||||
{ label: "已关闭", value: "3" },
|
||||
{ label: "已废弃", value: "3" },
|
||||
];
|
||||
|
||||
/** 项目状态 */
|
||||
|
||||
@@ -0,0 +1,498 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
title="日报详情"
|
||||
size="600px"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<div v-loading="loading" class="report-detail">
|
||||
<template v-if="detail">
|
||||
<div class="detail-head">
|
||||
<el-avatar :size="40" class="author-avatar">
|
||||
{{ (detail.report.user_name || '?').slice(0, 1) }}
|
||||
</el-avatar>
|
||||
<div class="head-main">
|
||||
<div class="author-line">
|
||||
<span class="author-name">{{ detail.report.user_name }}</span>
|
||||
<el-tag
|
||||
:type="detail.report.status === 1 ? 'success' : 'info'"
|
||||
size="small"
|
||||
>
|
||||
{{ detail.report.status === 1 ? '已提交' : '草稿' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="author-sub">
|
||||
{{ detail.report.org_name || '未分配部门' }} ·
|
||||
{{ detail.report.report_date }}
|
||||
<template v-if="detail.report.submit_time">
|
||||
· 提交于 {{ detail.report.submit_time }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="head-actions">
|
||||
<el-button
|
||||
v-if="detail.can_edit"
|
||||
link
|
||||
type="primary"
|
||||
@click="$emit('edit', detail.report)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="detail.can_delete"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDeleteReport"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="detail-section">
|
||||
<h4>今日完成</h4>
|
||||
<div class="section-text">{{ detail.report.today_done || '—' }}</div>
|
||||
</section>
|
||||
<section class="detail-section">
|
||||
<h4>明日计划</h4>
|
||||
<div class="section-text">{{ detail.report.tomorrow_plan || '—' }}</div>
|
||||
</section>
|
||||
<section class="detail-section">
|
||||
<h4>问题与建议</h4>
|
||||
<div class="section-text">{{ detail.report.issues || '—' }}</div>
|
||||
</section>
|
||||
|
||||
<section v-if="detail.report.attachments?.length" class="detail-section">
|
||||
<h4>附件</h4>
|
||||
<div class="attachment-list">
|
||||
<a
|
||||
v-for="file in detail.report.attachments"
|
||||
:key="file.url"
|
||||
:href="file.url"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="attachment-item"
|
||||
>
|
||||
<el-icon><Paperclip /></el-icon>
|
||||
<span class="file-name">{{ file.name || '附件' }}</span>
|
||||
<span v-if="file.size" class="file-size">{{ formatSize(file.size) }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="detail-section comment-section">
|
||||
<h4>评论 <span class="comment-count">{{ detail.comments.length }}</span></h4>
|
||||
|
||||
<el-empty
|
||||
v-if="!detail.comments.length"
|
||||
description="还没有评论"
|
||||
:image-size="60"
|
||||
/>
|
||||
<div v-else class="comment-list">
|
||||
<div
|
||||
v-for="item in detail.comments"
|
||||
:key="item.id"
|
||||
class="comment-item"
|
||||
:class="{ 'is-reply': item.reply_to_id > 0 }"
|
||||
>
|
||||
<div class="comment-head">
|
||||
<span class="comment-user">{{ item.user_name }}</span>
|
||||
<span v-if="item.reply_to_name" class="reply-to">
|
||||
回复 {{ item.reply_to_name }}
|
||||
</span>
|
||||
<span class="comment-time">{{ item.create_time }}</span>
|
||||
</div>
|
||||
<div class="comment-content">{{ item.content }}</div>
|
||||
<div class="comment-actions">
|
||||
<el-button link type="primary" @click="startReply(item)">
|
||||
回复
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="item.can_delete"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDeleteComment(item)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="detail.can_comment" class="comment-editor">
|
||||
<div v-if="replyTo" class="reply-banner">
|
||||
正在回复 {{ replyTo.user_name }}
|
||||
<el-button link type="primary" @click="replyTo = null">取消</el-button>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="commentText"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
:placeholder="replyTo ? `回复 ${replyTo.user_name}` : '写下你的评论或建议'"
|
||||
/>
|
||||
<div class="editor-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
@click="submitComment"
|
||||
>
|
||||
发表评论
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="detail.report.status !== 1" class="comment-disabled">
|
||||
日报提交后才能评论
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Paperclip } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDailyReportDetail,
|
||||
addDailyReportComment,
|
||||
deleteDailyReportComment,
|
||||
deleteDailyReport
|
||||
} from '@/api/oaDailyReport'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
reportId: { type: [Number, String], default: 0 }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'edit', 'deleted'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const detail = ref(null)
|
||||
const commentText = ref('')
|
||||
const replyTo = ref(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.reportId],
|
||||
([open]) => {
|
||||
if (open && props.reportId) loadDetail()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
async function loadDetail() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getDailyReportDetail(props.reportId)
|
||||
detail.value = res?.data || null
|
||||
if (!detail.value) ElMessage.error(res?.msg || '日报加载失败')
|
||||
commentText.value = ''
|
||||
replyTo.value = null
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '日报加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function startReply(comment) {
|
||||
replyTo.value = { id: comment.id, user_name: comment.user_name }
|
||||
}
|
||||
|
||||
async function submitComment() {
|
||||
const content = commentText.value.trim()
|
||||
if (!content) {
|
||||
ElMessage.warning('请输入评论内容')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await addDailyReportComment({
|
||||
report_id: Number(props.reportId),
|
||||
content,
|
||||
reply_to_id: replyTo.value?.id || 0
|
||||
})
|
||||
ElMessage.success('评论成功')
|
||||
commentText.value = ''
|
||||
replyTo.value = null
|
||||
await loadDetail()
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '评论失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteComment(comment) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定删除这条评论吗?', '删除评论', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await deleteDailyReportComment(comment.id)
|
||||
ElMessage.success('评论已删除')
|
||||
await loadDetail()
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteReport() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'删除后不可恢复,且该日期的评论会一并删除。确定删除这条日报吗?',
|
||||
'删除日报',
|
||||
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await deleteDailyReport(props.reportId)
|
||||
ElMessage.success('日报已删除')
|
||||
visible.value = false
|
||||
emit('deleted', Number(props.reportId))
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
function formatSize(size) {
|
||||
const n = Number(size) || 0
|
||||
if (n < 1024) return `${n} B`
|
||||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
|
||||
return `${(n / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
function handleClosed() {
|
||||
detail.value = null
|
||||
commentText.value = ''
|
||||
replyTo.value = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.report-detail {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.detail-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.author-avatar {
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.head-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.author-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.author-sub {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.head-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px dashed #ebeef5;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
|
||||
.section-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: #303133;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
color: #409eff;
|
||||
text-decoration: none;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.comment-count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.comment-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
background: #f7f8fa;
|
||||
|
||||
&.is-reply {
|
||||
margin-left: 24px;
|
||||
background: #f2f6fc;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.comment-user {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.reply-to {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.comment-time {
|
||||
margin-left: auto;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: #303133;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.comment-editor {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.reply-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.comment-disabled {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
html.dark & {
|
||||
.detail-head,
|
||||
.detail-section {
|
||||
border-color: var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.author-name,
|
||||
.section-text,
|
||||
.comment-user,
|
||||
.comment-content {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.author-sub,
|
||||
.detail-section h4,
|
||||
.comment-time,
|
||||
.file-size,
|
||||
.comment-disabled,
|
||||
.reply-banner {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.attachment-item,
|
||||
.comment-item {
|
||||
background: var(--el-fill-color-light);
|
||||
border-color: var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,286 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="isEdit ? '编辑日报' : '写日报'"
|
||||
width="720px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<el-form v-loading="loading" :model="form" label-width="88px">
|
||||
<el-form-item label="日报日期" required>
|
||||
<el-date-picker
|
||||
v-model="form.date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
:clearable="false"
|
||||
disabled
|
||||
style="width: 180px"
|
||||
/>
|
||||
<span class="date-tip">
|
||||
可补写 {{ windowStart }} 至 {{ windowEnd }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="今日完成">
|
||||
<el-input
|
||||
v-model="form.today_done"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
maxlength="5000"
|
||||
show-word-limit
|
||||
placeholder="今天完成了哪些工作?可按条列出"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="明日计划">
|
||||
<el-input
|
||||
v-model="form.tomorrow_plan"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="5000"
|
||||
show-word-limit
|
||||
placeholder="明天计划推进哪些事项?"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="问题与建议">
|
||||
<el-input
|
||||
v-model="form.issues"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="5000"
|
||||
show-word-limit
|
||||
placeholder="遇到的问题、需要的支持或改进建议(选填)"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="附件">
|
||||
<div class="attachment-block">
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:http-request="handleUpload"
|
||||
accept=".jpg,.jpeg,.png,.gif,.webp,.bmp,.pdf"
|
||||
>
|
||||
<el-button :icon="Upload" :loading="uploading">上传附件</el-button>
|
||||
</el-upload>
|
||||
<span class="upload-tip">图片或 PDF,单个不超过 10MB</span>
|
||||
<div v-if="form.attachments.length" class="attachment-list">
|
||||
<div
|
||||
v-for="(file, index) in form.attachments"
|
||||
:key="file.url || index"
|
||||
class="attachment-item"
|
||||
>
|
||||
<el-icon><Paperclip /></el-icon>
|
||||
<a :href="file.url" target="_blank" rel="noopener" class="file-name">
|
||||
{{ file.name || '附件' }}
|
||||
</a>
|
||||
<el-button link type="danger" @click="removeAttachment(index)">
|
||||
移除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button :loading="savingDraft" @click="handleSave(false)">保存草稿</el-button>
|
||||
<el-button type="primary" :loading="savingSubmit" @click="handleSave(true)">
|
||||
提交日报
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Upload, Paperclip } from '@element-plus/icons-vue'
|
||||
import { uploadFile } from '@/api/file'
|
||||
import { saveDailyReport } from '@/api/oaDailyReport'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
// 传入已有日报时为编辑,否则为新建
|
||||
report: { type: Object, default: null },
|
||||
defaultDate: { type: String, default: '' },
|
||||
windowStart: { type: String, default: '' },
|
||||
windowEnd: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'saved'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const isEdit = computed(() => Boolean(props.report?.id))
|
||||
const loading = ref(false)
|
||||
const uploading = ref(false)
|
||||
const savingDraft = ref(false)
|
||||
const savingSubmit = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
date: '',
|
||||
today_done: '',
|
||||
tomorrow_plan: '',
|
||||
issues: '',
|
||||
attachments: []
|
||||
})
|
||||
|
||||
function fillForm(report) {
|
||||
form.date = report?.report_date || props.defaultDate || todayStr()
|
||||
form.today_done = report?.today_done || ''
|
||||
form.tomorrow_plan = report?.tomorrow_plan || ''
|
||||
form.issues = report?.issues || ''
|
||||
form.attachments = Array.isArray(report?.attachments)
|
||||
? report.attachments.map((item) => ({ ...item }))
|
||||
: []
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(open) => {
|
||||
if (open) fillForm(props.report)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function todayStr() {
|
||||
const now = new Date()
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`
|
||||
}
|
||||
|
||||
async function handleUpload(options) {
|
||||
const file = options.file
|
||||
if (!file) return
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
uploading.value = true
|
||||
try {
|
||||
const res = await uploadFile(fd, { scope: 'user' })
|
||||
const data = res?.data || {}
|
||||
if (!data.url) throw new Error(res?.msg || '上传失败')
|
||||
if (form.attachments.length >= 20) {
|
||||
ElMessage.warning('附件最多 20 个')
|
||||
return
|
||||
}
|
||||
form.attachments.push({
|
||||
id: data.id || 0,
|
||||
url: data.url,
|
||||
name: data.name || file.name,
|
||||
size: file.size || 0
|
||||
})
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '上传失败')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function removeAttachment(index) {
|
||||
form.attachments.splice(index, 1)
|
||||
}
|
||||
|
||||
async function handleSave(submit) {
|
||||
if (!form.date) {
|
||||
ElMessage.warning('请选择日报日期')
|
||||
return
|
||||
}
|
||||
const hasContent =
|
||||
form.today_done.trim() || form.tomorrow_plan.trim() || form.issues.trim()
|
||||
if (!hasContent) {
|
||||
ElMessage.warning('请至少填写今日完成、明日计划、问题与建议中的一项')
|
||||
return
|
||||
}
|
||||
|
||||
const setLoading = submit ? savingSubmit : savingDraft
|
||||
setLoading.value = true
|
||||
try {
|
||||
const res = await saveDailyReport({
|
||||
date: form.date,
|
||||
today_done: form.today_done,
|
||||
tomorrow_plan: form.tomorrow_plan,
|
||||
issues: form.issues,
|
||||
attachments: JSON.stringify(form.attachments),
|
||||
submit
|
||||
})
|
||||
ElMessage.success(submit ? '日报已提交' : '草稿已保存')
|
||||
emit('saved', res?.data || null)
|
||||
visible.value = false
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '保存失败')
|
||||
} finally {
|
||||
setLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleClosed() {
|
||||
loading.value = false
|
||||
uploading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.date-tip {
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.attachment-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #409eff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
html.dark & {
|
||||
.date-tip,
|
||||
.upload-tip {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
background: var(--el-fill-color-light);
|
||||
border-color: var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.file-name {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,487 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="report-view">
|
||||
<template v-if="detail">
|
||||
<div class="detail-head">
|
||||
<el-avatar :size="40" class="author-avatar">
|
||||
{{ (detail.report.user_name || '?').slice(0, 1) }}
|
||||
</el-avatar>
|
||||
<div class="head-main">
|
||||
<div class="author-line">
|
||||
<span class="author-name">{{ detail.report.user_name }}</span>
|
||||
<el-tag
|
||||
:type="detail.report.status === 1 ? 'success' : 'info'"
|
||||
size="small"
|
||||
>
|
||||
{{ detail.report.status === 1 ? '已提交' : '草稿' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="author-sub">
|
||||
{{ detail.report.org_name || '未分配部门' }} ·
|
||||
{{ detail.report.report_date }}
|
||||
<template v-if="detail.report.submit_time">
|
||||
· 提交于 {{ detail.report.submit_time }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="head-actions">
|
||||
<el-button
|
||||
v-if="detail.can_edit"
|
||||
link
|
||||
type="primary"
|
||||
@click="$emit('edit', detail.report)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="detail.can_delete"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDeleteReport"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="detail-section">
|
||||
<h4>今日完成</h4>
|
||||
<div class="section-text">{{ detail.report.today_done || '—' }}</div>
|
||||
</section>
|
||||
<section class="detail-section">
|
||||
<h4>明日计划</h4>
|
||||
<div class="section-text">{{ detail.report.tomorrow_plan || '—' }}</div>
|
||||
</section>
|
||||
<section class="detail-section">
|
||||
<h4>问题与建议</h4>
|
||||
<div class="section-text">{{ detail.report.issues || '—' }}</div>
|
||||
</section>
|
||||
|
||||
<section v-if="detail.report.attachments?.length" class="detail-section">
|
||||
<h4>附件</h4>
|
||||
<div class="attachment-list">
|
||||
<a
|
||||
v-for="file in detail.report.attachments"
|
||||
:key="file.url"
|
||||
:href="file.url"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="attachment-item"
|
||||
>
|
||||
<el-icon><Paperclip /></el-icon>
|
||||
<span class="file-name">{{ file.name || '附件' }}</span>
|
||||
<span v-if="file.size" class="file-size">{{ formatSize(file.size) }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="detail-section comment-section">
|
||||
<h4>评论 <span class="comment-count">{{ detail.comments.length }}</span></h4>
|
||||
|
||||
<el-empty
|
||||
v-if="!detail.comments.length"
|
||||
description="还没有评论"
|
||||
:image-size="60"
|
||||
/>
|
||||
<div v-else class="comment-list">
|
||||
<div
|
||||
v-for="item in detail.comments"
|
||||
:key="item.id"
|
||||
class="comment-item"
|
||||
:class="{ 'is-reply': item.reply_to_id > 0 }"
|
||||
>
|
||||
<div class="comment-head">
|
||||
<span class="comment-user">{{ item.user_name }}</span>
|
||||
<span v-if="item.reply_to_name" class="reply-to">
|
||||
回复 {{ item.reply_to_name }}
|
||||
</span>
|
||||
<span class="comment-time">{{ item.create_time }}</span>
|
||||
</div>
|
||||
<div class="comment-content">{{ item.content }}</div>
|
||||
<div class="comment-actions">
|
||||
<el-button link type="primary" @click="startReply(item)">
|
||||
回复
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="item.can_delete"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDeleteComment(item)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="detail.can_comment" class="comment-editor">
|
||||
<div v-if="replyTo" class="reply-banner">
|
||||
正在回复 {{ replyTo.user_name }}
|
||||
<el-button link type="primary" @click="replyTo = null">取消</el-button>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="commentText"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
:placeholder="replyTo ? `回复 ${replyTo.user_name}` : '写下你的评论或建议'"
|
||||
/>
|
||||
<div class="editor-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
@click="submitComment"
|
||||
>
|
||||
发表评论
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="detail.report.status !== 1" class="comment-disabled">
|
||||
日报提交后才能评论
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<el-empty
|
||||
v-else-if="!loading"
|
||||
description="选择左侧日报查看详情"
|
||||
:image-size="80"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Paperclip } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDailyReportDetail,
|
||||
addDailyReportComment,
|
||||
deleteDailyReportComment,
|
||||
deleteDailyReport
|
||||
} from '@/api/oaDailyReport'
|
||||
|
||||
const props = defineProps({
|
||||
reportId: { type: [Number, String], default: 0 }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['edit', 'deleted', 'changed'])
|
||||
|
||||
const loading = ref(false)
|
||||
const detail = ref(null)
|
||||
const commentText = ref('')
|
||||
const replyTo = ref(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
watch(
|
||||
() => props.reportId,
|
||||
(id) => {
|
||||
if (id) loadDetail()
|
||||
else detail.value = null
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
async function loadDetail() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getDailyReportDetail(props.reportId)
|
||||
detail.value = res?.data || null
|
||||
if (!detail.value) ElMessage.error(res?.msg || '日报加载失败')
|
||||
commentText.value = ''
|
||||
replyTo.value = null
|
||||
} catch (err) {
|
||||
detail.value = null
|
||||
ElMessage.error(err?.message || '日报加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function startReply(comment) {
|
||||
replyTo.value = { id: comment.id, user_name: comment.user_name }
|
||||
}
|
||||
|
||||
async function submitComment() {
|
||||
const content = commentText.value.trim()
|
||||
if (!content) {
|
||||
ElMessage.warning('请输入评论内容')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await addDailyReportComment({
|
||||
report_id: Number(props.reportId),
|
||||
content,
|
||||
reply_to_id: replyTo.value?.id || 0
|
||||
})
|
||||
ElMessage.success('评论成功')
|
||||
commentText.value = ''
|
||||
replyTo.value = null
|
||||
await loadDetail()
|
||||
emit('changed')
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '评论失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteComment(comment) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定删除这条评论吗?', '删除评论', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await deleteDailyReportComment(comment.id)
|
||||
ElMessage.success('评论已删除')
|
||||
await loadDetail()
|
||||
emit('changed')
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteReport() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'删除后不可恢复,且该日期的评论会一并删除。确定删除这条日报吗?',
|
||||
'删除日报',
|
||||
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await deleteDailyReport(props.reportId)
|
||||
ElMessage.success('日报已删除')
|
||||
emit('deleted', Number(props.reportId))
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
function formatSize(size) {
|
||||
const n = Number(size) || 0
|
||||
if (n < 1024) return `${n} B`
|
||||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
|
||||
return `${(n / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.report-view {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.detail-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.author-avatar {
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.head-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.author-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.author-sub {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.head-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px dashed #ebeef5;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
|
||||
.section-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: #303133;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
color: #409eff;
|
||||
text-decoration: none;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.comment-count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.comment-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
background: #f7f8fa;
|
||||
|
||||
&.is-reply {
|
||||
margin-left: 24px;
|
||||
background: #f2f6fc;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.comment-user {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.reply-to {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.comment-time {
|
||||
margin-left: auto;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: #303133;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.comment-editor {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.reply-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.comment-disabled {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
html.dark & {
|
||||
.detail-head,
|
||||
.detail-section {
|
||||
border-color: var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.author-name,
|
||||
.section-text,
|
||||
.comment-user,
|
||||
.comment-content {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.author-sub,
|
||||
.detail-section h4,
|
||||
.comment-time,
|
||||
.file-size,
|
||||
.comment-disabled,
|
||||
.reply-banner {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.attachment-item,
|
||||
.comment-item {
|
||||
background: var(--el-fill-color-light);
|
||||
border-color: var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,918 @@
|
||||
<template>
|
||||
<div class="dailyreport-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>工作日报</h2>
|
||||
<p>按日记录今日完成、明日计划与问题建议,主管可查看团队汇总并进行点评</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button :icon="Refresh" @click="refreshAll">刷新</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openWriteToday">
|
||||
写今日日报
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab" class="report-tabs" @tab-change="handleTabChange">
|
||||
<!-- ==================== 我的日报 ==================== -->
|
||||
<el-tab-pane label="我的日报" name="mine">
|
||||
<div class="stats-row">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">本月日报</div>
|
||||
<div class="stat-value is-blue">{{ summary.month_total || 0 }}</div>
|
||||
<div class="stat-sub">条记录</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">本月已提交</div>
|
||||
<div class="stat-value is-green">{{ summary.month_submitted || 0 }}</div>
|
||||
<div class="stat-sub">条已提交</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">今日状态</div>
|
||||
<div class="stat-value" :class="todayStatusClass">
|
||||
{{ todayStatusText }}
|
||||
</div>
|
||||
<div class="stat-sub">{{ summary.window_end || todayStr() }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">补写窗口</div>
|
||||
<div class="stat-value is-orange window-value">
|
||||
{{ summary.window_start || '—' }}
|
||||
</div>
|
||||
<div class="stat-sub">至 {{ summary.window_end || '—' }},共 {{ profile.backfill_days || 7 }} 天</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<el-date-picker
|
||||
v-model="mineRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:disabled-date="disableFutureDate"
|
||||
unlink-panels
|
||||
style="width: 260px"
|
||||
/>
|
||||
<el-button type="primary" :icon="Search" @click="searchMine">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button :icon="Refresh" @click="resetMineFilters">重置</el-button>
|
||||
|
||||
<div class="backfill-group">
|
||||
<span class="backfill-label">补写往日</span>
|
||||
<el-date-picker
|
||||
v-model="backfillDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
:disabled-date="disableOutsideWindow"
|
||||
:clearable="false"
|
||||
style="width: 150px"
|
||||
/>
|
||||
<el-button @click="openBackfill">填写该日</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="split-layout">
|
||||
<aside class="list-pane">
|
||||
<div class="list-head">
|
||||
<span>我的日报</span>
|
||||
<span class="list-count">共 {{ pagination.total }} 条</span>
|
||||
</div>
|
||||
<div v-loading="loading" class="report-cards">
|
||||
<div
|
||||
v-for="row in list"
|
||||
:key="row.id"
|
||||
class="report-card"
|
||||
:class="{ active: mineSelectedId === row.id }"
|
||||
@click="mineSelectedId = row.id"
|
||||
>
|
||||
<div class="card-top">
|
||||
<span class="card-date">
|
||||
{{ row.report_date }}
|
||||
<el-tag v-if="row.report_date === todayStr()" size="small" effect="plain">
|
||||
今天
|
||||
</el-tag>
|
||||
</span>
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '已提交' : '草稿' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="card-summary">
|
||||
{{ row.today_done || row.tomorrow_plan || row.issues || '(内容为空)' }}
|
||||
</div>
|
||||
<div class="card-foot">
|
||||
<span>{{ row.comment_count || 0 }} 条评论</span>
|
||||
<span>{{ row.submit_time || '未提交' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty
|
||||
v-if="!loading && !list.length"
|
||||
description="还没有日报,点击右上角「写今日日报」开始记录"
|
||||
:image-size="70"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="pagination.total > pagination.pageSize" class="list-pagination">
|
||||
<el-pagination
|
||||
small
|
||||
layout="prev, pager, next"
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@current-change="handleMinePageChange"
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="detail-pane">
|
||||
<ReportView
|
||||
v-if="mineSelectedId"
|
||||
:key="`mine-${mineSelectedId}-${detailVersion}`"
|
||||
:report-id="mineSelectedId"
|
||||
@edit="handleDetailEdit"
|
||||
@deleted="handleMineDeleted"
|
||||
@changed="loadMine"
|
||||
/>
|
||||
<el-empty
|
||||
v-else
|
||||
description="选择左侧日报查看详情"
|
||||
:image-size="80"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ==================== 团队日报 ==================== -->
|
||||
<el-tab-pane v-if="profile.can_view_team" label="团队日报" name="team">
|
||||
<div class="filter-bar">
|
||||
<el-date-picker
|
||||
v-model="teamFilters.date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
:disabled-date="disableFutureDate"
|
||||
:clearable="false"
|
||||
style="width: 150px"
|
||||
/>
|
||||
<el-tree-select
|
||||
v-model="teamFilters.org_id"
|
||||
:data="orgTree"
|
||||
:props="{ value: 'id', label: 'name', children: 'children' }"
|
||||
node-key="id"
|
||||
check-strictly
|
||||
clearable
|
||||
placeholder="全部部门"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<el-input
|
||||
v-model="teamFilters.keyword"
|
||||
clearable
|
||||
placeholder="搜索成员姓名/账号"
|
||||
style="width: 200px"
|
||||
@keyup.enter="searchTeam"
|
||||
/>
|
||||
<el-button type="primary" :icon="Search" @click="searchTeam">查询</el-button>
|
||||
<el-button :icon="Refresh" @click="resetTeamFilters">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="stats-row team-stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">应提交</div>
|
||||
<div class="stat-value is-blue">{{ teamStats.total || 0 }}</div>
|
||||
<div class="stat-sub">名团队成员</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">已提交</div>
|
||||
<div class="stat-value is-green">{{ teamStats.submitted || 0 }}</div>
|
||||
<div class="stat-sub">人已提交</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">未提交</div>
|
||||
<div class="stat-value is-red">{{ teamStats.missing || 0 }}</div>
|
||||
<div class="stat-sub">人待提交</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">提交率</div>
|
||||
<div class="stat-value is-orange">{{ teamStats.rate || 0 }}%</div>
|
||||
<div class="stat-sub">{{ teamRes.date || teamFilters.date }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="split-layout">
|
||||
<aside class="list-pane">
|
||||
<div class="list-head">
|
||||
<span>团队成员</span>
|
||||
<span class="list-count">共 {{ teamPagination.total }} 人</span>
|
||||
</div>
|
||||
<div v-loading="teamLoading" class="report-cards">
|
||||
<div
|
||||
v-for="row in teamList"
|
||||
:key="row.user_id"
|
||||
class="report-card member-card"
|
||||
:class="{ active: teamSelectedId === row.report_id }"
|
||||
@click="selectTeam(row)"
|
||||
>
|
||||
<div class="card-top">
|
||||
<span class="member-name">{{ row.user_name }}</span>
|
||||
<el-tag v-if="row.submitted" type="success" size="small">已提交</el-tag>
|
||||
<el-tag v-else-if="row.status === 0" type="info" size="small">草稿</el-tag>
|
||||
<el-tag v-else type="danger" size="small" effect="plain">未提交</el-tag>
|
||||
</div>
|
||||
<div class="card-summary member-org">
|
||||
{{ row.org_name || '未分配部门' }}
|
||||
<span v-if="row.account" class="member-account">{{ row.account }}</span>
|
||||
</div>
|
||||
<div class="card-foot">
|
||||
<span>{{ row.submit_time || '—' }}</span>
|
||||
<el-tag v-if="row.has_issue" type="warning" size="small" effect="plain">
|
||||
有反馈
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty
|
||||
v-if="!teamLoading && !teamList.length"
|
||||
description="暂无团队成员"
|
||||
:image-size="70"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="teamPagination.total > teamPagination.pageSize" class="list-pagination">
|
||||
<el-pagination
|
||||
small
|
||||
layout="prev, pager, next"
|
||||
:current-page="teamPagination.page"
|
||||
:page-size="teamPagination.pageSize"
|
||||
:total="teamPagination.total"
|
||||
@current-change="handleTeamPageChange"
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="detail-pane">
|
||||
<ReportView
|
||||
v-if="teamSelectedId"
|
||||
:key="`team-${teamSelectedId}-${detailVersion}`"
|
||||
:report-id="teamSelectedId"
|
||||
@edit="handleDetailEdit"
|
||||
@deleted="handleTeamDeleted"
|
||||
@changed="loadTeam"
|
||||
/>
|
||||
<el-empty
|
||||
v-else
|
||||
description="选择左侧已提交的成员查看日报"
|
||||
:image-size="80"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<ReportEdit
|
||||
v-model="editVisible"
|
||||
:report="editingReport"
|
||||
:default-date="editDefaultDate"
|
||||
:window-start="profile.window_start || summary.window_start"
|
||||
:window-end="profile.window_end || summary.window_end"
|
||||
@saved="handleSaved"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDailyReportProfile,
|
||||
getMyDailyReports,
|
||||
getDailyReportDetail,
|
||||
getTeamDailyReports,
|
||||
getTeamDailyReportOrgs
|
||||
} from '@/api/oaDailyReport'
|
||||
import ReportEdit from './components/ReportEdit.vue'
|
||||
import ReportView from './components/ReportView.vue'
|
||||
|
||||
const activeTab = ref('mine')
|
||||
const profile = ref({
|
||||
is_admin: false,
|
||||
can_view_team: false,
|
||||
window_start: '',
|
||||
window_end: '',
|
||||
backfill_days: 7
|
||||
})
|
||||
|
||||
// ---------------- 我的日报 ----------------
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const summary = ref({})
|
||||
const mineRange = ref([])
|
||||
const backfillDate = ref(todayStr())
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const mineSelectedId = ref(0)
|
||||
|
||||
// ---------------- 团队日报 ----------------
|
||||
const teamLoading = ref(false)
|
||||
const teamList = ref([])
|
||||
const teamStats = ref({ total: 0, submitted: 0, missing: 0, rate: 0 })
|
||||
const teamRes = ref({})
|
||||
const orgOptions = ref([])
|
||||
const teamFilters = reactive({ date: todayStr(), org_id: null, keyword: '' })
|
||||
const teamPagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const teamSelectedId = ref(0)
|
||||
|
||||
// ---------------- 编辑弹窗 ----------------
|
||||
const editVisible = ref(false)
|
||||
const editingReport = ref(null)
|
||||
const editDefaultDate = ref(todayStr())
|
||||
// 详情刷新标记:编辑保存后递增,触发右侧详情重新加载
|
||||
const detailVersion = ref(0)
|
||||
|
||||
const orgTree = computed(() => buildOrgTree(orgOptions.value))
|
||||
|
||||
const todayStatusText = computed(() => {
|
||||
const status = summary.value.today_status
|
||||
if (status === 1) return '已提交'
|
||||
if (status === 0) return '草稿'
|
||||
return '未填写'
|
||||
})
|
||||
|
||||
const todayStatusClass = computed(() => {
|
||||
const status = summary.value.today_status
|
||||
if (status === 1) return 'is-green'
|
||||
if (status === 0) return 'is-orange'
|
||||
return 'is-red'
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await loadProfile()
|
||||
await loadMine()
|
||||
})
|
||||
|
||||
async function loadProfile() {
|
||||
try {
|
||||
const res = await getDailyReportProfile()
|
||||
profile.value = { ...profile.value, ...(res?.data || {}) }
|
||||
if (profile.value.window_end && profile.value.window_start) {
|
||||
backfillDate.value = profile.value.window_end
|
||||
}
|
||||
if (!profile.value.can_view_team && activeTab.value === 'team') {
|
||||
activeTab.value = 'mine'
|
||||
}
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '日报身份加载失败,请确认已执行建表脚本')
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMine() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getMyDailyReports({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
start_date: mineRange.value?.[0] || '',
|
||||
end_date: mineRange.value?.[1] || ''
|
||||
})
|
||||
const data = res?.data || {}
|
||||
list.value = data.list || []
|
||||
pagination.total = data.total || 0
|
||||
summary.value = data.summary || {}
|
||||
// 选中项仍在当前列表则保持,否则默认选中第一条
|
||||
const ids = list.value.map((x) => x.id)
|
||||
if (!ids.includes(mineSelectedId.value)) {
|
||||
mineSelectedId.value = ids[0] || 0
|
||||
}
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '日报列表加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleMinePageChange(page) {
|
||||
pagination.page = page
|
||||
loadMine()
|
||||
}
|
||||
|
||||
function searchMine() {
|
||||
pagination.page = 1
|
||||
loadMine()
|
||||
}
|
||||
|
||||
function resetMineFilters() {
|
||||
mineRange.value = []
|
||||
pagination.page = 1
|
||||
loadMine()
|
||||
}
|
||||
|
||||
async function openWriteToday() {
|
||||
if (summary.value.today_report_id) {
|
||||
await openEditById(summary.value.today_report_id)
|
||||
return
|
||||
}
|
||||
openEdit(null, todayStr())
|
||||
}
|
||||
|
||||
async function openBackfill() {
|
||||
const date = backfillDate.value
|
||||
if (!date) {
|
||||
ElMessage.warning('请选择要补写的日期')
|
||||
return
|
||||
}
|
||||
if (!inWindow(date)) {
|
||||
ElMessage.warning(`只能补写最近 ${profile.value.backfill_days || 7} 天的日报`)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await getMyDailyReports({ page: 1, pageSize: 1, start_date: date, end_date: date })
|
||||
const existed = res?.data?.list?.[0]
|
||||
if (existed) {
|
||||
editingReport.value = existed
|
||||
editDefaultDate.value = date
|
||||
editVisible.value = true
|
||||
return
|
||||
}
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '查询失败')
|
||||
return
|
||||
}
|
||||
openEdit(null, date)
|
||||
}
|
||||
|
||||
async function openEditById(id) {
|
||||
try {
|
||||
const res = await getDailyReportDetail(id)
|
||||
const report = res?.data?.report
|
||||
if (report) openEdit(report)
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '日报加载失败')
|
||||
}
|
||||
}
|
||||
|
||||
function openEdit(report, date = '') {
|
||||
editingReport.value = report || null
|
||||
editDefaultDate.value = date || report?.report_date || todayStr()
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
function handleDetailEdit(report) {
|
||||
openEdit(report)
|
||||
}
|
||||
|
||||
function handleMineDeleted() {
|
||||
mineSelectedId.value = 0
|
||||
loadMine()
|
||||
if (profile.value.can_view_team && activeTab.value === 'team') loadTeam()
|
||||
}
|
||||
|
||||
function handleTeamDeleted() {
|
||||
teamSelectedId.value = 0
|
||||
loadTeam()
|
||||
}
|
||||
|
||||
function handleSaved() {
|
||||
detailVersion.value++
|
||||
loadMine()
|
||||
if (profile.value.can_view_team) loadTeam()
|
||||
}
|
||||
|
||||
// ---------------- 团队日报 ----------------
|
||||
function handleTabChange(name) {
|
||||
if (name !== 'team') return
|
||||
if (!profile.value.can_view_team) return
|
||||
if (!orgOptions.value.length) loadTeamOrgs()
|
||||
loadTeam()
|
||||
}
|
||||
|
||||
async function loadTeamOrgs() {
|
||||
try {
|
||||
const res = await getTeamDailyReportOrgs()
|
||||
orgOptions.value = res?.data?.list || []
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '部门加载失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTeam() {
|
||||
if (!profile.value.can_view_team) return
|
||||
teamLoading.value = true
|
||||
try {
|
||||
const res = await getTeamDailyReports({
|
||||
date: teamFilters.date,
|
||||
org_id: teamFilters.org_id || 0,
|
||||
keyword: teamFilters.keyword.trim(),
|
||||
page: teamPagination.page,
|
||||
pageSize: teamPagination.pageSize
|
||||
})
|
||||
const data = res?.data || {}
|
||||
teamRes.value = data
|
||||
teamList.value = data.list || []
|
||||
teamPagination.total = data.total || 0
|
||||
teamStats.value = data.stats || { total: 0, submitted: 0, missing: 0, rate: 0 }
|
||||
// 选中项仍已提交则保持,否则默认选中第一个已提交成员
|
||||
const submitted = teamList.value.filter((x) => x.submitted && x.report_id)
|
||||
if (!submitted.some((x) => x.report_id === teamSelectedId.value)) {
|
||||
teamSelectedId.value = submitted[0]?.report_id || 0
|
||||
}
|
||||
} catch (err) {
|
||||
ElMessage.error(err?.message || '团队日报加载失败')
|
||||
} finally {
|
||||
teamLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleTeamPageChange(page) {
|
||||
teamPagination.page = page
|
||||
loadTeam()
|
||||
}
|
||||
|
||||
function searchTeam() {
|
||||
teamPagination.page = 1
|
||||
loadTeam()
|
||||
}
|
||||
|
||||
function resetTeamFilters() {
|
||||
teamFilters.date = todayStr()
|
||||
teamFilters.org_id = null
|
||||
teamFilters.keyword = ''
|
||||
teamPagination.page = 1
|
||||
loadTeam()
|
||||
}
|
||||
|
||||
function selectTeam(row) {
|
||||
if (!row.submitted || !row.report_id) {
|
||||
ElMessage.info('该成员当日未提交日报')
|
||||
return
|
||||
}
|
||||
teamSelectedId.value = row.report_id
|
||||
}
|
||||
|
||||
function refreshAll() {
|
||||
loadProfile()
|
||||
if (activeTab.value === 'mine') {
|
||||
loadMine()
|
||||
} else {
|
||||
loadTeam()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- 工具 ----------------
|
||||
function todayStr() {
|
||||
const now = new Date()
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`
|
||||
}
|
||||
|
||||
function inWindow(date) {
|
||||
const start = profile.value.window_start || summary.value.window_start
|
||||
const end = profile.value.window_end || summary.value.window_end
|
||||
if (!start || !end) return date === todayStr()
|
||||
return date >= start && date <= end
|
||||
}
|
||||
|
||||
function disableFutureDate(date) {
|
||||
const today = todayStr()
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
const value = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||||
return value > today
|
||||
}
|
||||
|
||||
function disableOutsideWindow(date) {
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
const value = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||||
const start = profile.value.window_start || summary.value.window_start
|
||||
const end = profile.value.window_end || summary.value.window_end
|
||||
if (!start || !end) return value !== todayStr()
|
||||
return value < start || value > end
|
||||
}
|
||||
|
||||
function buildOrgTree(rows) {
|
||||
const map = new Map()
|
||||
rows.forEach((row) => map.set(row.id, { ...row, children: [] }))
|
||||
const roots = []
|
||||
map.forEach((node) => {
|
||||
const parent = map.get(node.parent_id)
|
||||
if (parent) parent.children.push(node)
|
||||
else roots.push(node)
|
||||
})
|
||||
return roots
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.dailyreport-page {
|
||||
padding: 16px 20px 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.report-tabs {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.team-stats {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
margin-top: 6px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-value.is-green {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.stat-value.is-blue {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.stat-value.is-orange {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.stat-value.is-red {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.window-value {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.stat-sub {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.backfill-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.backfill-label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
// ---------------- 左右结构 ----------------
|
||||
.split-layout {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.list-pane {
|
||||
flex: 0 0 340px;
|
||||
width: 340px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.list-count {
|
||||
font-weight: 400;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.report-cards {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
max-height: 548px;
|
||||
min-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #c6e2ff;
|
||||
background: #f5faff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: #409eff;
|
||||
background: #ecf5ff;
|
||||
}
|
||||
}
|
||||
|
||||
.card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card-date {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.card-summary {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #606266;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.member-org {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.member-account {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.card-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.list-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 8px 10px 10px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.detail-pane {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 16px 20px;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
min-height: 480px;
|
||||
max-height: 660px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.split-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.list-pane {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.report-cards {
|
||||
max-height: 320px;
|
||||
}
|
||||
|
||||
.detail-pane {
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
html.dark & {
|
||||
.page-header p,
|
||||
.stat-label,
|
||||
.stat-sub,
|
||||
.member-account,
|
||||
.list-count,
|
||||
.card-foot {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.stat-card,
|
||||
.list-pane,
|
||||
.detail-pane,
|
||||
.report-card {
|
||||
background: var(--el-bg-color);
|
||||
border-color: var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.list-head {
|
||||
border-color: var(--el-border-color-lighter);
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.report-card {
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
|
||||
.card-date,
|
||||
.member-name {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.card-summary {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.list-pagination {
|
||||
border-color: var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.backfill-label {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -40,19 +40,12 @@
|
||||
|
||||
<!-- 申请列表 -->
|
||||
<el-table :data="applies" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||||
<el-table-column label="申请人" min-width="140" align="center">
|
||||
<template #default="scope">
|
||||
<div>{{ scope.row.identity_nickname || "-" }}</div>
|
||||
<div class="sub-text">{{ scope.row.identity_mobile || "" }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="tenant_name"
|
||||
label="申请加入企业"
|
||||
min-width="160"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column label="联系方式" min-width="150" align="center">
|
||||
<template #default="scope">
|
||||
<div>{{ scope.row.contact_person || "-" }}</div>
|
||||
@@ -153,7 +146,6 @@ import { formatDateTime } from "@/utils/datetime";
|
||||
interface TenantApply {
|
||||
id: number;
|
||||
identity_id: number;
|
||||
tenant_name: string;
|
||||
identity_nickname: string;
|
||||
identity_mobile: string;
|
||||
contact_person: string;
|
||||
|
||||
Reference in new issue
Block a user