master
lion 7 months ago
parent 48851f117e
commit 710b7b2fbf

@ -2,10 +2,13 @@
ENV='development'
# base api
#VUE_APP_BASE_API=https://suzhoukeji-test.ali251.langye.net
#VUE_APP_UPLOAD_API=https://suzhoukeji-test.ali251.langye.net/api/admin/upload-file
VUE_APP_PRO_API = https://suzhoukeji-test.ali251.langye.net
VUE_APP_PRO_API = https://www.sstbc.com
VUE_APP_BASE_API = https://suzhoukeji-test.ali251.langye.net
VUE_APP_BASE_API = https://wx.sstbc.com
VUE_APP_UPLOAD_API = https://wx.sstbc.com/api/admin/upload-file
VUE_APP_UPLOAD_API = https://suzhoukeji-test.ali251.langye.net/api/admin/upload-file
#VUE_APP_PRO_API = https://www.sstbc.com
#VUE_APP_BASE_API = https://wx.sstbc.com
#VUE_APP_UPLOAD_API = https://wx.sstbc.com/api/admin/upload-file

@ -0,0 +1,56 @@
import request from "@/utils/request";
function customParamsSerializer(params) {
let result = '';
for (let key in params) {
if (params.hasOwnProperty(key)) {
if (Array.isArray(params[key])) {
params[key].forEach((item, index) => {
if (item.key) {
result += `${key}[${index}][key]=${item.key}&${key}[${index}][op]=${item.op}&${key}[${index}][value]=${item.value}&`;
} else {
result += `${key}[${index}]=${item}&`
}
});
} else {
result += `${key}=${params[key]}&`;
}
}
}
return result.slice(0, -1);
}
export function index(params, isLoading = false) {
return request({
method: "get",
url: "/api/admin/article/index",
params,
paramsSerializer: customParamsSerializer,
isLoading
})
}
export function show(params, isLoading = true) {
return request({
method: "get",
url: "/api/admin/article/show",
params,
isLoading
})
}
export function save(data) {
return request({
method: "post",
url: "/api/admin/article/save",
data
})
}
export function destroy(params) {
return request({
method: "get",
url: "/api/admin/article/destroy",
params
})
}

@ -0,0 +1,63 @@
import request from "@/utils/request";
function customParamsSerializer(params) {
let result = '';
for (let key in params) {
if (params.hasOwnProperty(key)) {
if (Array.isArray(params[key])) {
params[key].forEach((item, index) => {
if (item.key) {
result += `${key}[${index}][key]=${item.key}&${key}[${index}][op]=${item.op}&${key}[${index}][value]=${item.value}&`;
} else {
result += `${key}[${index}]=${item}&`
}
});
} else {
result += `${key}=${params[key]}&`;
}
}
}
return result.slice(0, -1);
}
export function index(params, isLoading = false) {
return request({
method: "get",
url: "/api/admin/statistics-config/index",
params,
paramsSerializer: customParamsSerializer,
isLoading
})
}
export function show(params, isLoading = true) {
return request({
method: "get",
url: "/api/admin/statistics-config/show",
params,
isLoading
})
}
export function save(data) {
return request({
method: "post",
url: "/api/admin/statistics-config/save",
data
})
}
export function destroy(params) {
return request({
method: "get",
url: "/api/admin/statistics-config/destroy",
params
})
}
export function calculate(params) {
return request({
method: "get",
url: "/api/admin/statistics-config/calculate",
params
})
}

@ -7,3 +7,11 @@ export function home(params){
params
})
}
export function home2(params){
return request({
method:'get',
url:'/api/admin/other/home-v2',
params
})
}

