临时提交

This commit is contained in:
磷叶 2025-02-19 18:10:54 +08:00
parent 44fff551dc
commit 916efb8df5
20 changed files with 1133 additions and 321 deletions

View File

@ -0,0 +1,12 @@
import request from '@/utils/request'
// 每日新增客户统计
export function dailyCreateCountCustomer(params) {
return request({
url: '/dashboard/customer/dailyCreateCount',
method: 'get',
params
})
}

View File

@ -1,10 +1,19 @@
import request from '@/utils/request'
// 获取概览
export function getBrief() {
// 获取本人概览
export function getMineBrief() {
return request({
url: '/dashboard/brief',
url: '/dashboard/mineBrief',
method: 'get'
})
}
// 获取概览
export function getBrief(params) {
return request({
url: '/dashboard/brief',
method: 'get',
params
})
}

View File

@ -5,16 +5,8 @@
backgroundColor: backgroundColor
}"
>
<el-avatar
:size="size"
:src="src"
v-if="src"
>
</el-avatar>
<el-avatar
:size="size"
v-else
>
<el-avatar :size="size" :src="src" v-if="src"/>
<el-avatar :size="size" v-else :style="{fontSize: fontSize}">
{{ displayChar }}
</el-avatar>
</div>
@ -40,12 +32,11 @@ export default {
type: Number,
default: 0
},
id: {
type: [String, Number],
default: ''
}
},
computed: {
fontSize() {
return `${this.size / 1.8}px`
},
displayChar() {
if (!this.name) {
return '?';
@ -66,7 +57,12 @@ export default {
'#F8BBD0', //
'#D7CCC8' //
]
const index = parseInt(this.id) % colors.length
//
const nameStr = this.name || ''
const sum = nameStr.split('').reduce((acc, char) => {
return acc + char.charCodeAt(0)
}, 0)
const index = Math.abs(sum) % colors.length
return colors[index]
}
}

View File

@ -1,5 +1,5 @@
<template>
<el-tag v-if="value != null" :type="value ? trueType : falseType" :size="size">{{value ? trueText : falseText}}</el-tag>
<el-tag style="display: inline-block;" v-if="value != null" :type="value ? trueType : falseType" :size="size">{{value ? trueText : falseText}}</el-tag>
</template>
<script>
export default {

View File

@ -0,0 +1,76 @@
<template>
<div class="line-field">
<div class="label">
<slot name="label">
{{label | dv}}
</slot>
</div>
<div class="right-box" >
<slot>
{{value | dv}}
</slot>
</div>
<el-button type="text" v-if="editable" icon="el-icon-edit" class="editable" @click="$emit('edit')"/>
</div>
</template>
<script>
export default {
name: "LineField",
props: {
label: {
type: String,
default: null
},
value: {
type: [String, Number],
default: null,
},
editable: {
type: Boolean,
default: false
}
}
}
</script>
<style scoped lang="scss">
.line-field {
position: relative;
width: 100%;
padding: 0.5em 1.5em 0.5em 1em;
font-size: 14px;
display: flex;
height: fit-content;
line-height: 1.5em;
justify-content: space-between;
.label {
width: fit-content;
margin-right: 2em;
}
.right-box {
flex: 1;
text-align: right;
display: flex;
flex-direction: row;
justify-content: flex-end;
vertical-align: center;
}
i {
line-height: 1.5em;
margin: 0 0.5em;
}
.editable {
position: absolute;
padding: 0;
top: 10px;
right: 4px;
}
}
.line-field:nth-child(n + 2) {
border-top: 1px solid #ededed;
}
.line-field:hover {
background-color: rgba(131, 180, 240, 0.08);
}
</style>

View File

@ -0,0 +1,113 @@
<template>
<div class="statistics-card">
<div class="card-content">
<div class="info">
<div class="value">{{ value }}</div>
<div class="label">{{ label }}</div>
</div>
<div class="icon-wrapper">
<i :class="icon" :style="iconStyle"></i>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'StatisticsCard',
props: {
value: {
type: [Number, String],
default: null,
},
label: {
type: String,
required: true
},
icon: {
type: String,
required: true
},
startColor: {
type: String,
required: true
},
endColor: {
type: String,
required: true
}
},
computed: {
iconStyle() {
return {
background: `linear-gradient(135deg, ${this.startColor}, ${this.endColor})`,
'-webkit-background-clip': 'text',
'-webkit-text-fill-color': 'transparent',
'background-clip': 'text'
}
}
}
}
</script>
<style lang="scss" scoped>
.statistics-card {
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
padding: 12px;
height: 70px;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px 0 rgba(0,0,0,.15);
}
.card-content {
position: relative;
height: 100%;
z-index: 1;
.info {
position: relative;
z-index: 2;
.value {
font-size: 18px;
font-weight: bold;
color: #303133;
line-height: 1.2;
margin-bottom: 4px;
}
.label {
font-size: 12px;
color: #606266;
}
}
.icon-wrapper {
position: absolute;
right: -8px;
bottom: -12px;
z-index: 1;
i {
font-size: 48px;
opacity: 0.15;
transition: all 0.3s ease;
}
}
}
&:hover {
.icon-wrapper i {
opacity: 0.25;
transform: scale(1.05);
}
}
}
</style>

