This commit is contained in:
2026-09-11 16:39:50 +08:00
parent dc63a51638
commit 7d1e7dc9a8
27 changed files with 3471 additions and 1 deletions
@@ -0,0 +1,163 @@
<template>
<div style="position: relative !important">
<div style="margin-bottom: 10px">
<el-autocomplete
v-model="addressValue"
style="width: 100%"
:fetch-suggestions="querySearchAsync"
placeholder="请输入关键词查询地址"
@select="handleSelect"
:trigger-on-focus="false"
/>
</div>
<div id="mapContainer"></div>
<slot></slot>
</div>
</template>
<script>
module.exports = {
name: "TMap",
props: {
radius: {
type: Number,
default: 50
},
position: {
type: Array,
default: () => {
return []
}
},
view: { type: Boolean, default: false }
},
data() {
return {
map: null,
poi: this.position,
appMapCenterPointX: 0,
appMapCenterPointY: 0,
markerLayer: null,
addressValue: '',
circle: null,
}
},
methods: {
handleSelect(item) {
if (item.location) {
// 定位到中心点
const center = new TMap.LatLng(item.location.lat, item.location.lng)
this.map.setCenter(center)
this.clearMarker()
this.createMarker(center)
this.$set(this, "poi", [item.location.lat, item.location.lng])
}
},
// keyword为地址关键字;候选结果提供title、address和经纬度location。
querySearchAsync(queryString, cb) {
if (!queryString) {
cb([])
return
}
this.$axios.post("/platform/TMap/suggestion", {keyword: queryString}).then((res) => {
if (res.code === 0 && res.data.status === 0) {
cb((res.data.data || []).map((item) => Object.assign({}, item, {
value: item.title + "【详细地址:" + item.address + "】"
})))
} else {
this.$message.error(res.msg || res.data.message)
cb([])
}
}).catch(() => {
cb([])
})
},
initMap() {
const center = new TMap.LatLng(this.appMapCenterPointY, this.appMapCenterPointX)
this.$set(this, "map", new TMap.Map('mapContainer', {
resizeEnable: true,
zoom: 16,
center: center
}))
// 初始化
this.$set(this, "markerLayer", new TMap.MultiMarker({
map: this.map,
geometries: []
}));
if (this.poi && this.poi.length > 0) {
const posi = new TMap.LatLng(this.poi[0], this.poi[1])
this.createMarker(posi)
this.map.setCenter(posi)
} else {
this.createMarker(center)
}
this.map.on("click", (event) => {
if (!this.view) {
this.clearMarker()
this.createMarker(event.latLng)
this.$set(this, "poi", [event.latLng.getLat(), event.latLng.getLng()])
}
})
},
// 清除签到点位
clearMarker() {
if (this.markerLayer) {
this.markerLayer.setGeometries([])
}
if(this.circle) {
this.circle.setGeometries([])
}
},
// 创建签到点位
createMarker(position) {
this.markerLayer.add([
{
id: 'marker_' + Date.now(),
position: position
}
]);
this.$set(this, "circle", new TMap.MultiCircle({
map: this.map,
geometries: [{
center: position,
radius: this.radius,
}],
}));
},
getConfigKey(key) {
return this.$axios.post("/open/common/getConfigKey", {key}).then((resp) => resp.data)
}
},
watch: {
position(newVal) {
this.$set(this, "poi", newVal)
},
poi(newVal) {
this.$emit("update:position", newVal)
}
},
mounted() {
// 父页面已加载腾讯地图SDK;中心点沿用当前项目系统配置。
Promise.all([this.getConfigKey("AppMapCenterPointX"), this.getConfigKey("AppMapCenterPointY")])
.then(([lng, lat]) => {
this.$set(this, "appMapCenterPointX", Number(lng))
this.$set(this, "appMapCenterPointY", Number(lat))
this.$nextTick(() => this.initMap())
})
},
beforeDestroy() {
if (this.map) {
this.map.destroy()
}
}
}
</script>
<style scoped>
#mapContainer {
padding: 0;
margin: 0;
width: 100%;
height: 500px;
}
</style>
@@ -0,0 +1,229 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<div class="search">
<div class="search-item">
<div class="search-item-label">主活动名称:</div>
<div class="search-item-option">
<el-input v-model="pageForm.searchKeyword"
clearable
placeholder="请输入主活动名称"
@keyup.enter.native="doSearch">
</el-input>
</div>
</div>
<div class="search-query">
<el-button icon="el-icon-search" type="primary" @click="doSearch"></el-button>
</div>
</div>
</el-card>
<el-card shadow="never" class="mt10">
<table-tool :app="this" label="关联项目列表">
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd">创建关联项目</el-button>
</table-tool>
<el-table :data="tableData"
:size="tableSize"
class="vi-table"
row-key="id"
style="width: 100%"
v-loading="tableLoading">
<el-table-column type="index"
label="序号"
width="80px"
align="center"
header-align="center"
:index="indexMethod"></el-table-column>
<el-table-column prop="masterActivityName"
label="主活动名称"
align="center"
header-align="center"
show-overflow-tooltip></el-table-column>
<el-table-column prop="standardActivityNum"
label="达标活动数量"
align="center"
header-align="center"></el-table-column>
<el-table-column label="创建时间"
align="center"
header-align="center">
<template scope="{row}">
{{row.createdAt ? $moment(row.createdAt).format('YYYY-MM-DD HH:mm:ss') : ''}}
</template>
</el-table-column>
<el-table-column label="操作"
width="240px"
align="center"
header-align="center">
<template scope="{row}">
<el-button type="success" size="mini" @click="openView(row)">查看</el-button>
<el-button type="primary" size="mini" @click="openEdit(row)">编辑</el-button>
<el-button type="danger" size="mini" @click="doDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-row class="el-pagination-container">
<el-pagination
@size-change="pageSizeChange"
@current-change="pageNumberChange"
:current-page="pageForm.pageNumber"
:page-sizes="[10, 20, 30, 50]"
:page-size="pageForm.pageSize"
layout="total, sizes, prev, pager, next"
:total="pageForm.totalCount">
</el-pagination>
</el-row>
</el-card>
<template #edit>
<el-form :model="formData" :rules="rules" label-width="120px" ref="form">
<el-form-item label="主活动名称" prop="masterActivityName">
<el-input v-model="formData.masterActivityName" maxlength="100" placeholder="请填写主活动名称"></el-input>
</el-form-item>
<el-form-item label="达标活动数量" prop="standardActivityNum">
<el-input-number v-model="formData.standardActivityNum"
:min="1"
:precision="0"
style="width: 100%"></el-input-number>
</el-form-item>
</el-form>
<div style="display:flex;justify-content:flex-end">
<el-button type="primary" @click="doSubmit" :loading="formLoading">确定</el-button>
</div>
</template>
<template #view>
<el-form label-width="120px">
<el-form-item label="主活动名称">
{{viewData.masterActivityName}}
</el-form-item>
<el-form-item label="达标活动数量">
{{viewData.standardActivityNum}}
</el-form-item>
<el-form-item label="关联活动数">
{{viewActivityList.length}}
</el-form-item>
</el-form>
<el-table :data="viewActivityList" border style="width:100%">
<el-table-column prop="activityName"
label="活动名称"
align="center"
header-align="center"
show-overflow-tooltip></el-table-column>
<el-table-column prop="activityModelName"
label="活动模式"
width="120"
align="center"
header-align="center"></el-table-column>
<el-table-column label="创建时间"
width="180"
align="center"
header-align="center">
<template scope="{row}">
{{row.createdAt ? $moment(row.createdAt).format('YYYY-MM-DD HH:mm:ss') : ''}}
</template>
</el-table-column>
</el-table>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: '#app',
mixins: [initTableMixins],
data() {
return {
viewData: {},
viewActivityList: [],
rules: {
masterActivityName: [{required: true, message: '请填写主活动名称', trigger: ['change', 'blur']}],
standardActivityNum: [{required: true, message: '请填写达标活动数量', trigger: ['change', 'blur']}]
}
}
},
methods: {
openView(row) {
this.$axios.post(loc() + '/relationActivityList', {id: row.id}).then((resp) => {
if (resp.code === 0) {
this.$set(this, "viewData", Object.assign({}, row))
this.$set(this, "viewActivityList", resp.data || [])
this.$refs.guava.view()
} else {
this.notifyWarning(resp.msg)
}
})
},
openAdd() {
this.$refs.guava.edit()
this.$nextTick(() => {
this.$set(this, "formData", {
standardActivityNum: 1
})
if (this.$refs.form) {
this.$refs.form.clearValidate()
}
})
},
openEdit(row) {
this.$refs.guava.edit()
this.$nextTick(() => {
this.$set(this, "formData", Object.assign({}, row))
if (this.$refs.form) {
this.$refs.form.clearValidate()
}
})
},
doSubmit() {
this.$refs.form.validate((valid) => {
if (!valid) {
return
}
this.$set(this, "formLoading", true)
this.$axios.post(loc() + '/doSave', this.formData).then((resp) => {
if (resp.code === 0) {
this.notifySuccess(resp.msg)
this.$refs.guava.index()
this.pageData()
} else {
this.notifyWarning(resp.msg)
}
}).finally(() => {
this.$set(this, "formLoading", false)
})
})
},
doDelete(id) {
this.$confirm('您确定要删除该关联项目吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$set(this, "formLoading", true)
this.$axios.post(loc() + '/doRemove', {id: id}).then((resp) => {
if (resp.code === 0) {
this.notifySuccess(resp.msg)
this.pageData()
} else {
this.notifyWarning(resp.msg)
}
}).finally(() => {
this.$set(this, "formLoading", false)
})
}).catch(() => {
})
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,281 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<div class="search">
<div class="search-item">
<div class="search-item-label">年份:</div>
<div class="search-item-option">
<el-date-picker
:clearable="false"
placeholder="选择年"
style="width: 100%"
@change="getCascadersActivity"
type="year"
v-model="pageForm.year"
value-format="yyyy">
</el-date-picker>
</div>
</div>
<div class="search-item">
<div class="search-item-label">活动:</div>
<div class="search-item-option">
<div style="display: flex; align-items: center; gap: 10px;">
<el-select
:style="{width: subActivityOptions.length > 0 ? '50%' : '100%'}"
placeholder="请选择主活动"
@change="mainActivityChange"
v-model="pageForm.mainActivityId"
clearable>
<el-option
:key="item.value"
:label="item.label"
:value="item.value"
v-for="item in activityOptions"></el-option>
</el-select>
<el-select
v-if="subActivityOptions.length > 0"
style="width: 50%"
placeholder="请选择分活动"
@change="subActivityChange"
v-model="pageForm.subActivityId"
clearable>
<el-option
:key="item.value"
:label="item.label"
:value="item.value"
v-for="item in subActivityOptions"></el-option>
</el-select>
</div>
</div>
</div>
<div class="search-item">
<div class="search-item-label">分工会:</div>
<div class="search-item-option">
<el-select
@change="doSearch"
clearable
filterable
placeholder="请选择分工会"
style="width: 100%"
v-model="pageForm.unionId">
<el-option
:key="item.id"
:label="item.unionname"
:value="item.id"
v-for="item in unionOptions"></el-option>
</el-select>
</div>
</div>
<div class="search-item">
<div class="search-item-label">姓名/工号:</div>
<div class="search-item-option">
<el-input
@keyup.enter.native="doSearch"
clearable
placeholder="请输入姓名或工号"
v-model="pageForm.searchKeyword"></el-input>
</div>
</div>
<div class="search-query">
<el-button @click="doSearch" icon="el-icon-search" type="primary"></el-button>
</div>
</div>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="人员打卡情况">
<el-radio-group
v-model="pageForm.qualifiedStatus"
@change="doSearch"
size="mini">
<el-radio-button label="qualified">已达标人员</el-radio-button>
<el-radio-button label="unqualified">未达标人员</el-radio-button>
<el-radio-button label="all">全部</el-radio-button>
</el-radio-group>
<el-button
@click="exportExcel"
class="ml10"
icon="el-icon-download"
size="small"
type="primary">
导出
</el-button>
</table-tool>
<el-table :data="tableData" :size="tableSize" v-loading="tableLoading">
<el-table-column :index="indexMethod" align="center" header-align="center" label="序号" type="index"
width="80px"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns">
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
let vue = new Vue({
el: '#app',
mixins: [initTableMixins],
data() {
return {
tableColumns: [
{label: '姓名', prop: 'username'},
{label: '工号', prop: 'loginname'},
{label: '分工会', prop: 'unionname'},
{label: '单位', prop: 'unitname'},
{label: '达标几个活动', prop: 'qualifiedActivityNum', sortable: true},
{label: '点位总数', prop: 'pts_size', sortable: true},
{label: '已打卡点位数', prop: 'sign_size', sortable: true},
{label: '是否全部打卡完全部点位', prop: 'isAllSigned', sortable: true}
],
unionOptions: [],
pageForm: {
unionId: '',
year: new Date().getFullYear() + '',
mainActivityId: '',
subActivityId: '',
activityId: '',
qualifiedStatus: 'all',
searchKeyword: '',
pageNumber: 1,
pageSize: 10,
totalCount: 0
},
activityOptions: [],
subActivityOptions: []
}
},
methods: {
/**
* 根据当前页面选择,统一返回活动查询参数。
* 后端会按主活动、分活动与最终 activityId 自动判断查询单个活动还是整个关联活动集合。
*/
buildActivityRequestParams() {
return {
mainActivityId: this.pageForm.mainActivityId,
subActivityId: this.pageForm.subActivityId,
activityId: this.pageForm.activityId,
qualifiedStatus: this.pageForm.qualifiedStatus
}
},
/**
* 导出与列表复用同一套筛选条件,保证当前可见条件与导出结果一致。
*/
buildExportQueryString() {
const params = Object.assign({
unionId: this.pageForm.unionId,
searchKeyword: this.pageForm.searchKeyword
}, this.buildActivityRequestParams())
return 'mainActivityId=' + encodeURIComponent(params.mainActivityId || '')
+ '&subActivityId=' + encodeURIComponent(params.subActivityId || '')
+ '&activityId=' + encodeURIComponent(params.activityId || '')
+ '&unionId=' + encodeURIComponent(params.unionId || '')
+ '&searchKeyword=' + encodeURIComponent(params.searchKeyword || '')
+ '&qualifiedStatus=' + encodeURIComponent(params.qualifiedStatus || 'all')
},
exportExcel() {
window.open(loc() + '/exportExcel?' + this.buildExportQueryString())
},
/**
* 根据主活动构建分活动选项,并同步当前真正查询用的 activityId。
* 当主活动存在分活动时,默认保持“只选主活动”的聚合查询,用户手动选择分活动后再切到单活动查询。
*/
syncActivitySelection(mainActivityId, needSearch) {
const currentMainActivity = this.activityOptions.find(item => {
return item.value === mainActivityId
})
const children = currentMainActivity && currentMainActivity.children ? currentMainActivity.children : []
this.$set(this, 'subActivityOptions', children)
if (!currentMainActivity) {
this.$set(this.pageForm, 'mainActivityId', '')
this.$set(this.pageForm, 'subActivityId', '')
this.$set(this.pageForm, 'activityId', '')
this.$set(this, 'tableData', [])
this.$set(this.pageForm, 'totalCount', 0)
return
}
if (children.length > 0) {
const currentSubActivityId = this.pageForm.subActivityId
const hasMatchedSubActivity = currentSubActivityId && children.some(item => {
return item.value === currentSubActivityId
})
this.$set(this.pageForm, 'subActivityId', hasMatchedSubActivity ? currentSubActivityId : '')
this.$set(this.pageForm, 'activityId', hasMatchedSubActivity ? currentSubActivityId : mainActivityId)
} else {
this.$set(this.pageForm, 'subActivityId', '')
this.$set(this.pageForm, 'activityId', mainActivityId)
}
if (needSearch) {
this.doSearch()
}
},
/**
* 主活动切换后,重新联动分活动并立即刷新列表。
*/
mainActivityChange(val) {
this.$set(this.pageForm, 'mainActivityId', val || '')
this.$set(this.pageForm, 'subActivityId', '')
this.syncActivitySelection(this.pageForm.mainActivityId, true)
},
/**
* 分活动切换后,更新最终查询 activityId 并刷新列表。
*/
subActivityChange(val) {
this.$set(this.pageForm, 'subActivityId', val || '')
this.$set(this.pageForm, 'activityId', val || this.pageForm.mainActivityId || '')
this.doSearch()
},
/**
* 年份切换后重载活动级联数据,并默认选中首个可用活动。
*/
getCascadersActivity() {
this.$axios.post(loc() + '/getCascadersActivity', {year: this.pageForm.year}).then(res => {
if (res.code !== 0) {
this.$message.error(res.msg)
return
}
this.$set(this, 'activityOptions', res.data || [])
if (this.activityOptions.length > 0) {
this.$set(this.pageForm, 'mainActivityId', this.activityOptions[0].value)
this.$set(this.pageForm, 'subActivityId', '')
this.syncActivitySelection(this.pageForm.mainActivityId, false)
this.pageData()
} else {
this.$set(this, 'subActivityOptions', [])
this.$set(this.pageForm, 'mainActivityId', '')
this.$set(this.pageForm, 'subActivityId', '')
this.$set(this.pageForm, 'activityId', '')
this.$set(this, 'tableData', [])
this.$set(this.pageForm, 'totalCount', 0)
}
})
}
},
created() {
this.$businessTool.listUnion().then(res => {
this.$set(this, 'unionOptions', res || [])
this.getCascadersActivity()
})
}
})
</script>
<!--#
}
#-->