This commit is contained in:
磷叶 2025-01-24 18:06:55 +08:00
parent 274ab38926
commit 9b5464ffb6
20 changed files with 1509 additions and 83 deletions

9
src/api/bst/dashboard.js Normal file
View File

@ -0,0 +1,9 @@
import request from '@/utils/request'
// 获取概览
export function getBrief() {
return request({
url: '/bst/dashboard/brief',
method: 'get'
})
}

View File

@ -50,3 +50,37 @@ export function delProject(id) {
method: 'delete'
})
}
// 完成项目
export function completeProject(id) {
return request({
url: '/bst/project/complete/' + id,
method: 'put'
})
}
// 开始开发
export function startProject(data) {
return request({
url: '/bst/project/start',
method: 'put',
data
})
}
// 验收
export function acceptProject(id) {
return request({
url: '/bst/project/accept/' + id,
method: 'put'
})
}
// 维护
export function maintenanceProject(data) {
return request({
url: '/bst/project/maintenance',
method: 'put',
data
})
}

View File

@ -0,0 +1,161 @@
<template>
<div class="attach-content">
<el-row :gutter="10">
<el-col :span="6" v-for="(item, index) in fileList" :key="index">
<el-card :body-style="{ padding: '0px' }" shadow="hover" class="attach-item">
<!-- 图片预览 -->
<div v-if="isImage(item)" class="attach-image-box">
<el-image
:src="getRealUrl(item)"
fit="cover"
:preview-src-list="imageList"
class="attach-image"
>
<div slot="error" class="image-slot">
<i class="el-icon-picture-outline"></i>
</div>
</el-image>
</div>
<!-- 非图片文件 -->
<div v-else class="file-item">
<i class="el-icon-document"></i>
</div>
<div class="attach-info">
<div class="attach-name" :title="getFileName(item)">{{ getFileName(item) }}</div>
<div class="attach-action">
<el-link type="primary" :href="getRealUrl(item)" target="_blank" :download="getFileName(item)">
<i class="el-icon-download"></i>
</el-link>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import { getRealUrl } from '@/utils';
export default {
name: "AttachList",
props: {
//
fileList: {
type: Array,
default: () => []
},
//
column: {
type: Number,
default: 4
}
},
data() {
return {
//
imageTypes: ['.png', '.jpg', '.jpeg', '.gif', '.bmp']
}
},
computed: {
//
imageList() {
return this.fileList.filter(item => this.isImage(item)).map(item => this.getRealUrl(item));
},
//
colSpan() {
return 24 / this.column;
}
},
methods: {
// url
getRealUrl(url) {
return getRealUrl(url);
},
//
isImage(url) {
if (!url) return false;
return this.imageTypes.some(type => url.toLowerCase().endsWith(type));
},
//
getFileName(url) {
if (!url) {
return '';
}
return url.substring(url.lastIndexOf('/') + 1);
}
}
}
</script>
<style lang="scss" scoped>
.attach-content {
.attach-item {
margin-bottom: 10px;
overflow: hidden;
.attach-image-box {
width: 100%;
height: 120px;
overflow: hidden;
.el-image {
width: 100%;
height: 100%;
display: block;
transition: all 0.25s ease-in-out;
}
.el-image:hover {
cursor: pointer;
transform: scale(1.15);
}
}
.file-item {
width: 100%;
height: 120px;
background: #f5f7fa;
display: flex;
align-items: center;
justify-content: center;
i {
font-size: 40px;
color: #909399;
}
}
.attach-info {
padding: 8px;
background: #fff;
border-top: 1px solid #ebeef5;
.attach-name {
font-size: 13px;
color: #606266;
margin-bottom: 5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.attach-action {
text-align: right;
}
}
}
}
//
.image-slot {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: #f5f7fa;
i {
font-size: 30px;
color: #909399;
}
}
</style>

View File

@ -28,9 +28,13 @@
<!-- 文件列表 -->
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<li :key="file.url" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
<el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank">
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link>
<div class="file-link">
<image-preview v-if="isImage(file.url)" :src="file.url" :width="32" :height="32" style="margin-right: 10px"/>
<i class="el-icon-document" v-else/>
<el-link :href="realUrl(file.url)" :underline="false" target="_blank">
{{ getFileName(file.name) }}
</el-link>
</div>
<div class="ele-upload-list__item-content-action">
<el-link :underline="false" @click="handleDelete(index)" type="danger">删除</el-link>
</div>
@ -109,8 +113,21 @@ export default {
showTip() {
return this.isShowTip && (this.fileType || this.fileSize);
},
// url
realUrl() {
return (url) => {
if (url.startsWith('http')) {
return url;
}
return this.baseUrl + url;
}
}
},
methods: {
//
isImage(url) {
return url.endsWith('.png') || url.endsWith('.jpg') || url.endsWith('.jpeg');
},
//
handleBeforeUpload(file) {
//
@ -213,4 +230,9 @@ export default {
.ele-upload-list__item-content-action .el-link {
margin-right: 10px;
}
.file-link {
display: flex;
align-items: center;
flex-direction: row;
}
</style>

View File

@ -117,7 +117,7 @@ export default {
this.fileList = list.map(item => {
if (typeof item === "string") {
if (item.indexOf(this.baseUrl) === -1) {
item = { name: this.baseUrl + item, url: this.baseUrl + item };
item = { name: this.getRealUrl(item), url: this.getRealUrl(item) };
} else {
item = { name: item, url: item };
}
@ -146,6 +146,12 @@ export default {
}
},
methods: {
getRealUrl(url) {
if (url.startsWith('http')) {
return url;
}
return this.baseUrl + url;
},
//
handleMouseEnter() {
if (!this.isListening) {
@ -284,8 +290,13 @@ export default {
},
//
handlePictureCardPreview(file) {
this.dialogImageUrl = file.url;
this.dialogVisible = true;
if (file.url.match(/(docx?|xlsx?|pptx?|pdf)$/i)) {
// TODO office
window.open(`https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(this.getRealUrl(file.url))}`, '_blank');
} else{
this.dialogImageUrl = file.url;
this.dialogVisible = true;
}
},
//
listToString(list, separator) {

View File

@ -97,6 +97,12 @@ export const constantRoutes = [
component: Layout,
hidden: true,
children: [
{
path: 'project/:id?',
component: () => import('@/views/bst/project/view/index.vue'),
name: 'ProjectView',
meta: { title: '查看项目', noCache: false }
}
]
},
{

View File

@ -1,5 +1,5 @@
// 视图
import { getLastDate, getLastDateTimeEnd, getLastDateTimeStart, getLastMonth, getLastMonthTimeEnd } from '@/utils/index'
import { getLastDate, getLastDateTimeEnd, getLastDateTimeStart, getLastMonth, getLastMonthTimeEnd } from '@/utils/index';
export const views = {
}
@ -68,6 +68,12 @@ export const DatePickerOptions = {
disabledDate(date) {
return date.getTime() > Date.now();
}
},
// 禁用过去
DISABLE_PAST: {
disabledDate(date) {
return date.getTime() < Date.now();
}
}
}

View File

@ -34,4 +34,32 @@ export const CustomerIntentLevel = {
HIGH: "1", // 高
MEDIUM: "2", // 中
LOW: "3" // 低
}
}
// 项目状态
export const ProjectStatus = {
WAIT_START: "WAIT_START", // 待开始
IN_PROGRESS: "IN_PROGRESS", // 进行中
COMPLETED: "COMPLETED", // 完成开发
ACCEPTED: "ACCEPTED", // 已验收
MAINTENANCE: "MAINTENANCE", // 维护中
MAINTENANCE_OVERDUE: "MAINTENANCE_OVERDUE", // 维护到期
DEVELOPMENT_OVERDUE: "DEVELOPMENT_OVERDUE", // 开发超期
// 是否可以完成
canComplete() {
return [this.IN_PROGRESS, this.DEVELOPMENT_OVERDUE]
},
// 是否可以开始开发
canStart() {
return [this.WAIT_START]
},
// 是否可以验收
canAccept() {
return [this.COMPLETED]
},
// 是否可以维护
canMaintenance() {
return [this.ACCEPTED, this.MAINTENANCE_OVERDUE]
}
}

View File

@ -1,5 +1,5 @@
import { parseTime } from './ruoyi'
import Decimal from 'decimal.js'
import Decimal from 'decimal.js';
import { parseTime } from './ruoyi';
/**
* 表格时间格式化
@ -523,3 +523,11 @@ export function formatFraction(numerator, denominator) {
}
return `${numerator}/${denominator}`;
}
// 获取真实url
export function getRealUrl(url) {
if (url.startsWith('http')) {
return url;
}
return `${process.env.VUE_APP_BASE_API}${url}`;
}

View File

@ -37,16 +37,30 @@
</el-select>
</form-col>
<form-col :span="span" label="手机号" prop="mobile">
<el-input v-model="form.mobile" placeholder="请输入手机号" />
<el-input v-model="form.mobile" placeholder="请输入手机号" maxlength="11" show-word-limit />
</form-col>
<form-col :span="span" label="微信号" prop="wechat">
<el-input v-model="form.wechat" placeholder="请输入微信号" />
<el-input v-model="form.wechat" placeholder="请输入微信号" maxlength="50" show-word-limit />
</form-col>
<form-col :span="span" label="来源" prop="source">
<el-input v-model="form.source" placeholder="请输入来源" />
<el-select v-model="form.source" placeholder="请选择来源" style="width: 100%;" allow-create filterable default-first-option>
<el-option
v-for="dict in dict.type.customer_source"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</form-col>
<form-col :span="span" label="意向" prop="intent">
<el-input v-model="form.intent" placeholder="请输入意向" />
<form-col :span="span" label="意向" prop="intents">
<el-select v-model="form.intents" placeholder="请选择意向" style="width: 100%;" allow-create filterable default-first-option multiple>
<el-option
v-for="dict in dict.type.customer_intent"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</form-col>
<form-col :span="span" label="跟进人" prop="followId">
<user-select v-model="form.followId" />
@ -73,7 +87,7 @@ import UserSelect from '@/components/Business/User/UserSelect.vue';
export default {
name: "CustomerEditDialog",
components: { FormCol, UserSelect },
dicts: ['customer_intent_level', 'customer_status'],
dicts: ['customer_intent_level', 'customer_status', 'customer_source', 'customer_intent'],
props: {
show: {
type: Boolean,
@ -94,10 +108,10 @@ export default {
//
rules: {
code: [
{ required: true, message: "客户编号不能为空", trigger: "blur" }
{ required: true, message: "客户编号不能为空", trigger: "change" }
],
name: [
{ required: true, message: "客户姓名不能为空", trigger: "blur" }
{ required: true, message: "客户姓名不能为空", trigger: "change" }
],
status: [
{ required: true, message: "状态不能为空", trigger: "change" }
@ -106,10 +120,10 @@ export default {
{ required: true, message: "意向强度不能为空", trigger: "change" }
],
source: [
{ required: true, message: "来源不能为空", trigger: "blur" }
{ required: true, message: "来源不能为空", trigger: "change" }
],
followId: [
{ required: true, message: "跟进人不能为空", trigger: "blur" }
{ required: true, message: "跟进人不能为空", trigger: "change" }
]
}
};
@ -149,10 +163,8 @@ export default {
mobile: null,
wechat: null,
source: null,
intent: null,
intents: [],
followId: this.userId,
lastFollowTime: null,
nextFollowTime: null,
remark: null
};
this.resetForm("form");

View File

@ -142,6 +142,9 @@
<template v-else-if="column.key === 'intentLevel'">
<dict-tag :options="dict.type.customer_intent_level" :value="d.row[column.key]"/>
</template>
<template v-else-if="column.key === 'intents'">
{{d.row[column.key].join(',')}}
</template>
<template v-else>
{{d.row[column.key]}}
</template>
@ -186,7 +189,7 @@
</template>
<script>
import { listCustomer, getCustomer, delCustomer, addCustomer, updateCustomer } from "@/api/bst/customer";
import { listCustomer, delCustomer} from "@/api/bst/customer";
import { $showColumns } from '@/utils/mixins';
import FormCol from "@/components/FormCol/index.vue";
import CustomerEditDialog from './components/CustomerEditDialog.vue';
@ -215,7 +218,7 @@ export default {
{key: 'mobile', visible: true, label: '手机号', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'wechat', visible: true, label: '微信号', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'source', visible: true, label: '来源', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'intent', visible: true, label: '意向', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'intents', visible: true, label: '意向', minWidth: null, sortable: true, overflow: true, align: 'center', width: null},
{key: 'followName', visible: true, label: '跟进人', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'lastFollowTime', visible: true, label: '最近跟进', minWidth: null, sortable: false, overflow: false, align: 'center', width: "100"},
{key: 'nextFollowTime', visible: true, label: '下次跟进', minWidth: null, sortable: false, overflow: false, align: 'center', width: "100"},

View File

@ -0,0 +1,124 @@
<template>
<el-dialog
title="项目维护"
:visible.sync="dialogVisible"
width="500px"
append-to-body
@open="handleOpen"
@close="handleClose"
>
<el-form ref="form" :model="form" :rules="rules" label-width="100px" label-position="top" v-loading="loading">
<el-form-item label="维护到期" prop="maintenanceEndDate">
<el-date-picker
v-model="form.maintenanceEndDate"
type="date"
placeholder="请选择维护到期日期"
value-format="yyyy-MM-dd"
style="width: 100%"
:picker-options="DatePickerOptions.DISABLE_PAST"
/>
</el-form-item>
<el-form-item label="运维费用" prop="operationAmount">
<el-input v-model="form.operationAmount" placeholder="请输入运维费用" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="submitLoading" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</template>
<script>
import { maintenanceProject, getProject } from "@/api/bst/project";
import { DatePickerOptions } from '@/utils/constants';
export default {
name: "ProjectMaintenanceDialog",
props: {
//
visible: {
type: Boolean,
default: false
},
// ID
id: {
type: String,
default: null,
}
},
data() {
return {
DatePickerOptions,
//
form: {
},
//
rules: {
maintenanceEndDate: [
{ required: true, message: "请选择维护到期日期", trigger: "blur" }
]
},
//
loading: false,
submitLoading: false
};
},
computed: {
dialogVisible: {
get() {
return this.visible;
},
set(value) {
this.$emit("update:visible", value);
}
}
},
methods: {
handleOpen() {
this.getDetail();
},
/** 获取详情 */
getDetail() {
this.loading = true;
getProject(this.id).then(res => {
this.form = res.data;
}).finally(() => {
this.loading = false;
});
},
/** 提交按钮 */
submitForm() {
this.$refs.form.validate(valid => {
if (valid) {
this.submitLoading = true;
maintenanceProject(this.form).then(res => {
if (res.code === 200) {
this.$modal.msgSuccess("操作成功");
this.$emit("success");
this.cancel();
}
}).finally(() => {
this.submitLoading = false;
});
}
});
},
/** 取消按钮 */
cancel() {
this.dialogVisible = false;
},
/** 关闭弹窗 */
handleClose() {
this.cancel();
},
}
};
</script>
<style lang="scss" scoped>
.dialog-footer {
text-align: right;
padding-top: 20px;
}
</style>

View File

@ -0,0 +1,114 @@
<template>
<el-dialog
title="开始开发"
:visible.sync="dialogVisible"
width="500px"
append-to-body
@open="handleOpen"
@close="handleClose"
>
<el-form ref="form" :model="form" :rules="rules" label-width="100px" label-position="top">
<el-form-item label="预计完成" prop="expectedCompleteDate">
<el-date-picker
v-model="form.expectedCompleteDate"
type="date"
placeholder="请选择预计完成日期"
value-format="yyyy-MM-dd"
style="width: 100%"
:picker-options="DatePickerOptions.DISABLE_PAST"
/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="loading" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</template>
<script>
import { startProject } from "@/api/bst/project";
import { DatePickerOptions } from '@/utils/constants';
export default {
name: "ProjectStartDialog",
props: {
//
visible: {
type: Boolean,
default: false
},
// ID
id: {
type: String,
default: null
}
},
data() {
return {
DatePickerOptions,
//
form: {
},
//
rules: {
expectedCompleteDate: [
{ required: true, message: "请选择预计完成日期", trigger: "blur" }
]
},
//
loading: false
};
},
computed: {
dialogVisible: {
get() {
return this.visible;
},
set(value) {
this.$emit("update:visible", value);
}
}
},
methods: {
handleOpen() {
this.form = {
id: this.id,
expectedCompleteDate: null
}
},
/** 提交按钮 */
submitForm() {
this.$refs.form.validate(valid => {
if (valid) {
this.loading = true;
startProject(this.form).then(res => {
if (res.code === 200) {
this.$modal.msgSuccess("操作成功");
this.$emit("success");
this.cancel();
}
}).finally(() => {
this.loading = false;
});
}
});
},
/** 取消按钮 */
cancel() {
this.dialogVisible = false;
},
/** 关闭弹窗 */
handleClose() {
this.cancel();
},
}
};
</script>
<style lang="scss" scoped>
.dialog-footer {
text-align: right;
padding-top: 20px;
}
</style>

View File

@ -7,9 +7,6 @@
<div class="app-container">
<el-form ref="form" :model="form" :rules="rules" label-width="80px" v-loading="loading">
<el-row>
<form-col :span="span" label="项目编号" prop="no">
<el-input v-model="form.no" placeholder="请输入项目编号" />
</form-col>
<form-col :span="span" label="项目名称" prop="name">
<el-input v-model="form.name" placeholder="请输入项目名称" />
</form-col>
@ -22,8 +19,8 @@
style="width: calc(100% - 2em)"
/>
</form-col>
<form-col :span="span" label="客户ID" prop="customerId">
<el-input v-model="form.customerId" placeholder="请输入客户ID" />
<form-col :span="span" label="客户" prop="customerId">
<customer-input v-model="form.customerId" :text.sync="form.customerName"/>
</form-col>
<form-col :span="span" label="到期时间" prop="expireTime">
<el-date-picker clearable
@ -34,31 +31,17 @@
placeholder="请选择到期时间">
</el-date-picker>
</form-col>
<form-col :span="span" label="运维时间" prop="operationTime">
<el-date-picker clearable
style="width: 100%;"
v-model="form.operationTime"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择运维时间">
</el-date-picker>
<form-col :span="span" label="负责人" prop="ownerId">
<user-select v-model="form.ownerId" />
</form-col>
<form-col :span="span" label="运维费用" prop="operationAmount">
<el-input v-model="form.operationAmount" placeholder="请输入运维费用" />
<form-col :span="span" label="跟进人" prop="followId">
<user-select v-model="form.followId" />
</form-col>
<form-col :span="24" label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</form-col>
<form-col :span="24" label="附件" prop="attaches">
<file-upload v-model="form.attaches" />
</form-col>
</el-row>
<el-row>
<form-col :span="span" label="负责人" prop="ownerId">
<user-input v-model="form.ownerId" />
</form-col>
<form-col :span="span" label="跟进人" prop="followId">
<user-input v-model="form.followId" />
<image-upload v-model="form.attaches" />
</form-col>
</el-row>
</el-form>
@ -71,11 +54,11 @@
import { getProject, addProject, updateProject } from "@/api/bst/project";
import FormCol from "@/components/FormCol/index.vue";
import EditHeader from "@/components/EditHeader/index.vue";
import UserInput from "@/components/Business/User/UserInput.vue";
import FileUpload from "@/components/FileUpload/index.vue";
import CustomerInput from "@/components/Business/Customer/CustomerInput.vue";
import UserSelect from "@/components/Business/User/UserSelect.vue";
export default {
name: "ProjectEdit",
components: { FormCol, EditHeader, UserInput, FileUpload },
components: { FormCol, EditHeader, UserSelect, CustomerInput },
dicts: ['project_status'],
data() {
return {
@ -86,9 +69,6 @@ export default {
form: {},
//
rules: {
no: [
{ required: true, message: "项目编号不能为空", trigger: "blur" }
],
name: [
{ required: true, message: "项目名称不能为空", trigger: "blur" }
],

View File

@ -46,9 +46,8 @@
<el-checkbox-button
v-for="dict in dict.type.project_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
:label="dict.value"
>{{dict.label}}</el-checkbox-button>
</el-checkbox-group>
</el-form-item>
<el-form-item>
@ -124,6 +123,12 @@
</template>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleView(scope.row)"
>详情</el-button>
<el-button
size="mini"
type="text"
@ -138,6 +143,38 @@
@click="handleDelete(scope.row)"
v-has-permi="['bst:project:remove']"
>删除</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-check"
@click="handleStart(scope.row)"
v-has-permi="['bst:project:start']"
v-if="ProjectStatus.canStart().includes(scope.row.status)"
>开始开发</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-check"
@click="handleComplete(scope.row)"
v-has-permi="['bst:project:complete']"
v-if="ProjectStatus.canComplete().includes(scope.row.status)"
>开发完成</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-check"
@click="handleAccept(scope.row)"
v-has-permi="['bst:project:accept']"
v-if="ProjectStatus.canAccept().includes(scope.row.status)"
>项目验收</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-check"
@click="handleMaintenance(scope.row)"
v-has-permi="['bst:project:maintenance']"
v-if="ProjectStatus.canMaintenance().includes(scope.row.status)"
>运行维护</el-button>
</template>
</el-table-column>
</el-table>
@ -149,13 +186,30 @@
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
<!-- 开始开发弹窗 -->
<project-start-dialog
:visible.sync="showStartDialog"
:id="row.id"
@success="getList"
/>
<!-- 维护弹窗 -->
<project-maintenance-dialog
:visible.sync="showMaintenanceDialog"
:id="row.id"
@success="getList"
/>
</div>
</template>
<script>
import { listProject, delProject } from "@/api/bst/project";
import { listProject, delProject, completeProject, acceptProject } from "@/api/bst/project";
import { $showColumns } from '@/utils/mixins';
import { ProjectStatus } from '@/utils/enums';
import FormCol from "@/components/FormCol/index.vue";
import ProjectStartDialog from '@/views/bst/project/components/ProjectStartDialog.vue';
import ProjectMaintenanceDialog from '@/views/bst/project/components/ProjectMaintenanceDialog.vue';
//
const defaultSort = {
@ -167,25 +221,34 @@ export default {
name: "Project",
mixins: [$showColumns],
dicts: ['project_status'],
components: {FormCol},
components: {
FormCol,
ProjectStartDialog,
ProjectMaintenanceDialog
},
data() {
return {
ProjectStatus,
defaultSort,
showStartDialog: false, //
showMaintenanceDialog: false, //
row: {}, //
span: 24,
//
columns: [
{key: 'id', visible: false, label: 'ID', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'no', visible: true, label: '项目编号', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'no', visible: true, label: '项目编号', minWidth: null, sortable: true, overflow: true, align: 'center', width: null},
{key: 'name', visible: true, label: '项目名称', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'customerId', visible: true, label: '客户', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'status', visible: true, label: '状态', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'customerName', visible: true, label: '客户', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'amount', visible: true, label: '项目金额', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'expireTime', visible: true, label: '到期时间', minWidth: null, sortable: false, overflow: false, align: 'center', width: "100"},
{key: 'operationTime', visible: true, label: '运维时间', minWidth: null, sortable: false, overflow: false, align: 'center', width: "100"},
{key: 'expectedCompleteDate', visible: true, label: '开发时间', minWidth: null, sortable: false, overflow: false, align: 'center', width: "100"},
{key: 'maintenanceEndDate', visible: true, label: '运维时间', minWidth: null, sortable: false, overflow: false, align: 'center', width: "100"},
{key: 'ownerName', visible: true, label: '负责人', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'followName', visible: true, label: '跟进人', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'receivedAmount', visible: true, label: '已收金额', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'operationAmount', visible: true, label: '运维费用', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'status', visible: true, label: '状态', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'operationAmount', visible: false, label: '运维费用', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'remark', visible: true, label: '备注', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'createTime', visible: true, label: '创建时间', minWidth: null, sortable: true, overflow: false, align: 'center', width: "100"},
],
@ -222,10 +285,55 @@ export default {
},
};
},
activated() {
this.getList();
},
created() {
this.getList();
},
methods: {
//
handleView(row) {
this.$router.push({ path: `/view/project/${row.id}` });
},
//
handleMaintenance(row) {
this.showMaintenanceDialog = true;
this.row = row;
},
//
handleAccept(row) {
this.$modal.confirm('是否确认验收项目编号为"' + row.no + '"的数据项?')
.then(() => {
this.loading = true;
acceptProject(row.id).then(res => {
if (res.code === 200) {
this.$modal.msgSuccess("验收成功");
this.getList();
}
});
});
},
//
handleStart(row) {
this.showStartDialog = true;
this.row = row;
},
//
handleComplete(row) {
this.$modal.confirm('是否确认完成项目编号为"' + row.no + '"的数据项?')
.then(() => {
this.loading = true;
completeProject(row.id).then(res => {
if (res.code === 200) {
this.$modal.msgSuccess("完成成功");
this.getList();
}
}).catch(() => {
this.loading = false;
});
})
},
/** 当排序按钮被点击时触发 **/
onSortChange(column) {
if (column.order == null) {
@ -274,7 +382,7 @@ export default {
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.id || this.ids;
this.$modal.confirm('是否确认删除项目编号为"' + ids + '"的数据项?').then(function() {
this.$modal.confirm('是否确认删除项目ID为"' + ids + '"的数据项?').then(function() {
return delProject(ids);
}).then(() => {
this.getList();
@ -286,6 +394,10 @@ export default {
this.download('bst/project/export', {
...this.queryParams
}, `project_${new Date().getTime()}.xlsx`)
},
//
handleStartSuccess() {
this.getList();
}
}
};

View File

@ -0,0 +1,310 @@
<template>
<div class="app-container" v-loading="loading">
<el-row :gutter="20">
<el-col :span="18">
<!-- 金额信息 -->
<el-card class="box-card">
<div slot="header" class="info-header">
<span class="card-title">{{ detail.name }}</span>
<dict-tag :options="dict.type.project_status" :value="detail.status" />
</div>
<div class="amount-content">
<el-row :gutter="20">
<el-col :span="8">
<div class="amount-item">
<div class="amount-label">项目金额</div>
<div class="amount-value">¥ {{ detail.amount | fix2 | dv}}</div>
</div>
</el-col>
<el-col :span="8">
<div class="amount-item">
<div class="amount-label">已收金额</div>
<div class="amount-value">¥ {{ detail.receivedAmount | fix2 | dv}}</div>
</div>
</el-col>
<el-col :span="8">
<div class="amount-item">
<div class="amount-label">运维费用</div>
<div class="amount-value">{{ detail.operationAmount | dv}}</div>
</div>
</el-col>
</el-row>
</div>
</el-card>
<!-- 基本信息 -->
<el-card class="box-card main-info">
<div class="info-content">
<el-descriptions :column="3" border>
<el-descriptions-item label="项目编号">
{{ detail.no | dv}}
</el-descriptions-item>
<el-descriptions-item label="所属客户">
{{ detail.customerName | dv}}
</el-descriptions-item>
<el-descriptions-item label="到期时间">
{{ detail.expireTime | dv}}
</el-descriptions-item>
<el-descriptions-item label="负责人">
{{ detail.ownerName | dv}}
</el-descriptions-item>
<el-descriptions-item label="跟进人">
{{ detail.followName | dv}}
</el-descriptions-item>
<el-descriptions-item label="创建人">
{{ detail.createName | dv}}
</el-descriptions-item>
<el-descriptions-item label="备注" :span="3">
{{ detail.remark | dv}}
</el-descriptions-item>
</el-descriptions>
</div>
</el-card>
<!-- 附件信息 -->
<el-card class="box-card" v-if="attaches.length > 0">
<div slot="header" class="info-header">
<span class="card-title">附件信息</span>
<span class="attach-count"> {{ attaches.length }} 个附件</span>
</div>
<attach-list :file-list="attaches" :column="4" />
</el-card>
</el-col>
<el-col :span="6">
<!-- 时间线 -->
<el-card class="box-card">
<div slot="header" class="clearfix">
<span class="card-title">项目进度</span>
</div>
<div class="timeline-content">
<el-timeline>
<el-timeline-item
v-if="detail.createTime"
:timestamp="detail.createTime"
placement="top"
type="primary"
>
<div class="timeline-title">创建项目</div>
</el-timeline-item>
<el-timeline-item
v-if="detail.startTime"
:timestamp="detail.startTime"
placement="top"
type="warning"
>
<div class="timeline-title">开始开发</div>
<div class="timeline-extra">预计完成时间{{ detail.expectedCompleteDate }}</div>
</el-timeline-item>
<el-timeline-item
v-if="detail.completeTime"
:timestamp="detail.completeTime"
placement="top"
type="success"
>
<div class="timeline-title">开发完成</div>
</el-timeline-item>
<el-timeline-item
v-if="detail.acceptTime"
:timestamp="detail.acceptTime"
placement="top"
type="success"
>
<div class="timeline-title">项目验收</div>
</el-timeline-item>
<el-timeline-item
v-if="detail.maintenanceEndDate"
:timestamp="detail.maintenanceEndDate"
placement="top"
type="info"
>
<div class="timeline-title">运维结束</div>
</el-timeline-item>
</el-timeline>
</div>
</el-card>
</el-col>
</el-row>
<el-row>
<el-card class="box-card">
<el-tabs v-if="detail.id" lazy>
<el-tab-pane label="任务列表">
<task :query="{ projectId: detail.id }" :init-data="{projectId: detail.id}" :hide-columns="['projectName']"/>
</el-tab-pane>
</el-tabs>
</el-card>
</el-row>
</div>
</template>
<script>
import { getProject } from "@/api/bst/project";
import AttachList from '@/components/AttachList/index.vue';
import Task from '@/views/bst/task/index.vue';
export default {
name: "ProjectView",
components: {
AttachList,
Task
},
dicts: ['project_status'],
data() {
return {
detail: {},
loading: false,
//
imageTypes: ['.png', '.jpg', '.jpeg', '.gif', '.bmp']
}
},
computed: {
attaches() {
if (this.detail.attaches) {
return this.detail.attaches.split(',');
}
return [];
}
},
created() {
this.getDetail();
},
methods: {
getDetail() {
this.loading = true;
getProject(this.$route.params.id).then(res => {
this.detail = res.data;
}).finally(() => {
this.loading = false;
});
},
getStatusLabel(status) {
const dict = this.dict.type.project_status;
const item = dict.find(d => d.value === status);
return item ? item.label : status;
},
//
isImage(url) {
if (!url) return false;
return this.imageTypes.some(type => url.toLowerCase().endsWith(type));
},
// URL
getImageList() {
if (!this.detail.attaches) return [];
return this.detail.attaches
.filter(item => this.isImage(item.url))
.map(item => item.url);
}
}
}
</script>
<style lang="scss" scoped>
.box-card {
margin-bottom: 20px;
border-radius: 8px;
.info-header {
display: flex;
justify-content: space-between;
align-items: center;
.attach-count {
font-size: 13px;
color: #909399;
}
}
&.main-info {
.el-card__header {
padding: 15px 20px;
border-bottom: 1px solid #ebeef5;
.card-title {
font-size: 18px;
font-weight: 600;
color: #1f2f3d;
}
.status-tag {
margin-left: 12px;
}
}
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #1f2f3d;
}
}
.info-content {
padding: 10px 0;
}
.amount-content {
.amount-item {
text-align: center;
padding: 15px;
background: #f8f9fb;
border-radius: 6px;
.amount-label {
color: #909399;
margin-bottom: 8px;
font-size: 14px;
}
.amount-value {
color: #1f2f3d;
font-size: 20px;
font-weight: 600;
}
}
}
.timeline-content {
.timeline-title {
font-weight: 500;
color: #1f2f3d;
}
.timeline-extra {
font-size: 13px;
color: #909399;
margin-top: 4px;
}
}
.remark-content {
color: #606266;
line-height: 1.6;
padding: 10px;
}
.mt20 {
margin-top: 20px;
}
//
.image-slot {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: #f5f7fa;
i {
font-size: 30px;
color: #909399;
}
}
</style>

View File

@ -17,7 +17,7 @@
<el-input v-model="form.name" placeholder="请输入任务名称" />
</form-col>
<form-col :span="span" label="项目" prop="projectId">
<project-select v-model="form.projectId" />
<project-select v-model="form.projectId" :disabled="initData.projectId != null"/>
</form-col>
<form-col :span="24" label="类型" prop="type">
<el-radio-group v-model="form.type">
@ -33,7 +33,7 @@
<el-input v-model="form.description" placeholder="请输入任务内容" type="textarea" maxlength="1000" show-word-limit/>
</form-col>
<form-col :span="span" label="负责人" prop="ownerId">
<user-input v-model="form.ownerId" />
<user-select v-model="form.ownerId" />
</form-col>
<form-col :span="span" label="截止时间" prop="expireTime">
<el-date-picker clearable
@ -56,13 +56,13 @@
<script>
import { getTask, addTask, updateTask } from "@/api/bst/task";
import FormCol from "@/components/FormCol/index.vue";
import UserInput from '@/components/Business/User/UserInput.vue';
import ProjectSelect from '@/components/Business/Project/ProjectSelect.vue';
import { TaskType, TaskLevel } from '@/utils/enums';
import UserSelect from '@/components/Business/User/UserSelect.vue';
export default {
name: "TaskEditDialog",
components: { FormCol, UserInput, ProjectSelect },
components: { FormCol, ProjectSelect, UserSelect },
dicts: ['task_status', 'task_level', 'task_type'],
props: {
show: {
@ -72,6 +72,10 @@ export default {
id: {
type: [String, Number],
default: null
},
initData: {
type: Object,
default: () => ({})
}
},
data() {
@ -132,7 +136,8 @@ export default {
picture: null,
description: null,
expireTime: null,
ownerId: null
ownerId: null,
...this.initData
};
this.resetForm("form");
},

View File

@ -1,10 +1,10 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="项目" prop="projectId">
<el-form-item label="项目" prop="projectId" v-if="isShow('projectId')">
<project-select v-model="queryParams.projectId" @change="handleQuery"/>
</el-form-item>
<el-form-item label="任务名称" prop="name">
<el-form-item label="任务名称" prop="name" v-if="isShow('name')">
<el-input
v-model="queryParams.name"
placeholder="请输入任务名称"
@ -12,7 +12,7 @@
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="类型" prop="type">
<el-form-item label="类型" prop="type" v-if="isShow('type')">
<el-select v-model="queryParams.type" placeholder="请选择类型" clearable @change="handleQuery">
<el-option
v-for="dict in dict.type.task_type"
@ -22,7 +22,7 @@
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-form-item label="状态" prop="status" v-if="isShow('status')">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable @change="handleQuery">
<el-option
v-for="dict in dict.type.task_status"
@ -32,7 +32,7 @@
/>
</el-select>
</el-form-item>
<el-form-item label="优先级" prop="level">
<el-form-item label="优先级" prop="level" v-if="isShow('level')">
<el-select v-model="queryParams.level" placeholder="请选择优先级" clearable @change="handleQuery">
<el-option
v-for="dict in dict.type.task_level"
@ -42,7 +42,7 @@
/>
</el-select>
</el-form-item>
<el-form-item label="创建人" prop="createName">
<el-form-item label="创建人" prop="createName" v-if="isShow('createName')">
<el-input
v-model="queryParams.createName"
placeholder="请输入创建人"
@ -50,7 +50,7 @@
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="负责人" prop="ownerName">
<el-form-item label="负责人" prop="ownerName" v-if="isShow('ownerName')">
<el-input
v-model="queryParams.ownerName"
placeholder="请输入负责人"
@ -167,6 +167,7 @@
<task-edit-dialog
:show.sync="open"
:id="row.id"
:init-data="initData"
@success="getList"
/>
</div>
@ -190,6 +191,16 @@ export default {
mixins: [$showColumns],
dicts: ['task_status', 'task_level', 'task_type'],
components: {FormCol, TaskEditDialog, ProjectSelect},
props: {
initData: {
type: Object,
default: () => ({})
},
query: {
type: Object,
default: () => ({})
}
},
data() {
return {
span: 24,
@ -203,9 +214,9 @@ export default {
{key: 'status', visible: true, label: '状态', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'description', visible: true, label: '描述', minWidth: null, sortable: false, overflow: true, align: 'center', width: null},
{key: 'picture', visible: true, label: '图片', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'createName', visible: true, label: '创建人', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'expireTime', visible: true, label: '截止时间', minWidth: null, sortable: false, overflow: false, align: 'center', width: "100"},
{key: 'ownerName', visible: true, label: '负责人', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'expireTime', visible: true, label: '截止时间', minWidth: null, sortable: false, overflow: false, align: 'center', width: "100"},
{key: 'createName', visible: true, label: '创建人', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'createTime', visible: true, label: '创建时间', minWidth: null, sortable: true, overflow: false, align: 'center', width: "100"},
],
//
@ -279,6 +290,12 @@ export default {
};
},
created() {
this.initColumns();
this.queryParams = {
...this.queryParams,
...this.query
}
this.getList();
},
methods: {

View File

@ -0,0 +1,455 @@
<template>
<div class="dashboard-panel" v-loading="loading">
<!-- 项目统计 -->
<div class="stat-section">
<div class="section-title">项目统计</div>
<el-row :gutter="10">
<el-col :xs="12" :sm="4" :md="4" :lg="4">
<div class="stat-card project" :class="{'hover': hoveredCard === 'project-total'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-folder"></i>
</div>
<div class="card-label">合计项目</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.project.total || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="4" :md="4" :lg="4">
<div class="stat-card project" :class="{'hover': hoveredCard === 'project-progress'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-refresh"></i>
</div>
<div class="card-label">进行中</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.project.inProgress || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="4" :md="4" :lg="4">
<div class="stat-card project" :class="{'hover': hoveredCard === 'project-completed'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-check"></i>
</div>
<div class="card-label">已完成</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.project.completed || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="4" :md="4" :lg="4">
<div class="stat-card project" :class="{'hover': hoveredCard === 'project-overdue'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-refresh"></i>
</div>
<div class="card-label">维护中</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.project.maintenance || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="4" :md="4" :lg="4">
<div class="stat-card project warning" :class="{'hover': hoveredCard === 'project-overdue'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-warning"></i>
</div>
<div class="card-label">开发超期</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.project.developmentOverdue || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="4" :md="4" :lg="4">
<div class="stat-card project warning" :class="{'hover': hoveredCard === 'project-overdue'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-warning"></i>
</div>
<div class="card-label">维护到期</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.project.maintenanceOverdue || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
</el-row>
</div>
<!-- 任务统计 -->
<div class="stat-section">
<div class="section-title">任务统计</div>
<el-row :gutter="10">
<el-col :xs="12" :sm="6" :md="4" :lg="4">
<div class="stat-card task" :class="{'hover': hoveredCard === 'task-total'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-tickets"></i>
</div>
<div class="card-label">总任务</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.task.total || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="6" :md="4" :lg="4">
<div class="stat-card task" :class="{'hover': hoveredCard === 'task-wait'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-timer"></i>
</div>
<div class="card-label">待完成</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.task.waitCompleted || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="6" :md="4" :lg="4">
<div class="stat-card task" :class="{'hover': hoveredCard === 'task-progress'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-refresh"></i>
</div>
<div class="card-label">进行中</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.task.inProgress || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="6" :md="4" :lg="4">
<div class="stat-card task" :class="{'hover': hoveredCard === 'task-confirm'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-document-checked"></i>
</div>
<div class="card-label">待确认</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.task.waitConfirm || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="6" :md="4" :lg="4">
<div class="stat-card task success" :class="{'hover': hoveredCard === 'task-completed'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-circle-check"></i>
</div>
<div class="card-label">已完成</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.task.completed || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
</el-row>
</div>
<!-- 客户统计 -->
<div class="stat-section">
<div class="section-title">客户统计</div>
<el-row :gutter="10">
<el-col :xs="12" :sm="6" :md="4" :lg="4">
<div class="stat-card customer" :class="{'hover': hoveredCard === 'customer-total'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-user"></i>
</div>
<div class="card-label">总客户</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.customer.total || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="6" :md="4" :lg="4">
<div class="stat-card customer" :class="{'hover': hoveredCard === 'customer-today'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-date"></i>
</div>
<div class="card-label">今日新增</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.customer.today || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="6" :md="4" :lg="4">
<div class="stat-card customer" :class="{'hover': hoveredCard === 'customer-potential'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-user-solid"></i>
</div>
<div class="card-label">潜在客户</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.customer.potential || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="6" :md="4" :lg="4">
<div class="stat-card customer" :class="{'hover': hoveredCard === 'customer-intention'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-star-on"></i>
</div>
<div class="card-label">意向客户</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.customer.intention || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="6" :md="4" :lg="4">
<div class="stat-card customer success" :class="{'hover': hoveredCard === 'customer-transaction'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-shopping-cart-full"></i>
</div>
<div class="card-label">成交客户</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.customer.transaction || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="6" :md="4" :lg="4">
<div class="stat-card customer warning" :class="{'hover': hoveredCard === 'customer-invalid'}">
<div class="card-header">
<div class="card-icon">
<i class="el-icon-close"></i>
</div>
<div class="card-label">失效客户</div>
</div>
<div class="card-value">
<count-to :start-val="0" :end-val="data.customer.invalid || 0" :duration="2000" class="num"/>
</div>
</div>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
import CountTo from 'vue-count-to'
import {getBrief} from "@/api/bst/dashboard";
export default {
data() {
return {
loading: false,
hoveredCard: '',
data: {
project: {},
task: {},
customer: {}
}
}
},
components: {
CountTo
},
created() {
this.getData()
},
methods: {
getData() {
this.loading = true;
getBrief().then(res => {
this.data = res.data;
}).finally(() => {
this.loading = false;
})
}
}
}
</script>
<style lang="scss" scoped>
.dashboard-panel {
padding: 15px;
.stat-section {
margin-bottom: 20px;
.section-title {
font-size: 16px;
font-weight: 600;
color: #1f2f3d;
margin-bottom: 15px;
padding-left: 8px;
border-left: 3px solid #409EFF;
}
.stat-card {
background: #fff;
border-radius: 4px;
padding: 10px;
text-align: left;
box-shadow: 0 1px 4px rgba(0,0,0,.1);
transition: all .3s;
margin-bottom: 10px;
cursor: pointer;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 0;
opacity: 1;
transition: opacity .3s;
}
&:hover::before {
opacity: 1.2;
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 6px;
position: relative;
z-index: 1;
.card-icon {
margin-right: 6px;
i {
font-size: 16px;
}
}
.card-label {
font-size: 13px;
color: #909399;
flex: 1;
}
}
.card-value {
position: relative;
z-index: 1;
.num {
font-size: 22px;
font-weight: bold;
color: #1f2f3d;
}
}
&.project {
border-top: 2px solid #8064ff;
&::before {
background: linear-gradient(135deg, rgba(128, 100, 255, 0.08) 0%, rgba(255, 255, 255, 0) 45%, rgba(128, 100, 255, 0.1) 100%);
}
.card-icon {
color: #8064ff;
}
}
&.task {
border-top: 2px solid #ff9c6e;
&::before {
background: linear-gradient(135deg, rgba(255, 156, 110, 0.08) 0%, rgba(255, 255, 255, 0) 45%, rgba(255, 156, 110, 0.1) 100%);
}
.card-icon {
color: #ff9c6e;
}
}
&.customer {
border-top: 2px solid #20c997;
&::before {
background: linear-gradient(135deg, rgba(32, 201, 151, 0.08) 0%, rgba(255, 255, 255, 0) 45%, rgba(32, 201, 151, 0.1) 100%);
}
.card-icon {
color: #20c997;
}
}
&.warning {
border-top: 2px solid #ff4d4f;
&::before {
background: linear-gradient(135deg, rgba(255, 77, 79, 0.08) 0%, rgba(255, 255, 255, 0) 45%, rgba(255, 77, 79, 0.1) 100%);
}
.card-icon {
color: #ff4d4f;
}
}
&.success {
border-top: 2px solid #52c41a;
&::before {
background: linear-gradient(135deg, rgba(82, 196, 26, 0.08) 0%, rgba(255, 255, 255, 0) 45%, rgba(82, 196, 26, 0.1) 100%);
}
.card-icon {
color: #52c41a;
}
}
&:hover, &.hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,.15);
}
}
}
}
@media screen and (max-width: 768px) {
.dashboard-panel {
padding: 10px;
.stat-section {
margin-bottom: 15px;
.section-title {
font-size: 14px;
margin-bottom: 10px;
}
.stat-card {
padding: 8px;
.card-header {
margin-bottom: 4px;
.card-icon {
i {
font-size: 14px;
}
}
.card-label {
font-size: 12px;
}
}
.card-value {
.num {
font-size: 18px;
}
}
}
}
}
}
</style>

View File

@ -1,12 +1,21 @@
<template>
<div class="dashboard-editor-container">
<div class="app-container">
<el-row>
<el-col :span="18">
<panel-group />
</el-col>
<el-col :span="6">
</el-col>
</el-row>
</div>
</template>
<script>
import PanelGroup from './dashboard/mixins/PanelGroup.vue';
export default {
name: 'Index',
components: { PanelGroup },
data() {
return {
}