View File

@ -86,7 +86,13 @@ export const constantRoutes = [
component: () => import('@/views/bst/project/edit/index.vue'),
name: 'ProjectEdit',
meta: { title: '编辑项目', noCache: false }
}
},
{
path: 'customer/:id?',
component: () => import('@/views/bst/customer/edit/index.vue'),
name: 'CustomerEdit',
meta: { title: '编辑客户', noCache: false }
},
]
},
/**
@ -108,6 +114,12 @@ export const constantRoutes = [
component: () => import('@/views/bst/customer/view/index.vue'),
name: 'CustomerView',
meta: { title: '客户详情', noCache: false }
},
{
path: 'user/:id?',
component: () => import('@/views/system/user/view/index.vue'),
name: 'UserView',
meta: { title: '用户详情', noCache: false }
}
]
},

View File

@ -22,7 +22,7 @@ module.exports = {
/**
* 是否固定头部
*/
fixedHeader: false,
fixedHeader: true,
/**
* 是否显示logo

View File

@ -1,3 +1,5 @@
import { parseTime } from '@/utils/ruoyi';
/**
* 计算周年
*/
@ -46,3 +48,62 @@ export function calcSecond(start, end) {
let endDate = toDate(end);
return Math.floor((endDate.getTime() - startDate.getTime()) / 1000);
}
// 获取前n天的日期
export function getLastDate(n) {
let now = new Date();
return new Date(now.getTime() - n * 24 * 3600 * 1000)
}
// 获取前n月的日期
export function getLastMonth(n) {
const date = new Date();
const year = date.getFullYear();
const month = date.getMonth();
const day = date.getDate();
// 减去 n 个月
date.setMonth(month - n);
// 确保日期不超过当月的最大天数
const maxDay = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
date.setDate(Math.min(day, maxDay));
return date;
}
// 获取前n月的日期字符串
export function getLastMonthDateStr(n) {
let date = getLastMonth(n);
return parseTime(date, "{y}-{m}-{d}")
}
// 获取前n天的日期字符串
export function getLastDateStr(n) {
let date = getLastDate(n);
return parseTime(date, "{y}-{m}-{d}");
}
// 获取前n天的日期时间字符串00:00:00
export function getLastDateTimeStartStr(n) {
let date = getLastDate(n);
return parseTime(date, "{y}-{m}-{d} 00:00:00");
}
// 获取前n天的日期时间字符串23:59:59
export function getLastDateTimeEndStr(n) {
let date = getLastDate(n);
return parseTime(date, "{y}-{m}-{d} 23:59:59");
}
// 获取前n天的日期时间00:00:00
export function getLastDateTimeStart(n) {
return new Date(getLastDateTimeStartStr(n));
}
// 获取前n天的日期时间23:59:59
export function getLastDateTimeEnd(n) {
return new Date(getLastDateTimeEndStr(n));
}

View File