@ -9,6 +9,14 @@ export function home(params,isLoading) {
})
}
export function courseChartExport(params,isLoading) {
return request({
url: '/api/admin/other/courses-home-export',
method: 'get',
params,
isLoading: true
})
}
export function courseChart(params,isLoading) {
return request({
url: '/api/admin/other/courses-home',

@ -30,6 +30,15 @@ export function index(params,isLoading = false) {
})
}
export function companyConfig(params, isLoading = true) {
return request({
method: "get",
url: "/api/admin/company/config",
params,
isLoading
})
}
export function show(params, isLoading = true) {
return request({
method: "get",

@ -0,0 +1,56 @@
import request from "@/utils/request";
function customParamsSerializer(params) {
let result = '';
for (let key in params) {
if (params.hasOwnProperty(key)) {
if (Array.isArray(params[key])) {
params[key].forEach((item, index) => {
if (item.key) {
result += `${key}[${index}][key]=${item.key}&${key}[${index}][op]=${item.op}&${key}[${index}][value]=${item.value}&`;
} else {
result += `${key}[${index}]=${item}&`
}
});
} else {
result += `${key}=${params[key]}&`;
}
}
}
return result.slice(0, -1);
}
export function index(params, isLoading = false) {
return request({
method: "get",
url: "/api/admin/time-event/index",
params,
paramsSerializer: customParamsSerializer,
isLoading
})
}
export function show(params, isLoading = true) {
return request({
method: "get",
url: "/api/admin/time-event/show",
params,
isLoading
})
}
export function save(data) {
return request({
method: "post",
url: "/api/admin/time-event/save",
data
})
}
export function destroy(params) {
return request({
method: "get",
url: "/api/admin/time-event/destroy",
params
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

@ -1,6 +1,9 @@
import {
getparameter
} from "@/api/system/dictionary.js"
import {
companyConfig
} from "@/api/student/schoolmateCompany.js"
export default {
data() {
return {
@ -693,11 +696,18 @@ export default {
computed: {},
created() {
this.getAllPara()
this.getCompanyConfig()
},
mounted() {},
methods: {
// 获取企业资质下拉选择
getCompanyConfig() {
companyConfig().then(res => {
this.formSelect.company_type = res.companiesTags
})
},
getAllPara() {
let number = ['company_type', 'company_area','company_tag','company_scale', 'company_industry', 'company_industry_detail', 'type', 'education',
let number = ['company_area','company_tag','company_scale', 'company_industry', 'company_industry_detail', 'type', 'education',
'sign_from','from_tag'
]
getparameter({

@ -48,7 +48,7 @@ export async function download(url, method = "get", info, filename) {
url,
method,
responseType: "blob",
timeout: 10000,
timeout: 30000,
headers: {
Accept: "application/json",
"Content-Type": "application/json; charset=utf-8",

@ -14,7 +14,7 @@ let loading ;
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000, // request timeout
timeout: 30000, // request timeout
isLoading:true
})

@ -0,0 +1,175 @@
<template>
<div>
<xy-dialog ref="dialog" :width="70" :is-show.sync="isShow" :type="'form'"
:title="type === 'add' ? '新增文章' : '编辑文章'" :form="form" :rules="rules" @submit="submit">
<template v-slot:title>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
<span style="color: red;font-weight: bold;padding-right: 4px;">*</span>标题
</div>
<div class="xy-table-item-content">
<el-input v-model="form.title" placeholder="请输入标题" clearable style="width: 100%;" />
</div>
</div>
</template>
<template v-slot:type>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
<span style="color: red;font-weight: bold;padding-right: 4px;">*</span>类型
</div>
<div class="xy-table-item-content">
<el-select v-model="form.type" placeholder="请选择类型" clearable style="width: 100%;">
<el-option v-for="item in type_options" :key="item.id" :label="item.value" :value="item.id" />
</el-select>
</div>
</div>
</template>
<template v-slot:sort>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
<span style="color: red;font-weight: bold;padding-right: 4px;"></span>排序
</div>
<div class="xy-table-item-content">
<el-input-number v-model="form.sort" :min="0" placeholder="请输入排序默认为0" style="width: 100%;" />
</div>
</div>
</template>
<template v-slot:content>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
<span style="color: red;font-weight: bold;padding-right: 4px;">*</span>内容
</div>
<div class="xy-table-item-content" style="flex-grow: 1;">
<my-tinymce v-if="showTinymce" @input="saveContent" :value="form.content" />
</div>
</div>
</template>
</xy-dialog>
</div>
</template>
<script>
import { save, show } from '@/api/article/index.js'
export default {
components: {
},
data() {
return {
isShow: false,
type: 'add',
id: '',
showTinymce: false,
type_options: [{
id: 1,
value: '校友动态'
}, {
id: 2,
value: '业界动态'
}],
form: {
title: '',
type: '',
sort: 0,
content: ''
},
rules: {
title: [{
required: true,
message: '请输入标题'
}],
type: [{
required: true,
message: '请选择类型'
}],
content: [{
required: true,
message: '请输入内容'
}]
}
}
},
created() {},
methods: {
saveContent(e) {
this.form.content = e
},
submit() {
if (this.id) {
this.form.id = this.id
}
if (this.type === 'add') {
this.form.id = ''
}
//
if (!this.form.content || this.form.content.trim() === '') {
this.$message.warning('请输入内容')
return
}
save(this.form).then(res => {
this.$message({
type: 'success',
message: this.type === 'add' ? '新增成功' : '编辑成功'
})
this.isShow = false
this.$emit('refresh')
}).catch(error => {
console.log(error)
this.$message.error(this.type === 'add' ? '新增失败' : '编辑失败')
})
},
getDetail() {
show({
id: this.id
}).then(res => {
this.form = {
title: res.title || '',
type: res.type || '',
sort: res.sort !== undefined ? res.sort : 0,
content: res.content || ''
}
this.showTinymce = true
}).catch(error => {
console.log(error)
this.$message.error('获取详情失败')
})
}
},
watch: {
isShow(newVal) {
if (newVal) {
if (this.type === 'editor') {
this.getDetail()
} else {
this.showTinymce = true
this.form = {
title: '',
type: '',
sort: 0,
content: ''
}
}
} else {
this.id = ''
this.type = 'add'
this.showTinymce = false
this.form = {
title: '',
type: '',
sort: 0,
content: ''
}
this.$refs['dialog'].reset()
}
}
}
}
</script>
<style scoped lang="scss">
::v-deep .content {
flex-basis: 100%;
}
</style>

@ -0,0 +1,177 @@
<template>
<div>
<div>
<div ref="lxHeader">
<lx-header icon="md-apps" :text="$route.meta.title" style="margin-bottom: 10px; border: 0px; margin-top: 15px">
<div slot="content">
<div class="searchwrap" style="display: flex;align-items: center;">
<div>
<el-input v-model="select.title" placeholder="请输入标题" clearable />
</div>
<div>
<el-select v-model="select.type" placeholder="请选择类型" clearable>
<el-option v-for="item in type_options" :key="item.id" :label="item.value" :value="item.id" />
</el-select>
</div>
<div>
<el-button type="primary" size="small" @click="select.page=1,getList()"></el-button>
</div>
<div>
<el-button type="primary" size="small" @click="editArticle('add')"></el-button>
</div>
</div>
</div>
</lx-header>
</div>
</div>
<div>
<xy-table :list="list" :total="total" :table-item="table_item" @pageIndexChange="pageIndexChange" @pageSizeChange="pageSizeChange">
<template v-slot:btns>
<el-table-column align="center" label="操作" width="180" header-align="center">
<template slot-scope="scope">
<el-button type="primary" size="small" @click="editArticle('editor',scope.row.id)"></el-button>
<el-popconfirm title="确定删除吗?" style="margin:0 10px" @confirm="deleteList(scope.row.id)">
<el-button slot="reference" type="danger" size="small">删除</el-button>
</el-popconfirm>
</template>
</el-table-column>
</template>
</xy-table>
</div>
<add-article ref="addArticle" @refresh="getList" />
</div>
</template>
<script>
import addArticle from './components/addArticle.vue'
import { index, destroy } from '@/api/article/index.js'
export default {
components: {
addArticle
},
data() {
return {
select: {
title: '',
type: '',
page: 1,
page_size: 10
},
type_options: [{
id: 1,
value: '校友动态'
}, {
id: 2,
value: '业界动态'
}],
list: [],
total: 0,
table_item: [{
prop: 'title',
label: '标题',
align: 'left',
minWidth: 200
}, {
prop: 'type',
label: '类型',
align: 'center',
width: 120,
customFn: (row) => {
const item = this.type_options.find(item => item.id === row.type)
return item ? item.value : '-'
}
}, {
prop: 'sort',
label: '排序',
align: 'center',
width: 100
}, {
prop: 'created_at',
label: '创建时间',
align: 'center',
width: 180
}]
}
},
created() {
this.getList()
},
methods: {
pageIndexChange(e) {
this.select.page = e
this.getList()
},
pageSizeChange(e) {
this.select.page_size = e
this.select.page = 1
this.getList()
},
editArticle(type, id) {
if (type === 'editor') {
this.$refs.addArticle.id = id
}
this.$refs.addArticle.type = type
this.$refs.addArticle.isShow = true
},
async getList() {
const params = {
page_size: this.select.page_size,
page: this.select.page,
sort_name: 'sort',
sort_type: 'ASC'
}
if (this.select.title) {
params.filter = [{
key: 'title',
op: 'like',
value: this.select.title
}]
}
if (this.select.type) {
if (!params.filter) {
params.filter = []
}
params.filter.push({
key: 'type',
op: 'eq',
value: this.select.type
})
}
const res = await index(params, false)
this.list = res.data
this.total = res.total
},
deleteList(id) {
destroy({
id: id
}).then(response => {
this.$message.success('删除成功')
this.getList()
}).catch(error => {
console.log(error)
this.$message.error('删除失败')
})
}
}
}
</script>
<style lang="scss" scoped>
.searchwrap {
display: flex;
align-items: center;
&>div {
display: flex;
align-items: center;
margin-right: 10px;
span {
min-width: 70px;
}
}
}
</style>

File diff suppressed because it is too large Load Diff

@ -99,25 +99,39 @@ import { getToken } from '@/utils/auth';
},
methods: {
async exportCalendar() {
console.log('导出日历事件')
const res = await index({
month: this.selectMonth,
is_export: 1,
'export_fields[is_publish_text]':'是否对外展示',
'export_fields[type_text]': '日程类型',
'export_fields[course.name]': '课程名称',
'export_fields[introduce]': '具体说明',
'export_fields[title]': '标题',
'export_fields[url]': '资讯链接',
'export_fields[start_time]': '开始时间',
'export_fields[end_time]': '截止时间',
'export_fields[address]': '地址',
'export_fields[color]': '主题颜色',
'export_fields[content]': '内容'
})
var url = process.env.VUE_APP_BASE_API + '/api/admin/calendars/index?month=' + this.selectMonth + '&is_export=1&export_fields[is_publish_text]=是否对外展示&export_fields[type_text]=日程类型&export_fields[course.name]=课程名称&export_fields[introduce]=具体说明&export_fields[title]=标题&export_fields[url]=资讯链接&export_fields[start_time]=开始时间&export_fields[end_time]=截止时间&export_fields[address]=地址&export_fields[color]=主题颜色&export_fields[content]=内容&token=' + getToken()
try {
await this.$confirm('请选择导出范围', '导出日历事件', {
confirmButtonText: '导出本月',
cancelButtonText: '导出全部',
distinguishCancelAndClose: true,
type: 'info'
})
this.triggerExport(this.selectMonth)
} catch (action) {
if (action === 'cancel') {
this.triggerExport('')
}
//
}
},
triggerExport(month) {
const monthQuery = month ? `month=${month}` : 'month='
const url =
process.env.VUE_APP_BASE_API +
`/api/admin/calendars/index?${monthQuery}` +
'&is_export=1&export_fields[is_publish_text]=是否对外展示' +
'&export_fields[type_text]=日程类型' +
'&export_fields[course.name]=课程名称' +
'&export_fields[introduce]=具体说明' +
'&export_fields[title]=标题' +
'&export_fields[url]=资讯链接' +
'&export_fields[start_time]=开始时间' +
'&export_fields[end_time]=截止时间' +
'&export_fields[address]=地址' +
'&export_fields[color]=主题颜色' +
'&export_fields[content]=内容' +
'&token=' + getToken()
window.open(url, '_blank')
console.log(res)
},
onDateCellClick() {
// el-calendar

File diff suppressed because it is too large Load Diff

@ -39,7 +39,7 @@
<template v-slot:mobile>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
<span style="color: red;font-weight: bold;padding-right: 4px;">*</span>联系方式
<span style="color: red;font-weight: bold;padding-right: 4px;"></span>联系方式
</div>
<div class="xy-table-item-content">
<el-input v-model="form.mobile" placeholder="请输入联系方式" clearable style="width: 100%;"></el-input>
@ -102,10 +102,10 @@
required: true,
message: '请选择性别'
}],
mobile: [{
required: true,
message: '请输入联系方式'
}]
// mobile: [{
// required: true,
// message: ''
// }]
}
}
},

@ -54,8 +54,8 @@
<el-select style="width:100%" v-model="companyType"
@change="(e)=>{changeSelectType(e,'company_type')}" multiple collapse-tags
placeholder="请选择企业资质" clearable>
<el-option v-for="item in formSelect.company_type" :key="item.id" :label="item.value"
:value="item.value">
<el-option v-for="(item,index) in formSelect.company_type" :key="index" :label="item"
:value="item">
</el-option>
</el-select>
</div>
@ -256,7 +256,7 @@
width: 50,
fixed: 'left'
},{
prop: 'course_signs',
label: '历史报名信息',
align: 'center',

@ -236,6 +236,19 @@
</div>
</div>
</template>
<template v-slot:is_chart v-if="active===0">
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
<span style="color: red;font-weight: bold;padding-right: 4px;"></span>是否统计
</div>
<div class="xy-table-item-content">
<el-radio-group style="width:100%" v-model="form.is_chart">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</div>
</div>
</template>
<template v-slot:qun_image_id v-if="active===0 && form.is_fee===0">
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold;">
@ -413,6 +426,7 @@
show_txl: '',
show_mobile: '',
auto_schoolmate: '',
is_chart:'',
qun_image_id: '',
image_id: '',
publicize_ids: [],
@ -449,6 +463,10 @@
required: true,
message: '请选择是否自动成为校友'
}],
is_chart: [{
required: true,
message: '请选择是否统计'
}],
sign_start_date: [{
required: true,
message: '请选择报名开始日期'
@ -545,7 +563,6 @@
this.form.show_txl = selectedCourseType.show_txl;
this.form.show_mobile = selectedCourseType.show_mobile;
this.form.auto_schoolmate = selectedCourseType.auto_schoolmate;
console.log('课程体系选择变更,自动填充字段:', {
is_fee: this.form.is_fee,
is_arrange: this.form.is_arrange,
@ -757,6 +774,7 @@
this.form.show_txl = res.show_txl === 0 ? 0 : 1
this.form.show_mobile = res.show_mobile ? res.show_mobile : 0
this.form.auto_schoolmate = res.auto_schoolmate ? res.auto_schoolmate : 0
this.form.is_chart = res.is_chart ? res.is_chart : 0
this.showTinymce = true
})
},
@ -853,6 +871,7 @@
show_txl: '',
show_mobile: '',
auto_schoolmate: '',
is_chart:'',
qun_image_id: '',
image_id: '',
publicize_ids: [],

@ -50,9 +50,9 @@
<el-select style="width:100%" v-model="companyType"
@change="(e)=>{changeSelectType(e,'company_type')}" multiple collapse-tags
placeholder="请选择企业资质" clearable>
<el-option v-for="item in formSelect.company_type" :key="item.id" :label="item.value"
:value="item.value">
</el-option>
<el-option v-for="(item,index) in formSelect.company_type" :key="index" :label="item"
:value="item">
</el-option>
</el-select>
</div>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
<template>
<div class="statistics-container">
<div class="dashboard-container">
<div class="dashboard-container" v-loading="chartLoading">
<!-- 筛选条件区域 -->
<div class="filter-section">
<div class="row">
@ -69,6 +69,7 @@
<!-- 学员统计卡片 -->
<div class="stats-container">
<div class="stats-card" :class="stat.cardClass" v-for="(stat, index) in studentStats" :key="index">
<i class="el-icon-download stats-download" @click.stop="exportStat(stat)"></i>
<div class="stats-icon">
<i :class="stat.icon"></i>
</div>
@ -104,6 +105,12 @@
<el-table-column prop="courseName" label="开课" min-width="200"></el-table-column>
<el-table-column prop="coursePeople" label="课程培养人数" width="150" align="center"></el-table-column>
</el-table>
<div class="table-summary">
<span>累计</span>
<span>培养人数未去重{{ courseDetailSummary.totalPeople }}</span>
<span>培养人数课程体系内已去重{{ courseDetailSummary.uniquePeople }}</span>
<span>课程培养人数{{ courseDetailSummary.coursePeople }}</span>
</div>
</div>
</el-col>
</el-row>
@ -115,9 +122,14 @@
<h5 class="table-title">
<i class="el-icon-location"></i> 区域明细统计
</h5>
<el-button type="success" @click="exportRegionData" class="btn-export">
<i class="el-icon-download"></i> 导出数据
</el-button>
<div>
<el-button type="primary" @click="exportRegionDetailData" class="btn-export">
<i class="el-icon-download"></i> 导出区域明细数据
</el-button>
<el-button type="success" @click="exportRegionData" class="btn-export">
<i class="el-icon-download"></i> 导出数据
</el-button>
</div>
</div>
<div class="detail-table">
<el-table :data="regionData" style="width: 100%" :header-cell-style="headerCellStyle">
@ -125,6 +137,11 @@
<el-table-column prop="totalPeople" label="培养人数(未去重)" align="center"></el-table-column>
<el-table-column prop="uniquePeople" label="培养人数(已去重)" align="center"></el-table-column>
</el-table>
<div class="table-summary">
<span>累计</span>
<span>培养人数未去重{{ regionSummary.totalPeople }}</span>
<span>培养人数已去重{{ regionSummary.uniquePeople }}</span>
</div>
</div>
</el-col>
</el-row>
@ -134,7 +151,7 @@
<script>
import { index as courseTypeIndex } from '@/api/course/courseType.js'
import { courseChart } from '@/api/homeChart.js'
import { courseChart,courseChartExport } from '@/api/homeChart.js'
import * as XLSX from "xlsx";
import { download } from "@/utils/downloadRequest";
import formMixin from "@/mixin/formMixin.js";
@ -145,6 +162,7 @@ export default {
components: {},
data() {
return {
chartLoading: false,
filterForm: {
timeRange: 'all',
startDate: '',
@ -156,39 +174,80 @@ export default {
courseTypeList: [], //
studentStats: [
{
key: 'course_signs_invested',
icon: 'el-icon-user-solid',
value: '0',
// label: '',
label: '投后企业数',
label: '被投企业数',
cardClass: 'student-card-1'
},
{
key: 'course_signs_pass',
icon: 'el-icon-s-check',
value: '0',
label: '培养人数(未去重)',
cardClass: 'student-card-2'
},
{
key: 'course_signs_pass_unique',
icon: 'el-icon-s-custom',
value: '0',
label: '培养人数(已去重)',
cardClass: 'student-card-3'
},
{
key: 'course_total',
icon: 'el-icon-date',
value: '0',
label: '开课场次',
cardClass: 'student-card-4'
},
{
key: 'course_day_total',
icon: 'el-icon-c-scale-to-original',
value: '0',
label: '开课天数',
cardClass: 'student-card-5'
},
{
key: 'company_market_total',
icon: 'el-icon-office-building',
value: '0',
label: '上市公司数',
cardClass: 'student-card-1'
},
{
key: 'company_market_year_total',
icon: 'el-icon-s-finance',
value: '0',
label: '今年上市公司数',
cardClass: 'student-card-2'
},
{
key: 'company_market_after_enrollment_total',
icon: 'el-icon-s-marketing',
value: '0',
label: '入学后上市公司数',
cardClass: 'student-card-3'
},
{
key: 'ganbu_total',
icon: 'el-icon-s-opportunity',
value: '0',
label: '跟班学员数',
cardClass: 'student-card-4'
}
],
courseDetailData: [],
regionData: []
courseDetailSummary: {
totalPeople: 0,
uniquePeople: 0,
coursePeople: 0
},
regionData: [],
regionSummary: {
totalPeople: 0,
uniquePeople: 0
}
}
},
mounted() {
@ -199,28 +258,15 @@ export default {
},
methods: {
async getCourseChart() {
this.chartLoading = true
try {
// ID
const courseTypeIds = this.filterForm.selectedCourses.length > 0
? this.filterForm.selectedCourses.join(',')
: ''
//
const startDate = this.formatDate(this.filterForm.startDate)
const endDate = this.formatDate(this.filterForm.endDate)
const res = await courseChart({
timeRange: this.filterForm.timeRange,
start_date: startDate,
end_date: endDate,
course_type_id: courseTypeIds
})
const params = this.buildChartParams()
const res = await courseChart(params)
console.log('课程图表数据:', res)
console.log('日期参数:', { startDate, endDate })
console.log('查询参数:', params)
if (res) {
//
this.updateStatisticsData(res)
} else {
this.$message.error('获取课程图表数据失败')
@ -228,6 +274,8 @@ export default {
} catch (error) {
console.error('获取课程图表数据失败:', error)
this.$message.error('获取课程图表数据失败')
} finally {
this.chartLoading = false
}
},
@ -304,17 +352,42 @@ export default {
//
filterStudentData() {
const params = {
console.log('学员数据筛选:', {
timeRange: this.filterForm.timeRange,
startDate: this.filterForm.startDate,
endDate: this.filterForm.endDate,
selectedCourses: this.filterForm.selectedCourses
}
console.log('学员数据筛选:', params)
//
})
this.getCourseChart()
},
buildChartParams(extra = {}) {
const courseTypeIds = this.filterForm.selectedCourses.length > 0
? this.filterForm.selectedCourses.join(',')
: ''
return {
timeRange: this.filterForm.timeRange,
start_date: this.formatDate(this.filterForm.startDate),
end_date: this.formatDate(this.filterForm.endDate),
course_type_id: courseTypeIds,
...extra
}
},
exportStat(stat) {
if (!stat.key) return
if(!stat.value || stat.value == 0){
this.$message.warning('暂无数据可导出')
return
}
const params = this.buildChartParams({
export_type: stat.key
})
download(
'/api/admin/other/courses-home-export',
'get',
params,
`${stat.label}.xlsx`
)
},
//
exportCourseData() {
@ -375,63 +448,73 @@ export default {
//
exportCoursePersonData() {
try {
// ID
const courseTypeIds = this.filterForm.selectedCourses.length > 0
? this.filterForm.selectedCourses.join(',')
: ''
const params = this.buildChartParams({
export_type: 'course_signs_pass'
})
download(
'/api/admin/other/courses-home-export',
'get',
params,
'人员明细数据.xlsx'
)
// try {
// // ID
// const courseTypeIds = this.filterForm.selectedCourses.length > 0
// ? this.filterForm.selectedCourses.join(',')
// : ''
//
const startDate = this.formatDate(this.filterForm.startDate)
const endDate = this.formatDate(this.filterForm.endDate)
// //
// const startDate = this.formatDate(this.filterForm.startDate)
// const endDate = this.formatDate(this.filterForm.endDate)
//
const exportFields = {
'course.type_detail.name': '课程体系',
'course.name': '课程名称',
'status_text': '审核状态',
'created_at': '报名时间'
// 'user.name': '',
// 'user.sex': '',
// 'user.no': '',
// 'user.idcard': '',
// 'user.mobile': '',
// 'user.birthday': '',
// 'user.email': '',
// 'user.company_name': '',
// 'user.company_position': '',
// //
// const exportFields = {
// 'course.type_detail.name': '',
// 'course.name': '',
// 'status_text': '',
// 'created_at': ''
// // 'user.name': '',
// // 'user.sex': '',
// // 'user.no': '',
// // 'user.idcard': '',
// // 'user.mobile': '',
// // 'user.birthday': '',
// // 'user.email': '',
// // 'user.company_name': '',
// // 'user.company_position': '',
}
this.selectFormList.map(item => {
if (item.prop === 'index') {
// }
// this.selectFormList.map(item => {
// if (item.prop === 'index') {
} else {
exportFields['user.'+item.field] = item.name
}
})
// } else {
// exportFields['user.'+item.field] = item.name
// }
// })
// API
download(
'/api/admin/course-signs/index',
'get',
{
export_fields: exportFields,
is_export: 1,
page: 1,
page_size: 9999,
start_date: startDate,
end_date: endDate,
course_type_id: courseTypeIds,
clear: 1
},
this.generateFileName('人员明细')
)
// // API
// download(
// '/api/admin/course-signs/index',
// 'get',
// {
// export_fields: exportFields,
// is_export: 1,
// page: 1,
// page_size: 9999,
// start_date: startDate,
// end_date: endDate,
// course_type_id: courseTypeIds,
// clear: 1,
// status: 1
// },
// this.generateFileName('')
// )
this.$message.success('人员明细导出任务已开始,请稍后查看下载文件')
} catch (error) {
console.error('导出失败:', error)
this.$message.error('导出失败,请重试')
}
// this.$message.success('')
// } catch (error) {
// console.error(':', error)
// this.$message.error('')
// }
},
//
exportCourseOpenData() {
@ -555,6 +638,18 @@ export default {
this.$message.error('导出失败,请重试')
}
},
//
exportRegionDetailData() {
const params = this.buildChartParams({
export_type: 'areas'
})
download(
'/api/admin/other/courses-home-export',
'get',
params,
'区域明细数据.xlsx'
)
},
//
generateFileName(type) {
@ -589,20 +684,14 @@ export default {
updateStatisticsData(data) {
// API
if (data && data.list) {
//
this.studentStats[0].value = data.list.course_signs_invested || '0'
//
this.studentStats[1].value = data.list.course_signs_pass || '0'
//
this.studentStats[2].value = data.list.course_signs_pass_unique || '0'
//
this.studentStats[3].value = data.list.course_total || '0'
//
this.studentStats[4].value = data.list.course_day_total || '0'
const statsData = data.list
this.studentStats.forEach(stat => {
if (stat.key && statsData.hasOwnProperty(stat.key)) {
stat.value = statsData[stat.key] ?? '0'
} else {
stat.value = '0'
}
})
}
//
@ -630,8 +719,9 @@ export default {
})
//
const groupedValues = Object.values(groupedData)
this.courseDetailData = []
Object.values(groupedData).forEach(group => {
groupedValues.forEach(group => {
//
group.courses.forEach((course, index) => {
this.courseDetailData.push({
@ -644,6 +734,17 @@ export default {
})
})
})
//
const summary = groupedValues.reduce((acc, group) => {
acc.totalPeople += Number(group.totalPeople) || 0
acc.uniquePeople += Number(group.uniquePeople) || 0
acc.coursePeople += group.courses.reduce((sum, course) => sum + (Number(course.coursePeople) || 0), 0)
return acc
}, { totalPeople: 0, uniquePeople: 0, coursePeople: 0 })
this.courseDetailSummary = summary
} else {
this.courseDetailSummary = { totalPeople: 0, uniquePeople: 0, coursePeople: 0 }
}
//
@ -653,6 +754,12 @@ export default {
totalPeople: item.course_signs_pass || 0,
uniquePeople: item.course_signs_pass_unique || 0
}))
this.regionSummary = this.regionData.reduce((acc, row) => ({
totalPeople: acc.totalPeople + (Number(row.totalPeople) || 0),
uniquePeople: acc.uniquePeople + (Number(row.uniquePeople) || 0)
}), { totalPeople: 0, uniquePeople: 0 })
} else {
this.regionSummary = { totalPeople: 0, uniquePeople: 0 }
}
console.log('统计数据已更新:', this.studentStats)
@ -863,9 +970,8 @@ export default {
flex-direction: column;
justify-content: center;
align-items: center;
flex: 1;
min-width: 0;
max-width: calc(20% - 16px);
flex: 0 0 calc(20% - 16px);
min-width: calc(20% - 16px);
&::before {
content: '';
@ -900,6 +1006,21 @@ export default {
}
}
.stats-download {
position: absolute;
top: 12px;
right: 12px;
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.8);
cursor: pointer;
transition: color 0.2s ease;
z-index: 3;
}
.stats-download:hover {
color: #fff;
}
.stats-icon {
font-size: 2.8rem;
opacity: 0.9;
@ -1020,6 +1141,19 @@ export default {
background: linear-gradient(135deg, #f8f9fa 0%, #e3f2fd 100%) !important;
}
.table-summary {
display: flex;
flex-wrap: wrap;
gap: 24px;
margin-top: 15px;
font-weight: 600;
color: #0f4c75;
padding: 12px 18px;
border-radius: 12px;
background: rgba(15, 76, 117, 0.05);
border: 1px solid rgba(15, 76, 117, 0.12);
}
.btn-filter {
background: linear-gradient(135deg, #00a8ff 0%, #0097e6 100%);
border: none;

@ -100,7 +100,7 @@
</el-option>
</el-select>
</div>
<div style="width:250px">
<el-date-picker v-model="companyDates" format="yyyy-MM-dd" value-format="yyyy-MM-dd"
style="width:100%" @change="changeCompanyDates" type="daterange" range-separator="至"
@ -122,9 +122,9 @@
<el-select style="width:100%" v-model="companyType"
@change="(e)=>{changeSelectType(e,'company_type')}" multiple collapse-tags
placeholder="请选择企业资质" clearable>
<el-option v-for="item in formSelect.company_type" :key="item.id" :label="item.value"
:value="item.value">
</el-option>
<el-option v-for="(item,index) in formSelect.company_type" :key="index" :label="item"
:value="item">
</el-option>
</el-select>
</div>
@ -583,7 +583,7 @@
this.seleSchoolmates.map(item => {
ids.push(item.id)
})
updateSchoolmate({
is_schoolmate: '', //
ids: ids.join(','),

@ -32,38 +32,24 @@
</div>
</template>
<template v-slot:company_industry>
<template v-slot:company_tag>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
<span style="color: red;font-weight: bold;padding-right: 4px;">*</span>企业资质
</div>
<div class="xy-table-item-content">
<el-select v-model="form.company_industry" placeholder="请选择企业资质" clearable style="width: 100%;">
<el-option v-for="(item,index) in formSelect.company_type" :key="index" :label="item.value" :value="item.value">
</el-option>
<el-select multiple v-model="form.company_tag" placeholder="请选择企业资质" clearable style="width: 100%;">
<el-option v-for="(item,index) in formSelect.company_type" :key="index" :label="item"
:value="item">
</el-option>
</el-select>
</div>
</div>
</template>
<template v-slot:company_tag>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
<span style="color: red;font-weight: bold;padding-right: 4px;"></span>集团标签
</div>
<div class="xy-table-item-content">
<el-input v-model="form.company_tag" placeholder="请输入集团标签" clearable style="width: 100%;"></el-input>
<!-- <el-select v-model="form.company_tag" placeholder="请选择集团标签" clearable style="width: 100%;">
<el-option v-for="(item,index) in formSelect.company_tag" :key="index" :label="item.value" :value="item.value">
</el-option>
</el-select> -->
</div>
</div>
</template>
<template v-slot:is_yh_invested>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
<span style="color: red;font-weight: bold;padding-right: 4px;"></span>是否被投企业
<span style="color: red;font-weight: bold;padding-right: 4px;"></span>集团标签
</div>
<div class="xy-table-item-content">
<el-select v-model="form.is_yh_invested" placeholder="请选择是否被投企业" clearable style="width: 100%;">
@ -73,7 +59,7 @@
</div>
</div>
</template>
<template v-slot:company_scale>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
@ -121,7 +107,7 @@
</div>
</div>
</template>
<template v-slot:management_platform>
<!-- <template v-slot:management_platform>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
<span style="color: red;font-weight: bold;padding-right: 4px;"></span>管理平台
@ -130,8 +116,8 @@
<el-input v-model="form.management_platform" placeholder="请输入管理平台" type="text" clearable style="width: 100%;"></el-input>
</div>
</div>
</template>
<template v-slot:project_manager>
</template> -->
<!-- <template v-slot:project_manager>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
<span style="color: red;font-weight: bold;padding-right: 4px;"></span>项目经理
@ -140,7 +126,7 @@
<el-input v-model="form.project_manager" placeholder="请输入项目经理" type="text" clearable style="width: 100%;"></el-input>
</div>
</div>
</template>
</template> -->
<template v-slot:company_fund>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
@ -208,15 +194,14 @@
id: '',
form: {
company_name: '',
company_industry: '',
company_tag: '',
company_tag: [],
is_yh_invested: '',
company_scale: '',
company_date: '',
company_legal_representative: '',
company_shareholder:'',
management_platform:'',
project_manager:'',
// management_platform:'',
// project_manager:'',
company_fund:'',
market_value:'',
valuation:'',
@ -230,14 +215,10 @@
required: true,
message: '请输入企业名称'
}],
company_industry: [{
company_tag: [{
required: true,
message: '请选择企业资质'
}],
// company_tag: [{
// required: true,
// message: ''
// }]
}
}
},
@ -251,10 +232,13 @@
if (this.type == 'add') {
this.form.id = ''
}
console.log("this.form",this.form)
save({
...this.form
}).then(res => {
const payload = {
...this.form,
company_tag: Array.isArray(this.form.company_tag)
? this.form.company_tag.join(',')
: (this.form.company_tag || '')
}
save(payload).then(res => {
this.$message({
type: 'success',
message: this.type === 'add' ? '新增成功' : '编辑成功'
@ -270,6 +254,9 @@
}).then(res => {
this.form = this.base.requestToForm(res, this.form)
this.form.is_yh_invested = this.form.is_yh_invested ? 1 : 0
this.form.company_tag = res.company_tag
? res.company_tag.split(',').filter(item => item !== '')
: []
})
},
async searchCompany(query) {
@ -298,7 +285,7 @@
const selectedCompany = this.companySearchResults.find(item => item.enterpriseName === value)
if (selectedCompany) {
//
// this.form.company_industry = selectedCompany.attribute
// this.form.company_tag = selectedCompany.attribute
}
}
},
@ -327,15 +314,14 @@
this.type = "add"
this.form = {
company_name: '',
company_industry: '',
company_tag: '',
company_tag: [],
is_yh_invested: '',
company_scale: '',
company_date: '',
company_legal_representative: '',
company_shareholder:'',
management_platform:'',
project_manager:'',
// management_platform:'',
// project_manager:'',
market_value:'',
overseas_experience:''
}

@ -169,8 +169,9 @@
</div>
<div class="xy-table-item-content">
<el-select v-model="companyTypeList" multiple placeholder="请选择企业资质" clearable style="width: 100%;">
<el-option v-for="(item,index) in formSelect.company_type" :key="index" :label="item.value" :value="item.value">
</el-option>
<el-option v-for="(item,index) in formSelect.company_type" :key="index" :label="item"
:value="item">
</el-option>
</el-select>
</div>
</div>

@ -45,7 +45,12 @@
</el-option>
</el-select>
</div>
<div>
<el-select v-model="select.from" placeholder="请选择学员标签" clearable style="width: 100%;">
<el-option v-for="(item,index) in formSelect.from_tag" :key="index" :label="item.value" :value="item.value">
</el-option>
</el-select>
</div>
<div style="width:250px">
<el-date-picker v-model="birthdayDates" format="yyyy-MM-dd" value-format="yyyy-MM-dd"
style="width:100%" @change="changeBirthdayDates" type="daterange" range-separator="至"
@ -121,8 +126,8 @@
<el-select style="width:100%" v-model="companyType"
@change="(e)=>{changeSelectType(e,'company_type')}" multiple collapse-tags
placeholder="请选择企业资质" clearable>
<el-option v-for="item in formSelect.company_type" :key="item.id" :label="item.value"
:value="item.value">
<el-option v-for="(item,index) in formSelect.company_type" :key="index" :label="item"
:value="item">
</el-option>
</el-select>
</div>
@ -139,7 +144,10 @@
</el-select>
</div>
<div>
<el-input v-model="select.company_tag" placeholder="请输入集团标签"></el-input>
<el-select style="width:100%" v-model="select.is_yh_invested" placeholder="请选择集团标签" clearable>
<el-option label="被投企业" :value="1">
</el-option>
</el-select>
</div>
<div>
<el-select v-model="select.has_openid" placeholder="是否绑定小程序" clearable>
@ -264,10 +272,10 @@
<el-table-column align='center' label="学员标签" width="120" header-align="center">
<template slot-scope="scope">
<div v-if="scope.row.from">
<el-tag
v-for="tag in getStudentTags(scope.row.from)"
<el-tag
v-for="tag in getStudentTags(scope.row.from)"
:key="tag"
style="margin: 2px;"
>
{{ tag }}
@ -337,6 +345,7 @@
name: '',
year:'',
mobile: '',
from:'',
course_name: '',
company_name: '',
school: '',
@ -350,7 +359,7 @@
company_area: '',
company_type: '',
company_industry: '',
company_tag: '',
is_yh_invested: '',
is_vip: '',
is_schoolmate: '',
courses_end_date: '',
@ -459,7 +468,7 @@
width: 120,
customFn:(row)=>{
if(row.company){
return(<div>{row.company.is_yh_invested?'被投企业':''}</div>)
return(row.company.is_yh_invested?<el-tag type="danger">被投企业</el-tag>:'')
}
}
},
@ -504,7 +513,12 @@
},
exportExcel() {
let _export = {
'all_course': '课程名称'
'all_course': '课程名称',
'from': '学员标签',
'no':'学号',
'is_yh_invested_text': '集团标签',
'is_vip_text': '学员身份',
'is_schoolmate_text': '是否校友库学员'
}
this.selectFormList.map(item => {
if (item.prop === 'index') {
@ -513,6 +527,7 @@
_export[item.field] = item.name
}
})
download(
'/api/admin/users/study',
'get', {
@ -521,6 +536,7 @@
name: this.select.name,
year:this.select.year?this.select.year:'',
mobile: this.select.mobile,
from: this.select.from,
company_name: this.select.company_name,
school: this.select.school,
start_birthday: this.select.start_birthday,
@ -582,6 +598,7 @@
this.select.name = ''
this.select.year = ''
this.select.mobile = ''
this.select.from = ''
this.select.company_name = ''
this.select.school = ''
this.select.company_has_share = ''
@ -597,7 +614,7 @@
this.select.company_area = ''
this.select.company_type = ''
this.select.company_industry = ''
this.select.company_tag = ''
this.select.is_yh_invested = ''
this.select.is_vip = ''
this.select.courses_end_date = ''
this.select.is_schoolmate = ''
@ -618,6 +635,7 @@
year:this.select.year?this.select.year:'',
mobile: this.select.mobile,
company_name: this.select.company_name,
from:this.select.from,
school: this.select.school,
start_birthday: this.select.start_birthday,
end_birthday: this.select.end_birthday,
@ -630,7 +648,7 @@
company_area: this.select.company_area,
company_type: this.select.company_type,
company_industry: this.select.company_industry,
company_tag: this.select.company_tag,
is_yh_invested: this.select.is_yh_invested,
is_vip: this.select.is_vip,
courses_end_date: this.select.courses_end_date,
is_schoolmate: this.select.is_schoolmate,
@ -684,12 +702,12 @@
} else {
// -
const blacklistedUsers = this.seleSchoolmates.filter(user => user.is_black == 1);
if (blacklistedUsers.length > 0) {
//
const blacklistedNames = blacklistedUsers.map(user => user.username || user.name || '未知用户').join('、');
const otherUsers = this.seleSchoolmates.filter(user => user.is_black != 1);
this.$confirm(
`以下学员\n\n${blacklistedNames}\n\n目前处于黑名单中需先将其设置为非黑名单才可以加入到校友库中是否先将其他学员设置为校友`,
'黑名单提示',
@ -736,12 +754,12 @@
submitUpdateSchoolmates(vip, users = null) {
// users使使seleSchoolmates
const targetUsers = users || this.seleSchoolmates;
let ids = []
targetUsers.map(item => {
ids.push(item.id)
})
updateSchoolmate({
is_schoolmate: vip,
ids: ids.join(','),
@ -749,14 +767,14 @@
}).then(res => {
const userCount = targetUsers.length;
const operation = vip === 1 ? '加入校友库' : '从校友库移除';
if (users && users.length !== this.seleSchoolmates.length) {
//
this.$Message.success(`批量${operation}成功,共处理 ${userCount} 名学员`)
} else {
this.$Message.success(`批量${operation}成功`)
}
this.getList()
}).catch(error => {
this.$Message.error("批量设置失败:" + (error.message || '未知错误'))

@ -45,7 +45,12 @@
</el-option>
</el-select>
</div>
<div>
<el-select v-model="select.from" placeholder="请选择学员标签" clearable style="width: 100%;">
<el-option v-for="(item,index) in formSelect.from_tag" :key="index" :label="item.value" :value="item.value">
</el-option>
</el-select>
</div>
<div style="width:250px">
<el-date-picker v-model="birthdayDates" format="yyyy-MM-dd" value-format="yyyy-MM-dd"
style="width:100%" @change="changeBirthdayDates" type="daterange" range-separator="至"
@ -121,9 +126,9 @@
<el-select style="width:100%" v-model="companyType"
@change="(e)=>{changeSelectType(e,'company_type')}" multiple collapse-tags
placeholder="请选择企业资质" clearable>
<el-option v-for="item in formSelect.company_type" :key="item.id" :label="item.value"
:value="item.value">
</el-option>
<el-option v-for="(item,index) in formSelect.company_type" :key="index" :label="item"
:value="item">
</el-option>
</el-select>
</div>
@ -139,7 +144,10 @@
</el-select>
</div>
<div>
<el-input v-model="select.company_tag" placeholder="请输入集团标签"></el-input>
<el-select style="width:100%" v-model="select.is_yh_invested" placeholder="请选择集团标签" clearable>
<el-option label="被投企业" :value="1">
</el-option>
</el-select>
</div>
<div>
<el-select v-model="select.has_openid" placeholder="是否绑定小程序" clearable>
@ -156,6 +164,7 @@
<div>
<el-button type="primary" size="small" @click="select.page=1,getList()"></el-button>
<el-button type="primary" size="small" @click="resetSelect"></el-button>
<el-button type="primary" size="small" @click="exportExcel"></el-button>
<el-button type="primary" size="small" @click="updateSchoolmates(0)"></el-button>
</div>
</div>
@ -196,7 +205,23 @@
</template>
</el-table-column>
</template>
<template v-slot:from>
<el-table-column align='center' label="学员标签" width="120" header-align="center">
<template slot-scope="scope">
<div v-if="scope.row.from">
<el-tag
v-for="tag in getStudentTags(scope.row.from)"
:key="tag"
style="margin: 2px;"
>
{{ tag }}
</el-tag>
</div>
<span v-else></span>
</template>
</el-table-column>
</template>
<template v-slot:headimgurl>
<el-table-column align='center' label="头像" width="100" header-align="center">
<template slot-scope="scope">
@ -270,19 +295,20 @@
</template>
<script>
import studentDetail from './components/detail.vue';
import editDetail from './components/editDetail.vue';
import myMixins from "@/mixin/selectMixin.js";
import formMixin from "@/mixin/formMixin.js";
import {
indexStudy,
updateSchoolmate
} from '@/api/student/index.js'
import imports from "@/views/component/imports.vue"
import {
index as indexTypes
} from "@/api/course/courseType.js"
import studentDetail from './components/detail.vue';
import editDetail from './components/editDetail.vue';
import myMixins from "@/mixin/selectMixin.js";
import formMixin from "@/mixin/formMixin.js";
import {
indexStudy,
updateSchoolmate
} from '@/api/student/index.js'
import imports from "@/views/component/imports.vue"
import {
index as indexTypes
} from "@/api/course/courseType.js"
import { download } from '@/utils/downloadRequest'
export default {
mixins: [myMixins, formMixin],
components: {
@ -306,6 +332,7 @@
name: '',
year:'',
mobile: '',
from:'',
course_name: '',
company_name: '',
school: '',
@ -319,7 +346,7 @@
company_area: '',
company_type: '',
company_industry: '',
company_tag: '',
is_yh_invested: '',
is_vip: '',
is_schoolmate: 1,
courses_end_date: '',
@ -404,12 +431,17 @@
label: '学员标签',
align: 'center',
width: 120,
}, {
prop: 'is_schoolmate',
label: '是否校友库学员',
},{
prop: 'is_yuanhe',
label: '集团标签',
align: 'center',
width: 120,
}, {
customFn:(row)=>{
if(row.company){
return(row.company.is_yh_invested?<el-tag type="danger">被投企业</el-tag>:'')
}
}
},{
prop: 'is_vip',
label: '学员身份',
align: 'center',
@ -428,6 +460,10 @@
this.getCourseType()
},
methods: {
getStudentTags(tags) {
if (!tags) return [];
return tags.split(',').map(tag => tag.trim());
},
changeSelectType(e, selectName) {
if (e) {
this.select[selectName] = e.join(",")
@ -487,6 +523,7 @@
this.select.name = ''
this.select.year = ''
this.select.mobile = ''
this.select.from = ''
this.select.company_name = ''
this.select.school = ''
this.select.company_has_share = ''
@ -503,7 +540,7 @@
this.select.company_type = ''
this.select.company_industry = ''
this.select.is_vip = ''
this.select.company_tag = ''
this.select.is_yh_invested = ''
this.select.courses_end_date = ''
this.select.is_schoolmate = 1
this.select.education = ''
@ -515,13 +552,20 @@
this.getList()
},
async getList() {
const res = await indexStudy({
const params = this.buildQueryParams()
const res = await indexStudy(params)
this.list = res.list.data
this.total = res.list.total
},
buildQueryParams(extra = {}) {
return {
page: this.select.page,
page_size: this.select.page_size,
keyword: this.select.keyword,
name: this.select.name,
year:this.select.year?this.select.year:'',
year: this.select.year ? this.select.year : '',
mobile: this.select.mobile,
from: this.select.from,
company_name: this.select.company_name,
school: this.select.school,
start_birthday: this.select.start_birthday,
@ -535,18 +579,17 @@
company_area: this.select.company_area,
company_type: this.select.company_type,
company_industry: this.select.company_industry,
company_tag: this.select.company_tag,
is_yh_invested: this.select.is_yh_invested,
is_vip: this.select.is_vip,
courses_end_date: this.select.courses_end_date,
is_schoolmate: this.select.is_schoolmate,
education: this.select.education,
type: this.select.type,
status: this.select.status,
has_openid:this.select.has_openid,
course_type: this.select.course_type
})
this.list = res.list.data
this.total = res.list.total
has_openid: this.select.has_openid,
course_type: this.select.course_type,
...extra
}
},
changeDate(e) {
if (e) {
@ -555,6 +598,35 @@
this.select.courses_end_date = ''
}
},
exportExcel() {
let _export = {
'all_course': '课程名称',
'no':'学号',
'from': '学员标签',
'is_yh_invested_text': '集团标签',
'is_vip_text': '学员身份',
'is_schoolmate_text': '是否校友库学员'
}
this.selectFormList.map(item => {
if (item.prop === 'index') {
} else {
_export[item.field] = item.name
}
})
const params = this.buildQueryParams({
page: 1,
page_size: 9999,
is_export:1,
export_fields: _export
})
download(
'/api/admin/users/study',
'get',
params,
'学员列表.xlsx'
)
},
//
selectionChange(e) {
console.log(e)
@ -583,12 +655,12 @@
} else {
// -
const blacklistedUsers = this.seleSchoolmates.filter(user => user.is_black == 1);
if (blacklistedUsers.length > 0) {
//
const blacklistedNames = blacklistedUsers.map(user => user.username || user.name || '未知用户').join('、');
const otherUsers = this.seleSchoolmates.filter(user => user.is_black != 1);
this.$confirm(
`以下学员目前处于黑名单中,需先将其设置为非黑名单才可以加入到校友库中:\n\n${blacklistedNames}\n\n是否先将其他学员设置为校友`,
'黑名单提示',
@ -635,12 +707,12 @@
submitUpdateSchoolmates(vip, users = null) {
// users使使seleSchoolmates
const targetUsers = users || this.seleSchoolmates;
let ids = []
targetUsers.map(item => {
ids.push(item.id)
})
updateSchoolmate({
is_schoolmate: vip,
ids: ids.join(','),
@ -648,14 +720,14 @@
}).then(res => {
const userCount = targetUsers.length;
const operation = vip === 1 ? '加入校友库' : '从校友库移除';
if (users && users.length !== this.seleSchoolmates.length) {
//
this.$Message.success(`批量${operation}成功,共处理 ${userCount} 名学员(已剔除黑名单学员)`)
} else {
this.$Message.success(`批量${operation}成功`)
}
this.getList()
}).catch(error => {
this.$Message.error("批量设置失败:" + (error.message || '未知错误'))

@ -10,28 +10,25 @@
<el-input v-model="select.company_name" placeholder="请输入企业名称"></el-input>
</div>
<div>
<el-select v-model="select.company_industry" placeholder="请选择企业资质" clearable style="width: 100%;">
<el-option v-for="(item,index) in formSelect.company_type" :key="index" :label="item.value" :value="item.value">
<el-select collapse-tags multiple v-model="select.company_tag" placeholder="请选择企业资质" clearable style="width: 100%;">
<el-option v-for="(item,index) in companiesTags" :key="index" :label="item" :value="item">
</el-option>
</el-select>
</div>
<div>
<el-input v-model="select.company_tag" placeholder="请输入集团标签"></el-input>
<!-- <el-select v-model="select.company_tag" placeholder="请选择集团标签" clearable style="width: 100%;">
<el-option label="被投企业" value="被投企业">
<el-select v-model="select.is_yh_invested" placeholder="请选择集团标签" clearable style="width: 100%;">
<el-option label="被投企业" :value="1">
</el-option>
<el-option v-for="(item,index) in formSelect.company_tag" :key="index" :label="item.value" :value="item.value">
</el-option>
</el-select> -->
</el-select>
</div>
<div>
<el-button type="primary" size="small" @click="select.page=1,getList()"></el-button>
<el-button type="primary" size="small" @click="resetSelect"></el-button>
<el-button type="primary" size="small" @click="exportExcel"></el-button>
</div>
<div>
<!-- <div>
<el-button type="primary" size="small" @click="editCompany('add')"></el-button>
</div>
</div> -->
</div>
</div>
@ -60,73 +57,117 @@
</template>
</el-table-column>
</template>
<template v-slot:company_shareholder>
<el-table-column align='center' label="股东信息" width="120" header-align="center">
<template v-slot:partners>
<el-table-column align='center' label="股东信息" width="360" header-align="center">
<template slot-scope="scope">
<div @click="toQicc(item.company_name)" style="color:blue;cursor: pointer;text-decoration: underline;">
{{scope.row.company_shareholder}}
<div v-for="(item,index) in scope.row.partners">
<div style="text-align: left;">
{{index+1}}{{ item.stockName }}-{{ item.stockPercent }}
</div>
</div>
</template>
</el-table-column>
</template>
<template v-slot:company_industry>
<el-table-column align='center' label="企业资质" width="160" header-align="center">
<template v-slot:project_users>
<el-table-column align='center' label="项目经理" width="300" header-align="center">
<template slot-scope="scope">
<div v-for="item in formSelect.company_type">
<div v-if="item.value===scope.row.company_industry">
<el-tag :type="item.remark?item.remark:''">{{scope.row.company_industry}}</el-tag>
<div v-for="(item,index) in scope.row.project_users">
<div style="text-align: left;">
{{index+1}}{{ item.groupName }}-{{ item.userName }}-{{ item.investDate }}
</div>
</div>
</template>
</el-table-column>
</template>
<!-- <template v-slot:tag>
<template v-slot:is_yh_invested>
<el-table-column align='center' label="集团标签" width="120" header-align="center">
<template slot-scope="scope">
<div v-if="scope.row.is_yh_invested" style="margin:3px">
<el-tag type="danger">被投企业</el-tag>
</div>
<div v-for="item in formSelect.company_tag">
<el-tag v-if="item.value===scope.row.company_tag" style="margin:3px"
:type="item.remark?item.remark:''">{{scope.row.company_tag}}</el-tag>
</div>
</template>
</el-table-column>
</template> -->
</template>
<template v-slot:users>
<el-table-column align='center' label="学员信息" width="800" header-align="center">
<el-table-column label="学号" width="120" align="center">
<template slot-scope="scope">
<div class="user-item" v-for="(item, uIdx) in (scope.row.users || [])" :key="uIdx">
{{ item.no || '' }}
<template v-if="item.course_signs && item.course_signs.length > 0">
<div v-for="(cs, cIdx) in item.course_signs" :key="cIdx" class="user-cell-row">
<span v-if="cIdx === 0">{{ item.no || '' }}</span>
</div>
</template>
<template v-else>
<div class="user-cell-row">{{ item.no || '' }}</div>
</template>
</div>
</template>
</el-table-column>
<el-table-column label="学员" width="120" align="left">
<template slot-scope="scope">
<div class="user-item" v-for="(item, uIdx) in (scope.row.users || [])" :key="uIdx">
{{ item.name || '' }}
<template v-if="item.course_signs && item.course_signs.length > 0">
<div v-for="(cs, cIdx) in item.course_signs" :key="cIdx" class="user-cell-row">
<span v-if="cIdx === 0">{{ item.name || '' }}</span>
</div>
</template>
<template v-else>
<div class="user-cell-row">{{ item.name || '' }}</div>
</template>
</div>
</template>
</el-table-column>
<el-table-column label="是否校友" width="120" align="left">
<template slot-scope="scope">
<div class="user-item" v-for="(item, uIdx) in (scope.row.users || [])" :key="uIdx">
<el-tag v-if="item.is_schoolmate===1" type=""></el-tag>
<el-tag v-else type="info"></el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="职务" width="120" align="left">
<template slot-scope="scope">
<div class="user-item" v-for="(item, uIdx) in (scope.row.users || [])" :key="uIdx">
{{ item.company_position || '' }}
<template v-if="item.course_signs && item.course_signs.length > 0">
<div v-for="(cs, cIdx) in item.course_signs" :key="cIdx" class="user-cell-row">
<span v-if="cIdx === 0">{{ item.company_position || '' }}</span>
</div>
</template>
<template v-else>
<div class="user-cell-row">{{ item.company_position || '' }}</div>
</template>
</div>
</template>
</el-table-column>
<el-table-column label="课程" width="240" align="left">
<el-table-column label="课程/入学时间" width="480" align="left">
<template slot-scope="scope">
<div class="user-item" v-for="(item, uIdx) in (scope.row.users || [])" :key="uIdx">
<template v-if="item.course_signs && item.course_signs.length > 0">
<span v-for="(cs, cIdx) in item.course_signs" :key="cIdx">
{{ cs.course && cs.course.name ? cs.course.name : '' }}<span v-if="cIdx + 1 < item.course_signs.length"></span>
</span>
<div v-for="(cs, cIdx) in item.course_signs" :key="cIdx" class="user-cell-row">
{{ cs.course && cs.course.name ? cs.course.name : '' }} - {{ cs.course && cs.course.start_date ? cs.course.start_date : '' }}
</div>
</template>
<template v-else>
<div class="user-cell-row"></div>
</template>
</div>
</template>
</el-table-column>
<!-- <el-table-column label="入学时间" width="120" align="left">
<template slot-scope="scope">
<div class="user-item" v-for="(item, uIdx) in (scope.row.users || [])" :key="uIdx">
<template v-if="item.course_signs && item.course_signs.length > 0">
<div v-for="(cs, cIdx) in item.course_signs" :key="cIdx" class="course-row">
{{ cs.course && cs.course.start_date ? cs.course.start_date : '' }}
</div>
</template>
<template v-else>
<div class="course-row"></div>
</template>
</div>
</template>
</el-table-column> -->
</el-table-column>
</template>
@ -149,9 +190,13 @@
<script>
import addCompany from './components/addCompany.vue';
import {
download
} from "@/utils/downloadRequest";
import {
index,
destroy
destroy,
companyConfig
} from "@/api/student/schoolmateCompany.js"
import formMixin from "@/mixin/formMixin.js";
export default {
@ -163,11 +208,12 @@
return {
select: {
company_name: '',
company_industry: '',
is_yh_invested: '',
company_tag: '',
page: 1,
page_size: 10
},
companiesTags:[],
list: [],
total: 0,
table_item: [{
@ -182,12 +228,12 @@
width: 240,
fixed: 'left'
}, {
prop: 'company_industry',
prop: 'company_tag',
label: '企业资质',
align: 'center',
width: 260
}, {
prop: 'company_tag',
prop: 'is_yh_invested',
label: '集团标签',
align: 'center',
width: 160,
@ -207,17 +253,12 @@
align: 'center',
width: 120,
}, {
prop: 'company_shareholder',
prop: 'partners',
label: '股东信息',
align: 'center',
width: 120,
}, {
prop: 'management_platform',
label: '管理平台',
align: 'center',
width: 120,
}, {
prop: 'project_manager',
prop: 'project_users',
label: '项目经理',
align: 'center',
width: 120,
@ -232,8 +273,51 @@
},
created() {
this.getList()
this.getCompanyConfig()
},
updated() {
//
this.$nextTick(() => {
this.alignUserItemHeights()
})
},
methods: {
exportExcel() {
console.log('导出')
let _export = {}
this.table_item.map(item => {
if (item.type === 'index') {
} else {
_export[item.prop] = item.label
}
})
download(
'/api/admin/company/index',
'get', {
export_fields: _export,
page_size: 9999,
page: 1,
is_export:1,
filter: [{
key: 'company_name',
op: 'like',
value: this.select.company_name
},{
key: 'is_yh_invested',
op: 'like',
value: this.select.is_yh_invested?this.select.is_yh_invested:''
},{
key: 'company_tag',
op: 'like',
value: this.select.company_tag?this.select.company_tag.join(','):''
}]
},
'校友企业信息.xlsx')
},
async getCompanyConfig() {
const res = await companyConfig()
this.companiesTags = res.companiesTags
},
toQicc(company_name) {
var url = 'https://www.qcc.com/web/search?key='
if(company_name){
@ -261,7 +345,7 @@
resetSelect() {
this.select.company_name = ''
this.select.company_industry = ''
this.select.is_yh_invested = ''
this.select.company_tag = ''
this.select.page = 1
this.getList()
@ -275,18 +359,22 @@
op: 'like',
value: this.select.company_name
},{
key: 'company_industry',
key: 'is_yh_invested',
op: 'like',
value: this.select.company_industry
value: this.select.is_yh_invested?this.select.is_yh_invested:''
},{
key: 'company_tag',
op: 'like',
value: this.select.company_tag
value: this.select.company_tag?this.select.company_tag.join(','):''
}]
})
this.list = res.data
this.total = res.total
//
this.$nextTick(() => {
this.alignUserItemHeights()
})
},
deleteList(id) {
var that = this;
@ -300,6 +388,51 @@
reject(error)
})
},
// 线
alignUserItemHeights() {
this.$nextTick(() => {
//
const tableRows = document.querySelectorAll('.el-table__body tbody tr')
if (!tableRows.length) return
tableRows.forEach(row => {
// user-item
const cells = row.querySelectorAll('td')
const userItemCells = Array.from(cells).filter(cell => {
return cell.querySelector('.user-item') !== null
})
if (userItemCells.length === 0) return
// user-item
const userItems = Array.from(userItemCells).map(cell => {
return Array.from(cell.querySelectorAll('.user-item'))
}).flat()
if (userItems.length === 0) return
// user-item
let maxHeight = 0
userItems.forEach(item => {
//
item.style.height = 'auto'
item.style.minHeight = 'auto'
const height = item.offsetHeight || item.scrollHeight
if (height > maxHeight) {
maxHeight = height
}
})
// user-item
if (maxHeight > 0) {
userItems.forEach(item => {
item.style.height = `${maxHeight}px`
item.style.minHeight = `${maxHeight}px`
})
}
})
})
},
}
}
</script>
@ -322,12 +455,27 @@
/* 校友信息子列每个用户的分隔样式 */
.user-item {
padding: 6px 0;
border-bottom: 1px dashed #dcdfe6;
padding: 0;
margin: 0;
line-height: 1.2;
word-break: break-all;
display: flex;
flex-direction: column;
box-sizing: border-box;
border-bottom: 1px dashed #dcdfe6;
}
.user-item:last-child {
border-bottom: none;
}
/* 统一的单元格行样式,确保所有列的行高一致 */
.user-cell-row {
height: 40px;
line-height: 28px;
padding: 6px 0;
margin: 0;
box-sizing: border-box;
display: flex;
align-items: center;
flex-shrink: 0;
}
</style>

@ -134,9 +134,9 @@
<el-select style="width:100%" v-model="companyType"
@change="(e)=>{changeSelectType(e,'company_type')}" multiple collapse-tags
placeholder="请选择企业资质" clearable>
<el-option v-for="item in formSelect.company_type" :key="item.id" :label="item.value"
:value="item.value">
</el-option>
<el-option v-for="(item,index) in formSelect.company_type" :key="index" :label="item"
:value="item">
</el-option>
</el-select>
</div>
@ -660,4 +660,4 @@
}
}
</style>
</style>

@ -0,0 +1,150 @@
<template>
<div>
<xy-dialog ref="dialog" :width="70" :is-show.sync="isShow" :type="'form'"
:title="type === 'add' ? '新增时间轴' : '编辑时间轴'" :form="form" :rules="rules" @submit="submit">
<template v-slot:title>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
<span style="color: red;font-weight: bold;padding-right: 4px;">*</span>标题
</div>
<div class="xy-table-item-content">
<el-input v-model="form.title" placeholder="请输入标题" clearable style="width: 100%;" />
</div>
</div>
</template>
<template v-slot:sort>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
<span style="color: red;font-weight: bold;padding-right: 4px;"></span>排序
</div>
<div class="xy-table-item-content">
<el-input-number v-model="form.sort" :min="0" placeholder="请输入排序默认为0" style="width: 100%;" />
</div>
</div>
</template>
<template v-slot:content>
<div class="xy-table-item">
<div class="xy-table-item-label" style="font-weight: bold">
<span style="color: red;font-weight: bold;padding-right: 4px;">*</span>内容
</div>
<div class="xy-table-item-content" style="flex-grow: 1;">
<my-tinymce v-if="showTinymce" @input="saveContent" :value="form.content" />
</div>
</div>
</template>
</xy-dialog>
</div>
</template>
<script>
import { save, show } from '@/api/timeEvent/index.js'
export default {
components: {
},
data() {
return {
isShow: false,
type: 'add',
id: '',
showTinymce: false,
form: {
time: '',
title: '',
sort: 0,
content: ''
},
rules: {
title: [{
required: true,
message: '请输入标题'
}],
content: [{
required: true,
message: '请输入内容'
}]
}
}
},
created() {},
methods: {
saveContent(e) {
this.form.content = e
},
submit() {
if (this.id) {
this.form.id = this.id
}
if (this.type === 'add') {
this.form.id = ''
}
//
if (!this.form.content || this.form.content.trim() === '') {
this.$message.warning('请输入内容')
return
}
save(this.form).then(res => {
this.$message({
type: 'success',
message: this.type === 'add' ? '新增成功' : '编辑成功'
})
this.isShow = false
this.$emit('refresh')
}).catch(error => {
console.log(error)
this.$message.error(this.type === 'add' ? '新增失败' : '编辑失败')
})
},
getDetail() {
show({
id: this.id
}).then(res => {
this.form = {
title: res.title || '',
sort: res.sort !== undefined ? res.sort : 0,
content: res.content || ''
}
this.showTinymce = true
}).catch(error => {
console.log(error)
this.$message.error('获取详情失败')
})
}
},
watch: {
isShow(newVal) {
if (newVal) {
if (this.type === 'editor') {
this.getDetail()
} else {
this.showTinymce = true
this.form = {
title: '',
sort: 0,
content: ''
}
}
} else {
this.id = ''
this.type = 'add'
this.showTinymce = false
this.form = {
title: '',
sort: 0,
content: ''
}
this.$refs['dialog'].reset()
}
}
}
}
</script>
<style scoped lang="scss">
::v-deep .content {
flex-basis: 100%;
}
</style>

@ -0,0 +1,146 @@
<template>
<div>
<div>
<div ref="lxHeader">
<lx-header icon="md-apps" :text="$route.meta.title" style="margin-bottom: 10px; border: 0px; margin-top: 15px">
<div slot="content">
<div class="searchwrap" style="display: flex;align-items: center;">
<div>
<el-input v-model="select.title" placeholder="请输入标题" clearable />
</div>
<div>
<el-button type="primary" size="small" @click="select.page=1,getList()"></el-button>
</div>
<div>
<el-button type="primary" size="small" @click="editTimeEvent('add')"></el-button>
</div>
</div>
</div>
</lx-header>
</div>
</div>
<div>
<xy-table :list="list" :total="total" :table-item="table_item" @pageIndexChange="pageIndexChange" @pageSizeChange="pageSizeChange">
<template v-slot:btns>
<el-table-column align="center" label="操作" width="180" header-align="center">
<template slot-scope="scope">
<el-button type="primary" size="small" @click="editTimeEvent('editor',scope.row.id)"></el-button>
<el-popconfirm title="确定删除吗?" style="margin:0 10px" @confirm="deleteList(scope.row.id)">
<el-button slot="reference" type="danger" size="small">删除</el-button>
</el-popconfirm>
</template>
</el-table-column>
</template>
</xy-table>
</div>
<add-time-event ref="addTimeEvent" @refresh="getList" />
</div>
</template>
<script>
import addTimeEvent from './components/addTimeEvent.vue'
import { index, destroy } from '@/api/timeEvent/index.js'
export default {
components: {
addTimeEvent
},
data() {
return {
select: {
title: '',
page: 1,
page_size: 10
},
list: [],
total: 0,
table_item: [{
prop: 'title',
label: '标题',
align: 'left',
minWidth: 200
}, {
prop: 'sort',
label: '排序',
align: 'center',
width: 100
}, {
prop: 'created_at',
label: '创建时间',
align: 'center',
width: 180
}]
}
},
created() {
this.getList()
},
methods: {
pageIndexChange(e) {
this.select.page = e
this.getList()
},
pageSizeChange(e) {
this.select.page_size = e
this.select.page = 1
this.getList()
},
editTimeEvent(type, id) {
if (type === 'editor') {
this.$refs.addTimeEvent.id = id
}
this.$refs.addTimeEvent.type = type
this.$refs.addTimeEvent.isShow = true
},
async getList() {
const params = {
page_size: this.select.page_size,
page: this.select.page,
sort_name: 'sort',
sort_type: 'ASC'
}
if (this.select.title) {
params.filter = [{
key: 'title',
op: 'like',
value: this.select.title
}]
}
const res = await index(params, false)
this.list = res.data
this.total = res.total
},
deleteList(id) {
destroy({
id: id
}).then(response => {
this.$message.success('删除成功')
this.getList()
}).catch(error => {
console.log(error)
this.$message.error('删除失败')
})
}
}
}
</script>
<style lang="scss" scoped>
.searchwrap {
display: flex;
align-items: center;
&>div {
display: flex;
align-items: center;
margin-right: 10px;
span {
min-width: 70px;
}
}
}
</style>

@ -0,0 +1,550 @@
# 用户统计数据配置 JSON 结构说明
## 概述
`user_statistics_configs` 表的 `config_json` 字段用于存储动态统计配置,包含三个主要部分:数据来源、条件设置、统计方式。
## JSON 结构
```json
{
"data_source": {
"main_model": "user|company|course_sign|course|course_type",
"relations": ["user", "company", "course_sign", "course", "course_type"]
},
"conditions": {
"logic": "and|or",
"items": [
{
"key": "字段名",
"operator": "操作类型",
"value": "值"
}
]
},
"statistics": {
"type": "sum|max|min|count|count_distinct",
"field": "统计字段sum/max/min 时使用,可选)",
"distinct_field": "去重字段count_distinct 时使用,可选)",
"group_by": "分组字段(可选,不设置则不分组)",
"order_by": {
"field": "排序字段(可选)",
"direction": "asc|desc"
}
}
}
```
---
## 一、数据来源data_source
### 1.1 主模型main_model
**说明**:指定统计数据的主要来源模型。
**可选值**
- `user` - 用户模型
- `company` - 公司模型
- `course_sign` - 报名模型
- `course` - 课程模型
- `course_type` - 课程分类模型
**示例**
```json
{
"main_model": "user"
}
```
### 1.2 关联模型relations
**说明**:指定需要关联的其他模型,可以关联多个模型。
**可选值**(数组):
- `user` - 用户模型
- `company` - 公司模型
- `course_sign` - 报名模型
- `course` - 课程模型
- `course_type` - 课程分类模型
**注意**
- 关联模型不能包含主模型本身
- 可以关联多个模型
- 数组可以为空
**示例**
```json
{
"relations": ["company", "course_sign"]
}
```
---
## 二、条件设置conditions
### 2.1 逻辑关系logic
**说明**:指定多个条件之间的逻辑关系。
**可选值**
- `and` - 所有条件都必须满足AND
- `or` - 至少一个条件满足OR
**示例**
```json
{
"logic": "and"
}
```
### 2.2 条件项items
**说明**:条件数组,每个条件包含键名、操作类型和值。
**条件项结构**
```json
{
"key": "字段名",
"operator": "操作类型",
"value": "值"
}
```
#### 字段说明
- **key**(字符串):要查询的字段名
- 可以是主模型的字段
- 可以是关联模型的字段(使用点号分隔,如 `company.name`
- **operator**(字符串):操作类型
- `eq` - 等于
- `neq` - 不等于
- `gt` - 大于
- `egt` - 大于等于
- `lt` - 小于
- `elt` - 小于等于
- `like` - 模糊匹配
- `notlike` - 不匹配
- `in` - 在范围内(值为逗号分隔的字符串)
- `notin` - 不在范围内
- `between` - 在范围内(值为逗号分隔的两个值)
- `notbetween` - 不在范围内
- `isnull` - 为空value 可省略)
- `isnotnull` - 不为空value 可省略)
- **value**(字符串/数字/数组):条件值
- 根据操作类型不同,值的形式也不同
- `in` 操作:值为逗号分隔的字符串,如 `"1,2,3"`
- `between` 操作:值为逗号分隔的两个值,如 `"2024-01-01,2024-12-31"`
- `isnull``isnotnull` 操作value 可以省略
**示例**
```json
{
"logic": "and",
"items": [
{
"key": "is_schoolmate",
"operator": "eq",
"value": "1"
},
{
"key": "company.is_yh_invested",
"operator": "eq",
"value": "1"
},
{
"key": "created_at",
"operator": "between",
"value": "2024-01-01,2024-12-31"
}
]
}
```
---
## 三、统计方式statistics
### 3.1 统计类型type
**说明**:指定统计的方式。
**可选值**
- `sum` - 求和(需要指定 `field` 字段)
- `max` - 最大值(需要指定 `field` 字段)
- `min` - 最小值(需要指定 `field` 字段)
- `count` - 统计总数量(不需要指定 `field` 字段)
- `count_distinct` - 统计去重数量(需要指定 `distinct_field` 字段)
**示例**
```json
{
"type": "count"
}
```
### 3.2 统计字段field
**说明**:当统计类型为 `sum`、`max` 或 `min` 时,指定要统计的字段名。
**注意**
- `type``sum`、`max`、`min` 时必须指定 `field`
- `type``count` 时可以省略 `field`
- 可以是主模型的字段
- 可以是关联模型的字段(使用点号分隔,如 `company.company_fund`
**示例**
```json
{
"type": "sum",
"field": "company_fund"
}
```
```json
{
"type": "max",
"field": "company.company_fund"
}
```
```json
{
"type": "min",
"field": "created_at"
}
```
### 3.3 去重字段distinct_field
**说明**:当统计类型为 `count_distinct` 时,指定要去重的字段名。
**注意**
- `type``count_distinct` 时必须指定 `distinct_field`
- 可以是主模型的字段
- 可以是关联模型的字段(使用点号分隔,如 `user.mobile`
- **可以与 `group_by` 同时使用**:可以按某个字段分组,然后统计每个分组的去重数量
**示例1不分组去重统计**
```json
{
"type": "count_distinct",
"distinct_field": "mobile"
}
```
**示例2关联模型字段去重**
```json
{
"type": "count_distinct",
"distinct_field": "user.mobile"
}
```
**示例3分组 + 去重统计(组合使用)**
```json
{
"type": "count_distinct",
"distinct_field": "user.mobile",
"group_by": "course.type"
}
```
### 3.4 分组字段group_by
**说明**:指定按哪个字段进行分组统计。这是一个可选配置,可以选择不分组或选择具体的分组字段。
**配置选项**
- **不分组**:不设置 `group_by` 字段,或设置为 `null`,将返回所有符合条件的记录列表
- **按字段分组**:设置具体的分组字段,将按该字段进行分组统计
**分组字段格式**
- 可以是主模型的字段(如:`company_area`
- 可以是关联模型的字段(使用点号分隔,如 `company.company_area`
**示例1不分组统计**
```json
{
"statistics": {
"type": "count"
// 不设置 group_by表示不分组
}
}
```
**示例2按主模型字段分组**
```json
{
"statistics": {
"type": "count",
"group_by": "company_area"
}
}
```
**示例3按关联模型字段分组**
```json
{
"statistics": {
"type": "count",
"group_by": "company.company_area"
}
}
```
**示例4分组 + 去重统计(组合使用)**
```json
{
"statistics": {
"type": "count_distinct",
"distinct_field": "user.mobile",
"group_by": "course.type"
}
}
```
### 3.4 排序方式order_by
**说明**:指定结果的排序方式。
**结构**
```json
{
"field": "排序字段",
"direction": "asc|desc"
}
```
**字段说明**
- **field**(字符串):排序字段名
- 可以是主模型的字段
- 可以是关联模型的字段(使用点号分隔)
- 可以是统计结果字段(如 `total`、`count`
- **direction**(字符串):排序方向
- `asc` - 升序
- `desc` - 降序
**示例**
```json
{
"order_by": {
"field": "total",
"direction": "desc"
}
}
```
---
## 完整示例
### 示例1统计各区域的校友人数
```json
{
"data_source": {
"main_model": "user",
"relations": ["company"]
},
"conditions": {
"logic": "and",
"items": [
{
"key": "is_schoolmate",
"operator": "eq",
"value": "1"
},
{
"key": "created_at",
"operator": "between",
"value": "2024-01-01,2024-12-31"
}
]
},
"statistics": {
"type": "count",
"group_by": "company.company_area",
"order_by": {
"field": "count",
"direction": "desc"
}
}
}
```
### 示例2统计各课程类型的报名人数
```json
{
"data_source": {
"main_model": "course_sign",
"relations": ["course", "user"]
},
"conditions": {
"logic": "and",
"items": [
{
"key": "status",
"operator": "eq",
"value": "1"
},
{
"key": "created_at",
"operator": "between",
"value": "2024-01-01,2024-12-31"
}
]
},
"statistics": {
"type": "count",
"group_by": "course.type",
"order_by": {
"field": "count",
"direction": "desc"
}
}
}
```
### 示例3统计各公司的融资总额
```json
{
"data_source": {
"main_model": "company",
"relations": ["user"]
},
"conditions": {
"logic": "and",
"items": [
{
"key": "is_yh_invested",
"operator": "eq",
"value": "1"
},
{
"key": "company_fund",
"operator": "isnotnull"
}
]
},
"statistics": {
"type": "sum",
"field": "company_fund",
"group_by": "company_area",
"order_by": {
"field": "total",
"direction": "desc"
}
}
}
```
### 示例4统计审核通过或待审核的报名人数
```json
{
"data_source": {
"main_model": "course_sign",
"relations": []
},
"conditions": {
"logic": "or",
"items": [
{
"key": "status",
"operator": "eq",
"value": "0"
},
{
"key": "status",
"operator": "eq",
"value": "1"
}
]
},
"statistics": {
"type": "count",
"order_by": {
"field": "created_at",
"direction": "desc"
}
}
}
```
### 示例5统计各课程类型的去重培养人数按手机号去重
```json
{
"data_source": {
"main_model": "course_sign",
"relations": ["user", "course"]
},
"conditions": {
"logic": "and",
"items": [
{
"key": "status",
"operator": "eq",
"value": "1"
},
{
"key": "created_at",
"operator": "between",
"value": "2020-01-01," . date('Y-m-d')
}
]
},
"statistics": {
"type": "count_distinct",
"distinct_field": "user.mobile",
"group_by": "course.type",
"order_by": {
"field": "total",
"direction": "desc"
}
}
}
```
---
## 注意事项
1. **字段引用**
- 主模型字段直接使用字段名
- 关联模型字段使用 `模型名.字段名` 格式
- 例如:`company.name`、`course.type`
2. **数据类型**
- 所有值在 JSON 中都存储为字符串
- 系统会根据字段类型自动转换
3. **条件逻辑**
- `and` 表示所有条件都必须满足
- `or` 表示至少一个条件满足
- 条件数组可以为空(表示无条件)
4. **统计字段**
- `sum` 类型必须指定 `field`
- `count` 类型不需要 `field`
- 分组字段可以为空(表示不分组)
5. **排序字段**
- 可以按任意字段排序
- 可以按统计结果字段排序(如 `total`、`count`
- 排序字段可以为空(使用默认排序)
---
## 文档版本
- **创建日期**2025-11-19
- **最后更新**2025-11-19

@ -27,8 +27,10 @@ module.exports = {
*
*/
publicPath: process.env.ENV === 'staging' ? '/admin' : '/admin',
// outputDir: '/Users/mac/Documents/朗业/2025/s-苏州科技商学院/wx.sstbc.com/public/admin',
outputDir: '/Users/mac/Documents/朗业/2024/s-苏州科技商学院/wx.sstbc.com/public/admin',
// 测试
outputDir: '/Users/mac/Documents/朗业/2025/s-苏州科技商学院/wx.sstbc.com/public/admin',
// 正式
// outputDir: '/Users/mac/Documents/朗业/2024/s-苏州科技商学院/wx.sstbc.com/public/admin',
assetsDir: 'static',
css: {
loaderOptions: { // 向 CSS 相关的 loader 传递选项

Loading…
Cancel
Save