@ -1,213 +0,0 @@
<template>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
width="800px"
append-to-body
:close-on-click-modal="false"
@close="handleClose"
@open="handleOpen"
>
<el-form ref="form" :model="form" :rules="rules" label-width="80px" v-loading="loading">
<el-row>
<form-col :span="24" label="客户名称" prop="name">
<el-input v-model="form.name" placeholder="请输入客户名称" />
</form-col>
<form-col :span="span" label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态" style="width: 100%;">
<el-option
v-for="dict in dict.type.customer_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</form-col>
<form-col :span="span" label="意向强度" prop="intentLevel">
<el-select v-model="form.intentLevel" placeholder="请选择意向强度" style="width: 100%;">
<el-option
v-for="dict in dict.type.customer_intent_level"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</form-col>
<form-col :span="span" label="手机号" prop="mobile">
<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="请输入微信号" maxlength="50" show-word-limit />
</form-col>
<form-col :span="span" label="来源" prop="source">
<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="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" />
</form-col>
<form-col :span="24" label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</form-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm" :loading="submitLoading"> </el-button>
<el-button @click="handleClose"> </el-button>
</div>
</el-dialog>
</template>
<script>
import { getCustomer, addCustomer, updateCustomer } from "@/api/bst/customer";
import FormCol from "@/components/FormCol/index.vue";
import { CustomerStatus, CustomerIntentLevel } from '@/utils/enums';
import { mapGetters } from 'vuex';
import UserSelect from '@/components/Business/User/UserSelect.vue';
export default {
name: "CustomerEditDialog",
components: { FormCol, UserSelect },
dicts: ['customer_intent_level', 'customer_status', 'customer_source', 'customer_intent'],
props: {
show: {
type: Boolean,
required: true
},
id: {
type: [String, Number],
default: null
}
},
data() {
return {
loading: false,
submitLoading: false,
span: 12,
//
form: {},
//
rules: {
code: [
{ required: true, message: "客户编号不能为空", trigger: "change" }
],
name: [
{ required: true, message: "客户姓名不能为空", trigger: "change" }
],
status: [
{ required: true, message: "状态不能为空", trigger: "change" }
],
intentLevel: [
{ required: true, message: "意向强度不能为空", trigger: "change" }
],
source: [
{ required: true, message: "来源不能为空", trigger: "change" }
],
followId: [
{ required: true, message: "跟进人不能为空", trigger: "change" }
]
}
};
},
computed: {
...mapGetters(['userId']),
title() {
return this.id ? '修改客户' : '新增客户';
},
dialogVisible: {
get() {
return this.show;
},
set(value) {
this.$emit('update:show', value);
}
}
},
methods: {
/** 获取详细信息 */
getInfo(id) {
this.loading = true;
getCustomer(id).then(response => {
this.form = response.data;
}).finally(() => {
this.loading = false;
});
},
//
reset() {
this.form = {
id: null,
code: null,
name: null,
status: CustomerStatus.POTENTIAL,
intentLevel: CustomerIntentLevel.MEDIUM,
mobile: null,
wechat: null,
source: null,
intents: [],
followId: this.userId,
remark: null
};
this.resetForm("form");
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.id != null) {
this.submitLoading = true;
updateCustomer(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.$emit('success');
this.handleClose();
}).finally(() => {
this.submitLoading = false;
});
} else {
this.submitLoading = true;
addCustomer(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.$emit('success');
this.handleClose();
}).finally(() => {
this.submitLoading = false;
});
}
}
});
},
//
handleClose() {
this.$emit('update:show', false);
},
//
handleOpen() {
if (this.id) {
this.getInfo(this.id);
} else {
this.reset();
}
}
}
};
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,221 @@
<template>
<div>
<edit-header :title="title">
<el-button @click="handleClose" plain icon="el-icon-close"> </el-button>
<el-button type="primary" @click="submitForm" :loading="submitLoading" plain icon="el-icon-check"> </el-button>
</edit-header>
<div class="app-container">
<el-form ref="form" :model="form" :rules="rules" label-width="80px" v-loading="loading">
<el-row>
<form-col :span="24" label="客户名称" prop="name">
<el-input v-model="form.name" placeholder="请输入客户名称" />
</form-col>
<form-col :span="span" label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态" style="width: 100%;">
<el-option
v-for="dict in dict.type.customer_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</form-col>
<form-col :span="span" label="意向强度" prop="intentLevel">
<el-select v-model="form.intentLevel" placeholder="请选择意向强度" style="width: 100%;">
<el-option
v-for="dict in dict.type.customer_intent_level"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</form-col>
<form-col :span="span" label="手机号" prop="mobile">
<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="请输入微信号" maxlength="50" show-word-limit />
</form-col>
<form-col :span="span" label="来源" prop="source">
<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="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" />
</form-col>
<form-col :span="24" label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</form-col>
</el-row>
<el-tabs v-if="form.id == null">
<el-tab-pane label="跟进记录">
<customer-follow-form :form="form.follow" :hide="['customerId']" :prop-prefix="'follow.'" :span="span"/>
</el-tab-pane>
</el-tabs>
</el-form>
</div>
</div>
</template>
<script>
import { getCustomer, addCustomer, updateCustomer } from "@/api/bst/customer";
import FormCol from "@/components/FormCol/index.vue";
import { CustomerStatus, CustomerIntentLevel } from '@/utils/enums';
import { mapGetters } from 'vuex';
import UserSelect from '@/components/Business/User/UserSelect.vue';
import CustomerFollowForm from '@/views/bst/customerFollow/components/CustomerFollowForm.vue';
import { parseTime } from '@/utils/ruoyi';
import EditHeader from '@/components/EditHeader/index.vue';
export default {
name: "CustomerEdit",
components: { FormCol, UserSelect, CustomerFollowForm, EditHeader },
dicts: ['customer_intent_level', 'customer_status', 'customer_source', 'customer_intent'],
data() {
return {
loading: false,
submitLoading: false,
span: 6,
//
form: {},
//
rules: {
code: [
{ required: true, message: "客户编号不能为空", trigger: "change" }
],
name: [
{ required: true, message: "客户姓名不能为空", trigger: "change" }
],
status: [
{ required: true, message: "状态不能为空", trigger: "change" }
],
intentLevel: [
{ required: true, message: "意向强度不能为空", trigger: "change" }
],
source: [
{ required: true, message: "来源不能为空", trigger: "change" }
],
followId: [
{ required: true, message: "跟进人不能为空", trigger: "change" }
],
follow: {
type: [
{ required: true, message: "跟进方式不能为空", trigger: "change" }
],
content: [
{ required: true, message: "跟进内容不能为空", trigger: "change" }
],
followTime: [
{ required: true, message: "跟进时间不能为空", trigger: "change" }
]
}
},
id: null,
};
},
computed: {
...mapGetters(['userId']),
title() {
return this.id ? '修改客户' : '新增客户';
},
dialogVisible: {
get() {
return this.show;
},
set(value) {
this.$emit('update:show', value);
}
}
},
created() {
this.id = this.$route.params.id;
if (this.id) {
this.getInfo(this.id);
} else {
this.reset();
}
},
methods: {
/** 获取详细信息 */
getInfo(id) {
this.loading = true;
getCustomer(id).then(response => {
this.form = response.data;
}).finally(() => {
this.loading = false;
});
},
//
reset() {
this.form = {
id: null,
code: null,
name: null,
status: CustomerStatus.POTENTIAL,
intentLevel: CustomerIntentLevel.MEDIUM,
mobile: null,
wechat: null,
source: null,
intents: [],
followId: this.userId,
remark: null,
follow: {
followTime: parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'),
}
};
this.resetForm("form");
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.id != null) {
this.submitLoading = true;
updateCustomer(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.$emit('success');
this.handleClose();
}).finally(() => {
this.submitLoading = false;
});
} else {
this.submitLoading = true;
addCustomer(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.$emit('success');
this.handleClose();
}).finally(() => {
this.submitLoading = false;
});
}
}
});
},
//
handleClose() {
this.$tab.closeBack();
},
}
};
</script>
<style lang="scss" scoped>
</style>

View File

@ -154,7 +154,7 @@
</template>
</el-table-column>
</template>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180">
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180" fixed="right">
<template slot-scope="scope">
<el-button
size="mini"
@ -196,13 +196,6 @@
@pagination="getList"
/>
<!-- 添加或修改客户对话框 -->
<customer-edit-dialog
:show.sync="open"
:id="row.id"
@success="getList"
/>
<!-- 跟进对话框 -->
<customer-follow-edit-dialog
:show.sync="showFollowDialog"
@ -216,7 +209,6 @@
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';
import CustomerLink from '@/components/Business/Customer/CustomerLink.vue';
import CustomerFollowEditDialog from '@/views/bst/customerFollow/components/CustomerFollowEditDialog.vue';
//
@ -229,7 +221,7 @@ export default {
name: "Customer",
mixins: [$showColumns],
dicts: ['customer_intent_level', 'customer_status'],
components: {FormCol, CustomerEditDialog, CustomerLink, CustomerFollowEditDialog},
components: {FormCol, CustomerLink, CustomerFollowEditDialog},
data() {
return {
showFollowDialog: false,
@ -351,13 +343,11 @@ export default {
},
/** 新增按钮操作 */
handleAdd() {
this.row = {};
this.open = true;
this.$router.push(`/edit/customer`);
},
/** 修改按钮操作 */
handleUpdate(row) {
this.row = row;
this.open = true;
this.$router.push(`/edit/customer/${row.id}`);
},
/** 删除按钮操作 */
handleDelete(row) {

View File

@ -9,49 +9,7 @@
@open="handleOpen"
>
<el-form ref="form" :model="form" :rules="rules" label-width="80px" v-loading="loading">
<el-row>
<form-col :span="24" label="图片" prop="picture">
<image-upload v-model="form.picture" />
</form-col>
<form-col :span="span" label="客户" prop="customerId">
<customer-input v-model="form.customerId" :text.sync="form.customerName" :disabled="initData.customerId != null"/>
</form-col>
<form-col :span="span" label="跟进方式" prop="type">
<el-select v-model="form.type" placeholder="请选择跟进方式" style="width: 100%;">
<el-option
v-for="dict in dict.type.customer_follow_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</form-col>
<form-col :span="24" label="跟进内容" prop="content">
<el-input v-model="form.content" type="textarea" placeholder="请输入跟进内容" maxlength="1000" show-word-limit :autosize="{ minRows: 4 }"/>
</form-col>
<form-col :span="span" label="跟进时间" prop="followTime">
<el-date-picker clearable
style="width: 100%;"
v-model="form.followTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
default-time="09:00:00"
:picker-options="DatePickerOptions.DISABLE_FUTURE"
placeholder="请选择跟进时间">
</el-date-picker>
</form-col>
<form-col :span="span" label="下次跟进" prop="nextFollowTime">
<el-date-picker clearable
style="width: 100%;"
v-model="form.nextFollowTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
default-time="09:00:00"
:picker-options="DatePickerOptions.DISABLE_PAST"
placeholder="请选择下次跟进时间">
</el-date-picker>
</form-col>
</el-row>
<customer-follow-form :form="form" :disabled="disabledKeys" :span="span"/>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm" :loading="submitLoading"> </el-button>
@ -62,17 +20,13 @@
</template>
<script>
import { getCustomerFollow, addCustomerFollow, updateCustomerFollow } from "@/api/bst/customerFollow";
import FormCol from "@/components/FormCol/index.vue";
import UserSelect from '@/components/Business/User/UserSelect.vue';
import CustomerInput from '@/components/Business/Customer/CustomerInput.vue';
import { getCustomerFollow, addCustomerFollow, updateCustomerFollow } from "@/api/bst/customerFollow"
import { mapGetters } from 'vuex';
import { parseTime } from '@/utils/ruoyi.js';
import { DatePickerOptions } from '@/utils/constants.js';
import CustomerFollowForm from '@/views/bst/customerFollow/components/CustomerFollowForm.vue';
export default {
name: "CustomerFollowEditDialog",
components: { FormCol, UserSelect, CustomerInput },
dicts: ['customer_follow_type'],
components: { CustomerFollowForm },
props: {
show: {
type: Boolean,
@ -85,11 +39,10 @@ export default {
initData: {
type: Object,
default: () => ({})
}
},
},
data() {
return {
DatePickerOptions,
loading: false,
submitLoading: false,
span: 12,
@ -105,8 +58,12 @@ export default {
],
content: [
{ required: true, message: "跟进内容不能为空", trigger: "change" }
],
followTime: [
{ required: true, message: "跟进时间不能为空", trigger: "change" }
]
}
},
disabledKeys: [],
};
},
computed: {
@ -148,6 +105,12 @@ export default {
customerName: null,
...this.initData
};
this.disabledKeys = [];
Object.keys(this.initData).forEach(key => {
if (this.initData[key] != null) {
this.disabledKeys.push(key);
}
});
this.resetForm("form");
},
/** 提交按钮 */

View File

@ -0,0 +1,99 @@
<template>
<el-row>
<form-col :span="24" label="图片" :prop="propPrefix + 'picture'">
<image-upload v-model="form.picture" />
</form-col>
<form-col :span="span" label="客户" prop="customerId" v-if="!isHide('customerId')">
<customer-input v-model="form.customerId" :text.sync="form.customerName" :disabled="isDisabled('customerId')"/>
</form-col>
<form-col :span="span" label="跟进方式" :prop="propPrefix + 'type'">
<el-select v-model="form.type" placeholder="请选择跟进方式" style="width: 100%;">
<el-option
v-for="dict in dict.type.customer_follow_type"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</form-col>
<form-col :span="24" label="跟进内容" :prop="propPrefix + 'content'">
<el-input v-model="form.content" type="textarea" placeholder="请输入跟进内容" maxlength="1000" show-word-limit :autosize="{ minRows: 4 }"/>
</form-col>
<form-col :span="span" label="跟进时间" :prop="propPrefix + 'followTime'">
<el-date-picker clearable
style="width: 100%;"
v-model="form.followTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
default-time="09:00:00"
:picker-options="DatePickerOptions.DISABLE_FUTURE"
placeholder="请选择跟进时间">
</el-date-picker>
</form-col>
<form-col :span="span" label="下次跟进" :prop="propPrefix + 'nextFollowTime'">
<el-date-picker clearable
style="width: 100%;"
v-model="form.nextFollowTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
default-time="09:00:00"
:picker-options="DatePickerOptions.DISABLE_PAST"
placeholder="请选择下次跟进时间">
</el-date-picker>
</form-col>
</el-row>
</template>
<script>
import { DatePickerOptions } from '@/utils/constants.js';
import FormCol from '@/components/FormCol/index.vue';
import CustomerInput from '@/components/Business/Customer/CustomerInput.vue';
export default {
name: 'CustomerFollowForm',
components: { FormCol, CustomerInput },
dicts: ['customer_follow_type'],
props: {
form: {
type: Object,
default: () => ({})
},
hide: {
type: Array,
default: () => []
},
disabled: {
type: Array,
default: null,
},
span: {
type: Number,
default: 12
},
propPrefix: {
type: String,
default: ''
}
},
data() {
return {
DatePickerOptions,
}
},
computed: {
isHide() {
return (key) => {
return this.hide.includes(key);
}
},
isDisabled() {
return (key) => {
return this.disabled.includes(key);
}
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -4,7 +4,7 @@
<el-form-item label="项目" prop="projectId" v-if="isShow('projectName')">
<project-select v-model="queryParams.projectId" @change="handleQuery"/>
</el-form-item>
<el-form-item label="类型" prop="type" v-if="isShow('type')">
<el-form-item label="类型" prop="type" v-if="isShow('description')">
<el-select v-model="queryParams.type" placeholder="请选择类型" clearable @change="handleQuery">
<el-option
v-for="dict in dict.type.task_type"
@ -14,7 +14,7 @@
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status" v-if="isShow('status')">
<el-form-item label="状态" prop="status" v-if="isShow('description')">
<el-radio-group v-model="queryParams.status" placeholder="请选择状态" clearable @change="handleQuery">
<el-radio-button :label="null">全部</el-radio-button>
<el-radio-button
@ -24,7 +24,7 @@
>{{ dict.label }}</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="优先级" prop="level" v-if="isShow('level')">
<el-form-item label="优先级" prop="level" v-if="isShow('description')">
<el-select v-model="queryParams.level" placeholder="请选择优先级" clearable @change="handleQuery">
<el-option
v-for="dict in dict.type.task_level"
@ -34,6 +34,13 @@
/>
</el-select>
</el-form-item>
<el-form-item label="是否逾期" prop="overdue" v-if="isShow('description')">
<el-radio-group v-model="queryParams.overdue" placeholder="请选择是否逾期" clearable @change="handleQuery">
<el-radio-button :label="null">全部</el-radio-button>
<el-radio-button :label="true">逾期</el-radio-button>
<el-radio-button :label="false">正常</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="创建人" prop="createId" v-if="isShow('createName')">
<user-select v-model="queryParams.createId" @change="handleQuery"/>
</el-form-item>
@ -101,15 +108,10 @@
</template>
<template v-else-if="column.key === 'description'">
<el-link :underline="false" @click="handleView(d.row)">{{d.row.description}}</el-link>
</template>
<template v-else-if="column.key === 'type'">
<dict-tag :options="dict.type.task_type" :value="d.row.type"/>
</template>
<template v-else-if="column.key === 'status'">
<dict-tag :options="dict.type.task_status" :value="d.row.status"/>
</template>
<template v-else-if="column.key === 'level'">
<dict-tag :options="dict.type.task_level" :value="d.row.level" />
<dict-tag style="margin-left: 4px" :options="dict.type.task_status" :value="d.row.status" size="mini"/>
<dict-tag style="margin-left: 4px" :options="dict.type.task_type" :value="d.row.type" size="mini"/>
<dict-tag style="margin-left: 4px" :options="dict.type.task_level" :value="d.row.level" size="mini"/>
<boolean-tag style="margin-left: 4px" v-if="d.row.overdue " :value="!d.row.overdue" true-text="正常" false-text="逾期" size="mini"/>
</template>
<template v-else-if="column.key === 'picture'">
<image-preview :src="d.row[column.key]" :width="50" :height="50"/>
@ -195,7 +197,7 @@ import ProjectLink from '@/components/Business/Project/ProjectLink.vue';
import AvatarList from '@/components/AvatarList/index.vue';
import UserSelect from '@/components/Business/User/UserSelect.vue';
import {TaskStatus} from '@/utils/enums'
import BooleanTag from '@/components/BooleanTag'
//
const defaultSort = {
prop: "createTime",
@ -206,7 +208,7 @@ export default {
name: "Task",
mixins: [$showColumns, $task],
dicts: ['task_status', 'task_level', 'task_type'],
components: {FormCol, TaskEditDialog, ProjectSelect, TaskViewDialog, ProjectLink, AvatarList, UserSelect},
components: {FormCol, TaskEditDialog, ProjectSelect, TaskViewDialog, ProjectLink, AvatarList, UserSelect, BooleanTag},
props: {
initData: {
type: Object,
@ -269,7 +271,8 @@ export default {
level: null,
createId: null,
ownerId: null,
deleted: null
deleted: null,
overdue: null,
},
//
row: {},

View File

@ -1,7 +1,7 @@
<template>
<div class="dashboard-panel" v-loading="loading">
<!-- 项目统计 -->
<el-card class="card-box" header="项目统计">
<el-card class="card-box" header="参与项目">
<el-row :gutter="10">
<el-col :span="8">
<div class="stat-card project" :class="{'hover': hoveredCard === 'project-total'}">
@ -86,7 +86,7 @@
<!-- 任务统计 -->
<el-card class="card-box" header="任务统计">
<el-card class="card-box" header="负责任务">
<el-row :gutter="10">
<el-col :span="8">
<div class="stat-card task" :class="{'hover': hoveredCard === 'task-total'}">
@ -170,7 +170,7 @@
</el-card>
<!-- 客户统计 -->
<el-card class="card-box" header="客户统计">
<el-card class="card-box" header="我的客户">
<el-row :gutter="10">
<el-col :span="8">
<div class="stat-card customer" :class="{'hover': hoveredCard === 'customer-total'}">
@ -257,7 +257,7 @@
<script>
import CountTo from 'vue-count-to'
import {getBrief} from "@/api/dashboard/dashboard";
import {getMineBrief} from "@/api/dashboard/dashboard";
export default {
data() {
@ -280,7 +280,7 @@ export default {
methods: {
getData() {
this.loading = true;
getBrief().then(res => {
getMineBrief().then(res => {
this.data = res.data;
}).finally(() => {
this.loading = false;

View File

@ -172,6 +172,13 @@
class-name="small-padding fixed-width"
>
<template slot-scope="scope" v-if="scope.row.userId !== '1'">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleView(scope.row)"
v-hasPermi="['system:user:query']"
>详情</el-button>
<el-button
size="mini"
type="text"
@ -430,6 +437,9 @@ export default {
});
},
methods: {
handleView(row) {
this.$router.push(`/view/user/${row.userId}`);
},
isEmpty,
calcBirthDay,
calcFullYear,

View File

@ -0,0 +1,142 @@
<template>
<div v-loading="loading">
<el-date-picker
v-model="queryParams.createDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
size="mini"
@change="getList"
/>
<div ref="chart" class="chart-container"></div>
</div>
</template>
<script>
import * as echarts from 'echarts'
import { dailyCreateCountCustomer } from '@/api/dashboard/customer'
import { getLastDateStr } from '@/utils/date'
import resize from '@/views/dashboard/mixins/resize'
export default {
name: 'PerformanceChart',
mixins: [resize],
props: {
query: {
type: Object,
default: () => ({})
}
},
data() {
return {
list: [],
loading: false,
chart: null,
queryParams: {
createDateRange: [
getLastDateStr(7),
getLastDateStr(0)
]
}
}
},
created() {
this.queryParams = {
...this.queryParams,
...this.query
}
this.getList()
},
methods: {
getList() {
this.loading = true
dailyCreateCountCustomer(this.queryParams).then(res => {
this.list = res.data;
this.initChart()
}).finally(() => {
this.loading = false
})
},
initChart() {
if (this.chart) {
this.chart.dispose()
}
this.chart = echarts.init(this.$refs.chart)
const xAxisData = this.list.map(item => item.key)
const seriesData = this.list.map(item => item.value)
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '2%',
right: '2%',
bottom: '2%',
top: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: xAxisData,
axisTick: {
alignWithLabel: true,
show: false
},
},
yAxis: {
type: 'value',
minInterval: 1,
splitLine: {
lineStyle: {
color: '#eee'
}
}
},
series: [
{
name: '新增客户数',
type: 'line',
data: seriesData,
smooth: true,
symbol: 'circle',
symbolSize: 6,
itemStyle: {
color: '#409EFF'
},
lineStyle: {
width: 2
},
}
]
}
this.chart.setOption(option)
//
window.addEventListener('resize', this.chart.resize)
}
},
beforeDestroy() {
if (this.chart) {
window.removeEventListener('resize', this.chart.resize)
this.chart.dispose()
this.chart = null
}
}
}
</script>
<style lang="scss" scoped>
.chart-container {
margin-top: 16px;
width: 100%;
height: 200px;
}
</style>

View File

@ -0,0 +1,143 @@
<template>
<div>
<div class="statistics-title">
<div style="flex:5">任务统计</div>
<div style="flex:4">项目统计</div>
</div>
<div class="statistics-wrapper" v-loading="loading">
<!-- 任务统计 -->
<statistics-card
style="flex: 1"
:value="task.total"
label="任务总数"
icon="el-icon-s-order"
start-color="#409EFF"
end-color="#53a8ff"
/>
<statistics-card
style="flex: 1"
:value="task.completed"
label="已完成任务"
icon="el-icon-check"
start-color="#67C23A"
end-color="#85ce61"
/>
<statistics-card
style="flex: 1"
:value="task.overdueUncompleted"
label="逾期未完成"
icon="el-icon-warning"
start-color="#E6A23C"
end-color="#ebb563"
/>
<statistics-card
style="flex: 1"
:value="task.overdueCompleted"
label="逾期完成"
icon="el-icon-time"
start-color="#F56C6C"
end-color="#f78989"
/>
<statistics-card
style="flex: 1"
:value="task.bug"
label="BUG数"
icon="el-icon-error"
start-color="#F56C6C"
end-color="#f78989"
/>
<!-- 项目统计 -->
<statistics-card
style="flex: 1"
:value="project.total"
label="项目总数"
icon="el-icon-folder"
start-color="#409EFF"
end-color="#53a8ff"
/>
<statistics-card
style="flex: 1"
:value="project.devCompleted"
label="正常开发完成"
icon="el-icon-circle-check"
start-color="#67C23A"
end-color="#85ce61"
/>
<statistics-card
style="flex: 1"
:value="project.devOverdueUncompleted"
label="逾期未完成"
icon="el-icon-warning"
start-color="#E6A23C"
end-color="#ebb563"
/>
<statistics-card
style="flex: 1"
:value="project.devOverdueCompleted"
label="逾期完成"
icon="el-icon-warning-outline"
start-color="#F56C6C"
end-color="#f78989"
/>
</div>
</div>
</template>
<script>
import StatisticsCard from '@/components/StatisticsCard'
import { getBrief } from '@/api/dashboard/dashboard'
export default {
name: 'UserStatistics',
components: {
StatisticsCard
},
props: {
userId: {
type: String,
default: null,
}
},
data() {
return {
loading: false,
task: {},
project: {}
}
},
created() {
this.getBrief()
},
methods: {
getBrief() {
this.loading = true
getBrief({ joinUserId: this.userId }).then(res => {
if (res.code === 200 && res.data) {
this.task = res.data.task
this.project = res.data.project
}
}).finally(() => {
this.loading = false
})
}
}
}
</script>
<style lang="scss" scoped>
.statistics-wrapper,
.statistics-title {
display: flex;
flex-wrap: wrap;
gap: 8px;
width: 100%;
}
.statistics-title {
margin-bottom: 8px;
div {
font-size: 14px;
color: #333;
}
}
</style>

View File

@ -0,0 +1,175 @@
<template>
<div class="app-container" v-loading="loading">
<el-row :gutter="12">
<!-- 左侧个人信息 -->
<el-col :xs="24" :sm="24" :md="8" :lg="6" class="mb-12">
<el-card class="user-info-card card-box" shadow="hover">
<div slot="header" class="card-header">
<span>个人信息</span>
</div>
<div class="avatar-box">
<avatar :src="detail.avatar" :size="64" :name="detail.nickName" :char-index="-1"/>
<div class="name">{{ detail.nickName | dv}}</div>
</div>
<div>
<line-field label="手机号" :value="detail.phonenumber" />
<line-field label="所属部门" :value="detail.deptName" />
<line-field label="账号" :value="detail.userName" />
<line-field label="入职时间" :value="detail.employDate" />
<line-field label="最后登录IP" :value="detail.loginIp" />
<line-field label="最后登录时间" :value="detail.loginDate" />
</div>
</el-card>
</el-col>
<!-- 右侧统计信息 -->
<el-col :xs="24" :sm="24" :md="16" :lg="18">
<!-- 统计数据 -->
<user-statistics class="mb-12" v-if="detail.userId" :user-id="detail.userId" />
<!-- 数据曲线图 -->
<el-card class="card-box" shadow="hover">
<el-tabs>
<el-tab-pane label="客户统计">
<performance-chart v-if="detail.userId" :query="{followId: detail.userId}" />
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
<el-card class="card-box" style="margin-top: 12px;" shadow="hover" v-if="detail.userId">
<el-tabs>
<el-tab-pane label="参与项目" v-if="checkPermi(['bst:project:list'])">
<project :query="{joinUserId: detail.userId}" />
</el-tab-pane>
<el-tab-pane label="任务列表" v-if="checkPermi(['bst:task:list'])">
<task :query="{ownerId: detail.userId}" />
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</template>
<script>
import { getUser } from '@/api/system/user'
import PerformanceChart from './components/PerformanceChart'
import LineField from '@/components/LineField'
import Avatar from '@/components/Avatar'
import Project from '@/views/bst/project/index.vue'
import Task from '@/views/bst/task/index.vue'
import { checkPermi } from '@/utils/permission'
import UserStatistics from '@/views/system/user/view/components/UserStatistics'
export default {
name: 'UserView',
components: {
PerformanceChart,
LineField,
Avatar,
Project,
Task,
UserStatistics
},
data() {
return {
detail: {},
loading: false,
}
},
created() {
this.getDetail()
},
methods: {
checkPermi,
/**
* 获取用户详细信息
*/
getDetail() {
this.loading = true
getUser(this.$route.params.id).then(res => {
this.detail = res.data
}).finally(() => {
this.loading = false
})
}
}
}
</script>
<style lang="scss" scoped>
.app-container {
padding: 12px;
.user-info-card {
.card-header {
display: flex;
align-items: center;
}
}
.mb-12 {
margin-bottom: 12px;
}
.login-info {
display: flex;
flex-direction: column;
font-size: 13px;
span {
line-height: 1.4;
}
}
:deep(.el-descriptions-item__label) {
color: #606266;
width: auto;
font-size: 13px;
}
:deep(.el-descriptions-item__content) {
font-size: 13px;
}
:deep(.el-card__body) {
padding: 10px;
}
:deep(.el-descriptions-item) {
padding-bottom: 8px;
}
:deep(.el-tag--small) {
height: 22px;
line-height: 20px;
}
}
.avatar-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 12px;
.name {
font-size: 16px;
margin-top: 12px;
margin-bottom: 12px;
color: #303133;
}
}
//
@media screen and (max-width: 767px) {
.app-container {
padding: 8px;
.mb-12 {
margin-bottom: 8px;
}
:deep(.el-card__body) {
padding: 8px;
}
}
}
</style>