提交 9c1e3c3f authored 作者: pengxiaohui's avatar pengxiaohui

update

上级 d60adcac
VITE_LOGIN_URL=https://login.ezijing.com/auth/login/index
VITE_SHARE_URL=https://marketing.ezijing.com
VITE_LOGIN_URL=https://login.ezijing.com/auth/login/index
VITE_LOGIN_URL=https://login2.ezijing.com/auth/login/index
VITE_SHARE_URL=https://marketing2.ezijing.com
\ No newline at end of file
VITE_LOGIN_URL=https://login2.ezijing.com/auth/login/index
VITE_SHARE_URL=https://marketing2.ezijing.com
\ No newline at end of file
......@@ -11,13 +11,15 @@
},
"dependencies": {
"axios": "^0.21.1",
"blueimp-md5": "^2.18.0",
"clipboard": "^2.0.8",
"element-ui": "^2.15.5",
"qrcode.vue": "^1.7.0",
"query-string": "^7.0.1",
"vue": "^2.6.14",
"vue-router": "^3.5.2",
"vuex": "^3.6.2"
"vuex": "^3.6.2",
"xlsx": "^0.17.0"
},
"devDependencies": {
"ali-oss": "^6.16.0",
......
......@@ -2,3 +2,33 @@
margin: 0;
padding: 0;
}
/* element-ui input,textarea font-family reset */
.el-input__inner, .el-textarea__inner{
font-family: 'PingFang SC', 'PingFangSC-Regular', 'Source Han Sans CN', -apple-system, 'Microsoft YaHei', 'Helvetica', 'Arial', Verdana,
'Hiragino Sans GB', 'Wenquanyi Micro Hei', sans-serif;
}
/* element-ui drawer reset */
.el-drawer__header{
padding:14px 16px;
margin:0;
border-bottom:1px solid #DCDFE6;
}
.el-drawer__header>h5{
font-size:16px;
color:#666;
font-weight:600;
}
.el-drawer__body{
height:calc(100% - 82px);
overflow-y: auto;
}
/* element-ui dialog reset */
.el-dialog__header{
padding:20px 16px 10px;
}
.el-dialog__body{
padding:10px 20px;
}
.el-dialog__footer{
text-align: center;
}
......@@ -32,8 +32,8 @@
</el-form-item>
</template>
<el-form-item class="filter-buttons">
<el-button type="primary" icon="el-icon-search" @click="search">搜索</el-button>
<el-button icon="el-icon-refresh-left" @click="reset">重置</el-button>
<el-button type="primary" size="small" icon="el-icon-search" @click="search">搜索</el-button>
<el-button size="small" icon="el-icon-refresh-left" @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
......@@ -140,24 +140,24 @@ export default {
let params = this.params
// 翻页参数设置
if (this.hasPagination) {
params.page = (this.page.currentPage - 1).toString()
params.page_size = this.page.size.toString()
params.page = this.page.currentPage.toString()
params.limit = this.page.size.toString()
}
// 接口请求之前
if (beforeRequest) {
params = beforeRequest(params, isReset)
}
// for (const key in params) {
// if (params[key] === '' || params[key] === undefined || params[key] === undefined) {
// delete params[key]
// }
// }
for (const key in params) {
if (params[key] === '' || params[key] === undefined || params[key] === null) {
delete params[key]
}
}
this.loading = true
httpRequest(params)
.then(res => {
const { data = [], page_info: pageInfo = {} } = res || {}
this.page.total = parseInt(pageInfo.total_number || '')
this.dataList = callback ? callback(data) : data
const { data = {} } = res || {}
this.page.total = parseInt(data.total || 0)
this.dataList = callback ? callback(data.data) : data.data
})
.catch(() => {
this.page.total = 0
......
......@@ -24,16 +24,22 @@ export default {
data() {
return {
menuList: [
{
name: '概览',
path: '/dashboard',
icon: 'el-icon-s-home'
},
// {
// name: '概览',
// path: '/dashboard',
// icon: 'el-icon-s-home'
// },
{
name: '营销工具',
path: '/tools/sign-in',
icon: 'el-icon-s-grid',
children: [{ name: '打卡签到', path: '/tools/sign-in' }]
},
{
name: '系统管理',
path: '/system/user',
icon: 'el-icon-s-tools',
children: [{ name: '用户管理', path: '/system/user' }]
}
]
}
......
......@@ -5,9 +5,6 @@ import store from './store'
import modules from './modules'
import beforeEnter from './utils/beforeEnter'
// 公共css
import '@/assets/css/base.css'
// 注册模块
modules({ router, store })
......@@ -16,6 +13,8 @@ import '@/assets/theme/index.css'
import ElementUI from 'element-ui'
Vue.use(ElementUI)
// 公共css
import '@/assets/css/base.css'
// 路由钩子函数
router.beforeEach(beforeEnter)
......
<template>
<div>dashboard</div>
<div>
概览
<!-- <button @click="handleClick">显示弹框</button> -->
<app-popup :visible.sync="popupVisible">
<a href="http://www.baidu.com">链接</a>
</app-popup>
</div>
</template>
<script>
export default {}
import AppPopup from './components/AppPopup.vue'
export default {
components: { AppPopup },
data() {
return {
popupVisible: false
}
},
methods: {
handleClick() {
this.popupVisible = true
}
}
}
</script>
<style>
......
<template>
<div class="popup" v-show="visible">
<div class="popup-container" :class="signStatus === 1 ? 'success':'warn'">
<p v-if="signStatus === 1" class="success">签到成功!</p>
<p v-if="signStatus === 2" class="warn">
您已迟到。<span>请于课后补看错过视频。</span>
</p>
<div class="content">
<slot></slot>
</div>
<i class="el-icon-circle-close" @click="handleClose"></i>
</div>
<div class="overlay"></div>
</div>
</template>
<script>
export default {
props: {
visible: {
type: Boolean,
default: false
},
signStatus: {
type: Number,
default: 1
}
},
data() {
return {}
},
methods: {
handleClose() {
this.$emit('update:visible', false)
}
}
}
</script>
<style scoped>
.overlay{
position:fixed;
left:0;
top:0;
right:0;
bottom:0;
z-index:1999;
background:rgba(0, 0, 0, .5);
}
.popup-container{
position:fixed;
left:50%;
top:50%;
transform:translate(-50%, -50%);
z-index:2000;
width:313px;
height:211px;
padding-top:150px;
border-radius:5px;
box-shadow: 0 1px 3px rgb(0 0 0 / 30%);
background-image: url('https://webapp-pub.ezijing.com/upload/marketing-admin/popup_bg1.png');
background-size:313px;
}
.popup-container.warn{
background-image: url('https://webapp-pub.ezijing.com/upload/marketing-admin/popup_bg2.png');
}
.popup-container p{
color:#1B6EBB;
font-size:28px;
text-align:center;
margin:0;
}
.popup-container p.warn{
color:#D51E2A;
}
.popup-container p.warn span{
color:#959595;
font-size:18px;
display:block;
}
.popup-container>i{
position:absolute;
left:50%;
bottom:-60px;
transform:translateX(-50%);
padding:5px;
cursor:pointer;
font-size:30px;
color:#fff;
}
.popup-container>i:hover{
color: #e3e3e3;
}
.content{
text-align: center;
padding-top:40px;
}
.content button{
width:180px;
font-size:16px;
background:#1B6EBB;
border-color:#1B6EBB;
}
.popup-container.warn .content button{
background:#D51E2A;
border-color:#D51E2A;
}
</style>
import user from './user'
import routes from './route'
export default function (options) {
// 路由
if (options.router && routes.length) {
routes.forEach(route => {
options.router.addRoute(route)
})
}
user(options)
}
\ No newline at end of file
import AppLayout from '@/components/layout/Index.vue'
const routes = [
{
name: 'system',
path: '/system',
component: AppLayout
}
]
export default routes
\ No newline at end of file
import httpRequest from '@/utils/axios'
/**
* 搜索统一用户
*/
export function search(params) {
return httpRequest.get('/api/marketing/admin/v1/system/search-user', { params })
}
/**
* 获取统一用户
*/
export function getUserList(params) {
return httpRequest.get('/api/marketing/admin/v1/users', { params })
}
/**
* 授权用户
*/
export function addUser(data) {
return httpRequest.post('/api/marketing/admin/v1/user', data)
}
/**
* 批量删除授权用户
*/
export function batchDeleteUser(data) {
return httpRequest.post('/api/marketing/admin/v1/user/batch-delete', data)
}
import routes from './route'
export default function (options = {}) {
// 路由
if (options.router && routes.length) {
routes.forEach(route => {
options.router.addRoute('system', route)
})
}
}
export default [
{
path: 'user',
component: () => import('./views/List.vue')
}
]
<template>
<app-card>
<app-list v-bind="tableOptions" ref="appList" @selection-change="handleSelectionChange">
<!-- 筛选 -->
<template v-slot:filter-user="{ params }">
<user-search v-model="params.sso_id" :options="{ size: 'small' }"/>
</template>
<template #header-aside>
<el-button type="primary" size="small" style="margin-top:5px;" @click="handleCreate">添加用户</el-button>
</template>
<template #footer>
<div class="selection_bar">
已选中 {{multipleSelection.length}}
<el-button style="margin-left:15px;" size="mini" :disabled="!multipleSelection.length" @click="handleRemove">删除</el-button>
</div>
</template>
</app-list>
<el-dialog title="添加用户" :visible.sync="dialogVisible" width="480px" :destroy-on-close="true" :close-on-click-modal="false" @close="handleDialogClose">
<el-form :model="form" :rules="rules" ref="ruleForm" label-width="60px">
<el-form-item label="用户" prop="sso_id" style="position:relative;">
<user-search v-model="form.sso_id" :options="{ size: 'small' }"/>
</el-form-item>
</el-form>
<div style="text-align:center;">
<el-button size="mini" @click="dialogVisible = false">取消</el-button>
<el-button type="primary" size="mini" @click="handleSubmit">提交</el-button>
</div>
</el-dialog>
</app-card>
</template>
<script>
// 引入组件
import AppList from '@/components/base/AppList.vue'
import AppCard from '@/components/base/AppCard.vue'
import UserSearch from './components/UserSearch.vue'
// api
import { getUserList, addUser, batchDeleteUser } from '../api'
export default {
components: { AppCard, AppList, UserSearch },
data() {
return {
multipleSelection: [],
dialogVisible: false,
form: {
sso_id: ''
},
rules: {
sso_id: { required: true, message: '请选择用户', trigger: 'change' }
}
}
},
computed: {
tableOptions() {
return {
remote: {
httpRequest: getUserList,
params: { sso_id: '' }
},
filters: [
{ prop: 'sso_id', slots: 'filter-user' }
],
columns: [
{ type: 'selection', minWidth: '50px', fixed: 'left' },
{ prop: 'id', label: '用户ID', minWidth: '160px' },
{ prop: 'sso_user.realname', label: '用户姓名', minWidth: '120px' },
{ prop: 'sso_user.nickname', label: '用户昵称', minWidth: '120px' },
{ prop: 'sso_user.email', label: '邮箱地址', minWidth: '160px' }
]
}
}
},
methods: {
handleSelectionChange(val) {
this.multipleSelection = val
},
handleCreate() {
this.dialogVisible = true
},
handleDialogClose() {
this.dialogVisible = false
this.form.sso_id = ''
},
handleSubmit() {
this.$refs.ruleForm.validate(valid => {
if (valid) {
this.fetchAddUser()
} else {
return false
}
this.handleDialogClose()
})
},
handleRemove() {
this.$confirm('执行操作则选中的用户将无法登录,确定删除??', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(() => {
this.fetchBatchDeleteUsers()
}).catch(() => {})
},
fetchAddUser() {
const params = {
sso_id: this.form.sso_id
}
addUser(params).then(res => {
if (res.code === 0) {
this.$message.success('添加用户成功')
this.$refs.appList.refetch(true)
} else {
this.$message.error('添加用户失败')
}
})
},
fetchBatchDeleteUsers() {
const params = {
sso_ids: this.multipleSelection.map(item => item.sso_id)
}
batchDeleteUser(params).then(res => {
if (res.code === 0 && res.data && res.data.status) {
this.$message.success('删除用户成功')
this.$refs.appList.refetch(true)
} else {
this.$message.error('删除用户失败')
}
})
}
}
}
</script>
<style>
.selection_bar{
font-size:14px;
color:#626266;
}
</style>
\ No newline at end of file
<template>
<el-select v-model="userId" v-bind="options" placeholder="输入邮箱/手机号码搜索" filterable remote :remote-method="fetchUserList" :loading="searchUsersloading" @change="handleChange">
<el-option :label="user.realname || user.nickname " :value="user.id" v-for="user in userList" :key="user.id" >
<span style="float: left">
{{ user.realname || user.nickname }}
<template v-if="user.mobile">(手机号:{{user.mobile}})</template>
</span>
<span style="float: right; color: #8492a6; font-size: 13px; margin:0 20px 0 10px;" v-if="user.email">邮箱:{{ user.email }}</span>
<span style="float: right; color: #8492a6; font-size: 13px; margin:0 20px 0 10px;" v-else>ID:{{ user.id }}</span>
</el-option>
</el-select>
</template>
<script>
import { search } from '../../api'
const MOBILE_REG = /^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\d{4,8}$/
const EMAIL_REG = /^[A-Za-z0-9]+([_.\\-][A-Za-z0-9]+)*@[A-Za-z0-9-.]+$/
export default {
props: {
value: {
type: [Array, String]
},
defaultList: {
type: Array,
default: () => {
return []
}
},
options: {
type: Object,
default: () => {
return {}
}
}
},
data() {
return {
userId: this.value,
searchUsersloading: false,
userList: []
}
},
watch: {
value: {
handler: function (nv) {
this.userId = nv
},
immediate: true,
deep: true
},
defaultList: {
handler: function (nv) {
this.userList = nv
},
immediate: true,
deep: true
}
},
methods: {
handleChange() {
const selectedUser = this.userList.find(item => item.id === this.userId)
this.$emit('input', this.userId)
this.$emit('select', selectedUser)
},
fetchUserList(val) {
let searchType = 'username'
if (EMAIL_REG.test(val)) {
searchType = 'email'
} else if (MOBILE_REG.test(val)) {
searchType = 'mobile'
}
if (!val) return false
else {
const params = {
[searchType]: val
}
this.searchUsersloading = true
search(params)
.then(res => {
this.searchUsersloading = false
if (res.data && Array.isArray(res.data.items)) {
this.userList = res.data.items
}
})
.catch(() => {})
}
}
}
}
</script>
<style scoped>
.el-select{
width:100%;
}
</style>
\ No newline at end of file
......@@ -3,6 +3,72 @@ import httpRequest from '@/utils/axios'
/**
* 获取商品列表
*/
export function getGoodsList(data) {
return httpRequest.post('/api/shop/commodity/spu/search', data).then({})
export function getActivityList(params) {
return httpRequest.get('/api/marketing/admin/v1/activities', { params })
}
/**
* 创建活动
*/
export function createActivity(data) {
return httpRequest.post('/api/marketing/admin/v1/activity', data)
}
/**
* 获取商品列表
*/
export function getActivityDetails(id) {
return httpRequest.get(`/api/marketing/admin/v1/${id}/activity`)
}
/**
* 更新活动
*/
export function updateActivity(id, data) {
return httpRequest.put(`/api/marketing/admin/v1/${id}/activity`, data)
}
/**
* 批量删除活动
*/
export function batchDeleteActivity(data) {
return httpRequest.post('/api/marketing/admin/v1/activity/batch-delete', data)
}
/**
* 导入学员
*/
export function importStudents(id, formData) {
return httpRequest({
url: `/api/marketing/admin/v1/activity/${id}/student/batch-import`,
method: 'post',
// headers: { 'Content-Type': 'multipart/form-data' },
timeout: 900000,
data: formData,
withCredentials: false
})
}
/**
* 导出学员列表
*/
export function exportStudents(id, data) {
return httpRequest({
url: `/api/marketing/admin/v1/activity/${id}/student/batch-export`,
method: 'post',
data,
responseType: 'blob'
})
}
/**
* 获取学员列表
*/
export function getStudentList(params) {
return httpRequest.get(`/api/marketing/admin/v1/activity/${params.id}/students`, { params })
}
/**
* 批量删除学员
*/
export function batchDeleteStudents(data) {
return httpRequest.post('/api/marketing/admin/v1/activity/student/batch-delete', data)
}
/**
* 批量标记签到/取消签到
*/
export function batchSignin(data) {
return httpRequest.post('/api/marketing/admin/v1/activity/students/sign-in', data)
}
\ No newline at end of file
......@@ -2,5 +2,9 @@ export default [
{
path: 'sign-in',
component: () => import('./views/List.vue')
},
{
path: 'preview',
component: () => import('./views/Preview.vue')
}
]
<template>
<app-card>
<app-list v-bind="tableOptions" ref="list">
<app-list v-bind="tableOptions" ref="appList">
<template #header-aside>
<el-button type="primary">新建活动</el-button>
<el-button type="primary" size="small" style="margin-top:5px;" @click="handleCreate">新建活动</el-button>
</template>
<!-- 筛选 -->
<template v-slot:filter-date="{ params }">
......@@ -12,6 +12,9 @@
range-separator="至"
start-placeholder="开始日期时间"
end-placeholder="结束日期时间"
value-format="yyyy-MM-dd HH:mm:ss"
size='small'
style="width:360px;"
>
</el-date-picker>
</template>
......@@ -19,30 +22,80 @@
<template v-slot:body="{ data }">
<div class="sign-item" v-for="(item, index) in data" :key="index">
<div class="sign-item-hd">
<el-checkbox v-model="item.checked">{{ item.name }}</el-checkbox>
<el-checkbox v-model="item.checked" @change="handleCheckChange(data)">{{ item.name }}</el-checkbox>
</div>
<div class="sign-item-bd">
<div class="sign-item-bd-pic"><img :src="item.img_url" width="100" /></div>
<div class="sign-item-bd-pic"><img :src="item.market_background_img" width="100" height="100" /></div>
<div class="sign-item-bd-content">
<p>名单人数:{{ item.user_count }}</p>
<p>签到人数:{{ item.sign_count }}</p>
<p>活动时间:{{ item.date }}</p>
<p>名单人数:{{ item.student_count }}<span style="margin-left:30px;">签到人数:{{ item.student_sign_count }}</span></p>
<p>活动时间:{{ item.start_time }}{{item.end_time}}</p>
<p>签到时间:{{ item.sign_start_time }}{{item.sign_end_time}}</p>
<p>活动地点:{{ item.address }}</p>
<p>创建时间:{{ item.created_at }}</p>
</div>
<div class="sign-item-bd-aside">
<el-button type="text">编辑活动</el-button>
<el-button type="text">导入人员</el-button>
<el-button type="text" @click="handleEdit(item)">编辑活动</el-button>
<el-button type="text" @click="handleImport(item)">导入人员</el-button>
<el-button type="text" @click="handlePromote(item)">推广</el-button>
<el-button type="text">人员列表</el-button>
<el-button type="text">删除</el-button>
<el-button type="text" @click="handlePerson(item)">人员列表</el-button>
<!-- <el-button type="text">删除</el-button> -->
</div>
</div>
</div>
</template>
<template #footer>
<div class="selection_bar">
已选中 {{multipleSelection.length}}
<el-button style="margin-left:15px;" size="mini" :disabled="!multipleSelection.length" @click="handleRemove">删除</el-button>
</div>
</template>
</app-list>
<!-- 推广 -->
<share-qrcode :visible.sync="shareDialogVisible" :value="shareUrl"></share-qrcode>
<el-drawer
:title="drawerTitle"
:visible.sync="drawerVisible"
:close-on-click-modal="false"
:destroy-on-close="true"
size="900px"
top="15px"
@close="handleClose">
<activity-form :type="drawerType" :data="selectedActivity" @refreshList="handleRefreshList"></activity-form>
</el-drawer>
<el-drawer
title="人员列表"
:visible.sync="personDrawerVisible"
:close-on-click-modal="false"
:destroy-on-close="true"
size="1100px"
top="15px"
@close="handleClose">
<person-list :id="selectedActivity.id" />
</el-drawer>
<el-dialog title="导入学员" :visible.sync="dialogVisible" width="480px" :destroy-on-close="true" :close-on-click-modal="false" @close="handleDialogClose">
<el-upload
class="file-import"
ref="upload"
action="#"
:auto-upload="false"
:file-list="fileList"
:limit="1"
:before-upload="beforeUpload"
:http-request="fetchFileUpload"
accept=".xls,.xlsx"
>
<el-button slot="trigger" size="mini" type="primary">选取文件</el-button>
<span slot="tip" style="margin-left:10px;">只能上传excel文件</span>
</el-upload>
<div style="margin-bottom:10px;">
导入模板下载:<a href="https://webapp-pub.ezijing.com/upload/marketing-admin/student_import.xlsx" download="课程模板"><el-button type="text" >student_import.xlsx</el-button></a>
</div>
<div style="text-align:center;">
<el-button size="mini" @click="dialogVisible = false">取消</el-button>
<el-button type="primary" size="mini" @click="submitUpload">确认提交</el-button>
</div>
</el-dialog>
</app-card>
</template>
......@@ -51,78 +104,160 @@
import AppList from '@/components/base/AppList.vue'
import AppCard from '@/components/base/AppCard.vue'
import ShareQrcode from '@/components/base/ShareQrcode.vue'
import ActivityForm from './components/ActivityForm.vue'
import PersonList from './components/PersonList.vue'
// 接口
// import { getRoleList } from '../api'
import { getActivityList, batchDeleteActivity, importStudents } from '../api'
// 方法
import { splitStrLast } from '@/utils/util'
export default {
components: { AppCard, AppList, ShareQrcode },
components: { AppCard, AppList, ShareQrcode, ActivityForm, PersonList },
data() {
return {
shareDialogVisible: false,
shareUrl: ''
shareUrl: '',
drawerVisible: false,
drawerType: 'create',
selectedActivity: {},
personDrawerVisible: false,
dialogVisible: false,
fileList: [],
importDisabled: false,
multipleSelection: []
}
},
computed: {
drawerTitle() {
return this.drawerType === 'create' ? '新建活动' : '编辑活动'
},
// 列表配置
tableOptions() {
return {
// remote: {
// httpRequest: getRoleList,
// params: { },
// },
remote: {
httpRequest: getActivityList,
beforeRequest: this.beforeRequest,
callback: this.callback,
params: { name: '' },
},
filters: [
{
type: 'input',
prop: 'name',
placeholder: '请输入活动名'
placeholder: '请输入活动名',
size: 'small'
},
{ prop: 'date', slots: 'filter-date' }
],
data: [
{
name: '毕业典礼签到活动',
user_count: 100,
sign_count: 100,
address: '北京市海淀区威盛大厦1楼',
date: '2020-12-12 13:34:12 至 2020-12-12 13:34:12',
img_url: 'https://zws-imgs-pub.ezijing.com/static/public/27f5b5a618df35e4d14665dfed8108d6.png'
},
{
name: '毕业典礼签到活动2',
user_count: 200,
sign_count: 200,
address: '北京市海淀区威盛大厦1楼',
date: '2020-12-12 13:34:12 至 2020-12-12 13:34:12',
img_url: 'https://zws-imgs-pub.ezijing.com/static/public/27f5b5a618df35e4d14665dfed8108d6.png'
}
]
}
}
},
methods: {
beforeRequest(params) {
if (params.date) {
const _params = Object.assign({}, params)
delete _params.date
_params.start_time = params.date[0]
_params.end_time = params.date[1]
return _params
} else {
return params
}
},
callback(data) {
data.forEach(item => {
item.checked = false
})
return data
},
handleCheckChange(val) {
const checkedVal = val.filter(item => item.checked)
this.multipleSelection = checkedVal
},
handleCreate() {
this.drawerType = 'create'
this.drawerVisible = true
},
handleEdit(row) {
this.drawerType = 'edit'
this.drawerVisible = true
this.selectedActivity = row
},
handleImport(row) {
this.dialogVisible = true
this.selectedActivity = row
},
handlePerson(row) {
this.personDrawerVisible = true
this.selectedActivity = row
},
// 编辑
handleUpdate(row) {
console.log(row)
},
// 删除
onRemove() {
this.$confirm('角色删除请谨慎操作,确定删除?', '删除角色', {
confirmButtonText: '删除',
cancelButtonText: '取消',
type: 'warning'
}).then(this.handleRemove)
},
handleClose() {},
// 删除
handleRemove() {
const data = this.multipleSelection.map(item => ({ spu_id: item.spu_id }))
deleteGoods({ shop_id: this.shopId, data }).then(res => {
this.$refs.list.refetch(true)
})
this.$confirm('执行操作将删除选中的活动,确定删除??', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(() => {
this.fetchBatchDeleteActivities()
}).catch(() => {})
},
// 推广
handlePromote(row) {
this.shareUrl = 'https://www.baidu.com/'
this.shareUrl = `${import.meta.env.VITE_SHARE_URL}/sign-in?id=${row.id}`
this.shareDialogVisible = true
},
handleDialogClose() {
this.fileList = []
},
handleRefreshList() {
this.$refs.appList.refetch(true)
this.multipleSelection = []
},
beforeUpload(file) {
const suffix = splitStrLast(file.name, '.')
if (!['xlsx', 'xls'].includes(suffix)) {
this.$message.error('只能上传excel文件')
return false
} else {
return true
}
},
submitUpload() {
if (!this.importDisabled) {
this.$refs.upload.submit()
}
},
fetchFileUpload(data) {
console.log(data)
const formData = new window.FormData()
formData.append('file', data.file)
importStudents(this.selectedActivity.id, formData).then(res => {
if (res.code === 0 && res.data && res.data.status) {
this.$message.success('导入数据成功')
this.handleRefreshList()
window.setTimeout(() => {
this.dialogVisible = false
}, 300)
} else {
this.$message.error(res.message || '导入数据失败,请重选选取文件上传')
}
})
},
fetchBatchDeleteActivities() {
const params = {
ids: this.multipleSelection.map(item => item.id)
}
batchDeleteActivity(params).then(res => {
if (res.code === 0 && res.data && res.data.status) {
this.$message.success('删除活动成功')
this.handleRefreshList()
} else {
this.$message.error('删除活动失败')
}
})
}
}
}
......
<template>
<div class="preview">
<div class="mobile">
<div class="content">
<!-- <img v-if="details.market_background_img" :src="details.market_background_img"> -->
<h5>签到成功</h5>
<p>此处显示签到备注文字</p>
</div>
</div>
<div class="bottom-bar">
<el-button size="small" @click="goBack">上一步</el-button>
<el-button size="small">完成</el-button>
</div>
</div>
</template>
<script>
import { getActivityDetails } from '../api'
export default {
data() {
return {
details: {}
}
},
computed: {
id() {
const { id = '' } = this.$route.query
return id
}
},
created() {
this.fetchActivityDetails()
},
methods: {
fetchActivityDetails() {
getActivityDetails(this.id).then(res => {
if (res.code === 0) {
this.details = res.data
}
})
},
goBack() {
this.$router.go(-1)
}
}
}
</script>
<style scoped>
.preview{
height:800px;
padding:30px 0;
background:#fff;
border-radius:6px;
}
.mobile{
width:320px;
height:600px;
background:#eee;
margin:0 auto;
border-radius:6px;
padding-top:100px;
}
.content{
margin:0 20px;
background:#fff;
height:300px;
border-radius:4px;
}
.content h5{
font-size:20px;
line-height:80px;
text-align:center;
}
.content p{
font-size:16px;
line-height:40px;
text-align:center;
margin-top:40px;
}
.bottom-bar{
margin-top:40px;
padding-top:20px;
text-align:center;
border-top:1px solid #e3e3e3;
}
</style>
\ No newline at end of file
<template>
<el-form :model="form" :rules="rules" ref="ruleForm" label-position="top" class="form-container">
<h5>基本内容</h5>
<div class="base-info">
<div class="left">
<el-form-item label="活动标题:" prop="name">
<el-input v-model="form.name" placeholder="请输入活动标题" size="mini"/>
</el-form-item>
<el-form-item label="活动时间:" prop="time">
<el-date-picker v-model="form.time" type="datetimerange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" value-format="yyyy-MM-dd HH:mm:ss" size="mini" style="width:360px;"/>
</el-form-item>
<el-form-item label="签到时间:">
<el-date-picker v-model="form.signin_time" type="datetimerange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" value-format="yyyy-MM-dd HH:mm:ss" size="mini" style="width:360px;"/>
</el-form-item>
</div>
<div class="right">
<el-form-item label="活动图片:" class="avatar-upload activity-img">
<el-upload class="avatar-uploader" action="https://webapp-pub.oss-cn-beijing.aliyuncs.com" :show-file-list="false" :before-upload="val => beforeUpload(val, 'background_img')" :on-success="(res, file) => handleSuccess(res, file, 'background_img')" :data="uploadData">
<img v-if="form.background_img" :src="form.background_img" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
<div class="avatar-upload-bar" v-if="form.background_img">
<div class="avatar-preview">
<i class="el-icon-zoom-in"></i>
<el-image :src="form.background_img" :preview-src-list="[form.background_img]" :z-index="9999"/>
</div>
<div>
<i class="el-icon-circle-close" @click="form.background_img = ''"></i>
</div>
</div>
</el-form-item>
<el-form-item label="活动地点:">
<el-input v-model="form.address" placeholder="请输入活动地点" size="mini" />
</el-form-item>
</div>
</div>
<h5>显示内容</h5>
<div class="show-info">
<el-form-item label="签到成功背景显示:" class="avatar-upload" prop="market_background_img">
<el-upload class="avatar-uploader" action="https://webapp-pub.oss-cn-beijing.aliyuncs.com" :show-file-list="false" :before-upload="val => beforeUpload(val, 'market_background_img')" :on-success="(res, file) => handleSuccess(res, file, 'market_background_img')" :data="uploadData">
<img v-if="form.market_background_img" :src="form.market_background_img" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
<div class="avatar-upload-bar" v-if="form.market_background_img">
<div class="avatar-preview">
<i class="el-icon-zoom-in"></i>
<el-image :src="form.market_background_img" :preview-src-list="[form.market_background_img]" :z-index="9999"/>
</div>
<div>
<i class="el-icon-circle-close" @click="form.market_background_img = ''"></i>
</div>
</div>
<div class="avatar-upload-text">
<p class="des">支持jpg,png等图片,限制大小在500M以内</p>
</div>
</el-form-item>
<el-form-item label="签到成功其他显示:" class="avatar-upload">
<el-upload class="avatar-uploader" action="https://webapp-pub.oss-cn-beijing.aliyuncs.com" :show-file-list="false" :before-upload="val => beforeUpload(val, 'market_other_img')" :on-success="(res, file) => handleSuccess(res, file, 'market_other_img')" :data="uploadData">
<img v-if="form.market_other_img" :src="form.market_other_img" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
<div class="avatar-upload-bar" v-if="form.market_other_img">
<div class="avatar-preview">
<i class="el-icon-zoom-in"></i>
<el-image :src="form.market_other_img" :preview-src-list="[form.market_other_img]" :z-index="9999"/>
</div>
<div>
<i class="el-icon-circle-close" @click="form.market_other_img = ''"></i>
</div>
</div>
<div class="avatar-upload-text">
<p class="des">支持jpg,png等图片,限制大小在500M以内</p>
</div>
</el-form-item>
</div>
<el-form-item style="text-align:center;padding-top:15px;">
<el-button type="primary" size="small" @click="handleEnter">保存并查看</el-button>
</el-form-item>
</el-form>
</template>
<script>
import { getSignature } from '@/api/base'
import { createActivity, updateActivity } from '../../api'
import md5 from 'blueimp-md5'
export default {
props: {
type: {
type: String,
default: 'create'
},
data: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
imgList: [],
imgDisabled: false,
uploadData: {},
form: {
name: '',
time: '',
signin_time: '',
background_img: '',
address: '',
market_background_img: '',
market_other_img: ''
},
rules: {
name: { required: true, message: '请输入活动标题', trigger: 'blur' },
time: { required: true, message: '请选择活动时间', trigger: 'change' },
market_background_img: { required: true, message: '请上传签到成功背景显示', trigger: 'change' }
}
}
},
created() {
if (this.type === 'edit') {
this.form.name = this.data.name
this.form.background_img = this.data.background_img
this.form.address = this.data.address
this.form.market_background_img = this.data.market_background_img
this.form.market_other_img = this.data.market_other_img
this.form.time = [this.data.start_time, this.data.end_time]
if (this.data.sign_end_time) {
this.signin_time = [this.data.sign_start_time, this.data.sign_end_time]
}
}
},
methods: {
handleEnter() {
this.$refs.ruleForm.validate(valid => {
if (valid) {
if (this.type === 'create') {
this.fetchActivityCreate()
} else {
this.fetchActivityUpdate()
}
}
})
},
handleSuccess(res, file, target) {
this.fileLoading = ''
const _file = file.raw
this.form[target] = _file.src
},
beforeUpload(file, target) {
let errorMsg = ''
if (!file.type.includes('image/')) {
errorMsg = '上传图片格式错误,只允许png/jpg格式'
}
if (errorMsg) {
this.$message.error(errorMsg)
return false
} else {
this.fileLoading = target
const fileName = file.name
const key = 'upload/marketing-admin/' + md5(fileName + new Date().getTime()) + fileName.substr(fileName.lastIndexOf('.'))
return new Promise((resolve, reject) => {
getSignature()
.then(response => {
const { accessid, policy, signature, host } = response
this.uploadData = { key, OSSAccessKeyId: accessid, policy, signature, success_action_status: '200' }
file.src = `${host}/${key}`
resolve(true)
})
.catch(err => {
console.log(err)
reject(err)
})
})
}
},
formParams() {
const params = Object.assign({}, this.form)
params.start_time = this.form.time[0]
params.end_time = this.form.time[1]
delete params.time
if (params.signin_time) {
params.sign_start_time = this.form.signin_time[0]
params.sign_end_time = this.form.signin_time[1]
}
delete params.signin_time
return params
},
fetchActivityCreate() {
createActivity(this.formParams()).then(res => {
if (res.code === 0 && res.data && res.data.id) {
this.$message.success('新建活动成功!')
this.$emit('refreshList')
this.$router.push({ path: '/tools/preview', query: { id: res.data.id }})
} else {
this.$message.error('新建活动失败!')
}
})
},
fetchActivityUpdate() {
updateActivity(this.data.id, this.formParams()).then(res => {
if (res.code === 0 && res.data && res.data.status) {
this.$message.success('更新活动成功')
this.$emit('refreshList')
} else {
this.$message.error('更新活动失败')
}
})
}
}
}
</script>
<style scoped>
h5{
font-size:16px;
font-weight: 400;
line-height:56px;
text-indent:25px;
color: #444;
}
.base-info, .show-info{
margin:0 25px;
border: 1px solid #ebebeb;
border-radius: 3px;
transition: .2s;
}
.base-info{
display:flex;
align-items: stretch;
}
.base-info .left{
width:calc(50% - 20px);
padding-right:20px;
}
.base-info .right{
flex-shrink: 0;
flex: 1;
box-sizing:border-box;
}
.el-form-item{
padding: 0 20px;
}
.el-form-item ::v-deep.el-form-item__label{
padding: 0;
}
.activity-img{
margin-bottom:0;
}
.avatar-upload .avatar-upload-text{
display:inline-block;
color: #c9c9c9;
line-height:48px;
font-size:14px;
text-indent:10px;
margin-top:50px;
position:relative;
}
.avatar-upload .avatar-upload-text .warn{
color:#f56c6c;
}
.avatar-upload .warn{
display:inline-block;
}
.avatar-uploader, .file-uploader{
display:inline-block;
vertical-align: top;
width:160px;
}
.avatar-uploader{
height:162px;
}
.avatar-uploader ::v-deep.el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
vertical-align: middle;
}
.avatar-uploader ::v-deep.el-upload:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 155px;
height: 154px;
line-height: 152px;
text-align: center;
}
.avatar {
width: 150px;
height: 150px;
display: block;
}
.avatar-upload-bar{
position:absolute;
left:1px;
top:121px;
width:150px;
height:30px;
border-radius:0 0 4px 0;
overflow: hidden;
background:rgba(0,0,0,.3);
color:#fff;
display:none;
border-radius:0 0 6px 6px;
}
.avatar-upload .el-form-item__content:hover .avatar-upload-bar{
display:flex;
}
.avatar-upload-bar>div{
flex:1;
position:relative;
cursor:pointer;
}
.avatar-upload-bar>div>i{
font-size:20px;
width:100%;
height:100%;
text-align:center;
line-height:30px;
}
.avatar-preview{
cursor:zoom-in;
}
.avatar-preview .el-image{
position:absolute;
left:0;
top:0;
width:100%;
height:100%;
opacity:0;
z-index:1;
}
.avatar-preview ::v-deep.el-image__preview{
cursor: zoom-in;
}
</style>
\ No newline at end of file
<template>
<div class="person-list">
<app-list v-bind="tableOptions" ref="appList" @selection-change="handleSelectionChange">
<template #header-aside>
<el-button type="primary" size="small" style="margin-top:5px;" @click="handleImport">导入</el-button>
</template>
<!-- 筛选 -->
<template v-slot:filter-type="{ params }">
<el-select v-model="params.type" placeholder="请选择类型" size="small">
<el-option label="姓名" value="username"></el-option>
<el-option label="手机号码" value="mobile"></el-option>
<el-option label="邮箱" value="email"></el-option>
</el-select>
</template>
<template #footer>
<div style="font-size:14px;">
已选中 {{multipleSelection.length}}
<el-button style="margin:0 15px;" size="mini" :disabled="!multipleSelection.length" @click="handleRemove">删除</el-button>
<!-- <el-button size="mini" :disabled="!multipleSelection.length" @click="exportSelected">导出</el-button> -->
<el-dropdown size="small" @command="handleCommand">
<el-button type="primary" size="mini">
导出<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="all">导出全部</el-dropdown-item>
<el-dropdown-item command="selected" :disabled="!multipleSelection.length">导出选中项</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-button style="margin-left:15px;" size="mini" :disabled="!multipleSelection.length" @click="fetchBatchSignin(1)">标记为已签到</el-button>
<el-button style="margin-left:15px;" size="mini" :disabled="!multipleSelection.length" @click="fetchBatchSignin(0)">取消签到</el-button>
</div>
</template>
</app-list>
<el-dialog title="导入学员" :visible.sync="dialogVisible" width="480px" append-to-body :close-on-click-modal="false" @close="handleDialogClose">
<el-upload
class="file-import"
ref="upload"
action="#"
:auto-upload="false"
:file-list="fileList"
:limit="1"
:before-upload="beforeUpload"
:http-request="fetchFileUpload"
accept=".xls,.xlsx"
>
<el-button slot="trigger" size="mini" type="primary">选取文件</el-button>
<span slot="tip" style="margin-left:10px;">只能上传excel文件</span>
</el-upload>
<div style="margin-bottom:10px;">
导入模板下载:<a href="https://webapp-pub.ezijing.com/upload/marketing-admin/student_import.xlsx" download="课程模板"><el-button type="text" >student_import.xlsx</el-button></a>
</div>
<div style="text-align:center;">
<el-button size="mini" @click="dialogVisible = false">取消</el-button>
<el-button type="primary" size="mini" @click="submitUpload">确认提交</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import AppList from '@/components/base/AppList.vue'
import { getStudentList, batchDeleteStudents, batchSignin, exportStudents, importStudents } from '../../api'
import { splitStrLast, funDownload } from '@/utils/util'
import XLSX from 'xlsx'
export default {
props: {
id: {
type: String,
default: ''
}
},
components: { AppList },
data() {
return {
multipleSelection: [],
dialogVisible: false,
fileList: [],
importDisabled: false
}
},
computed: {
tableOptions() {
return {
remote: {
httpRequest: getStudentList,
beforeRequest: this.beforeRequest,
params: { id: this.id, type: 'username', key: '' }
},
filters: [
{ prop: 'type', slots: 'filter-type' },
{ type: 'input', placeholder: '请输入', prop: 'key', size: 'small' }
],
columns: [
{ type: 'selection', minWidth: '50px', fixed: 'left' },
{ prop: 'username', label: '姓名', minWidth: '80px', fixed: 'left' },
{ prop: 'phone', label: '手机号码', minWidth: '100px' },
{ prop: 'email', label: '邮箱地址', minWidth: '130px' },
{ prop: 'company_name', label: '公司名称', minWidth: '140px' },
{ prop: 'position', label: '职务', minWidth: '80px' },
{ prop: 'age', label: '年龄', minWidth: '50px' },
{ prop: 'remark', label: '备注', minWidth: '160px', 'show-overflow-tooltip': true },
{
prop: 'sign_in_status',
label: '签到状态',
minWidth: '80px',
computed({ row }) {
return row.sign_in_status === 0 ? '未签到' : '已签到'
}
},
{ prop: 'sign_in_time', label: '签到时间', minWidth: '130px' }
]
}
}
},
methods: {
beforeRequest(params) {
const _params = Object.assign({}, params)
if (_params.key) {
_params[_params.type] = _params.key
}
delete _params.type
delete _params.key
return _params
},
handleSelectionChange(val) {
this.multipleSelection = val
},
handleCommand(type) {
if (type === 'all') this.fetchExportStudentList()
if (type === 'selected') this.exportSelected()
},
handleRemove() {
this.$confirm('执行操作将删除选中的学员,确定删除??', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(() => {
this.fetchBatchDeleteStudents()
}).catch(() => {})
},
handleImport() {
this.dialogVisible = true
},
handleDialogClose() {
this.fileList = []
},
beforeUpload(file) {
const suffix = splitStrLast(file.name, '.')
if (!['xlsx', 'xls'].includes(suffix)) {
this.$message.error('只能上传excel文件')
return false
} else {
return true
}
},
submitUpload() {
if (!this.importDisabled) {
this.$refs.upload.submit()
}
},
fetchFileUpload(data) {
const formData = new window.FormData()
formData.append('file', data.file)
importStudents(this.id, formData).then(res => {
if (res.code === 0 && res.data && res.data.status) {
this.$message.success('导入数据成功')
this.$refs.appList.refetch(true)
window.setTimeout(() => {
this.dialogVisible = false
}, 300)
} else {
this.$message.error(res.message || '导入数据失败,请重选选取文件上传')
}
})
},
fetchBatchDeleteStudents() {
const params = {
ids: this.multipleSelection.map(item => item.id)
}
batchDeleteStudents(params).then(res => {
if (res.code === 0 && res.data && res.data.status) {
this.$message.success('删除学员成功')
this.$refs.appList.refetch(true)
} else {
this.$message.error('删除学员失败')
}
})
},
fetchBatchSignin(status) {
const params = {
status,
ids: this.multipleSelection.map(item => item.id)
}
batchSignin(params).then(res => {
if (res.code === 0 && res.data && res.data.status) {
this.$message.success('更改签到状态成功')
this.$refs.appList.refetch(true)
} else {
this.$message.error('更改签到状态失败')
}
})
},
exportSelected() {
const list = this.tableOptions.columns.filter(item => {
return item.prop && item.prop !== 'head_img'
})
const headList = list.map(item => item.label)
const propList = list.map(item => item.prop)
const excelList = []
excelList.push(headList)
this.multipleSelection.forEach(item => {
const rowValArr = []
propList.forEach(key => {
let val = item[key]
if (key === 'sign_in_status') val = val === 1 ? '已签到' : '未签到'
rowValArr.push(val)
})
excelList.push(rowValArr)
})
const ws = XLSX.utils.aoa_to_sheet(excelList)
ws['!cols'] = [
{ wpx: 120 },
{ wpx: 120 },
{ wpx: 160 },
{ wpx: 180 },
{ wpx: 120 },
{ wpx: 80 },
{ wpx: 200 },
{ wpx: 120 },
{ wpx: 120 }
]
const wb = XLSX.utils.book_new()
wb.SheetNames.push('Worksheet')
wb.Sheets.Worksheet = ws
const wopts = { bookType: 'xlsx', bookSST: false, type: 'array' }
const wbout = XLSX.write(wb, wopts)
const url = URL.createObjectURL(new window.Blob([wbout], { type: 'application/octet-stream' }))
funDownload(url, `学员列表_${Date.now()}.xlsx`)
},
fetchExportStudentList() {
const params = {}
if (this.tableOptions.remote.params.key) {
params[this.tableOptions.remote.params.type] = this.tableOptions.remote.params.key
}
exportStudents(this.id, params).then(res => {
if (res && res.type === 'text/xlsx') {
const url = URL.createObjectURL(res)
funDownload(url, `学员列表_${Date.now()}.xlsx`)
}
})
}
}
}
</script>
<style scoped>
.person-list{
padding:15px 20px;
}
</style>
\ No newline at end of file
/**
* 文件下载
* @param {string} fileUrl 文件下载地址
* @param {string} fileName 文件名
* @returns {null}
*/
export function funDownload(fileUrl, fileName) {
// console.log(fileUrl)
const elink = document.createElement('a')// 创建一个a标签
elink.download = fileName;// 设置a标签的下载属性
elink.style.display = 'none';// 将a标签设置为隐藏
elink.href = fileUrl;// 把之前处理好的地址赋给a标签的href
document.body.appendChild(elink);// 将a标签添加到body中
elink.click();// 执行a标签的点击方法
// URL.revokeObjectURL(elink.href) // 下载完成释放URL 对象
document.body.removeChild(elink)// 移除a标签
}
/**
* 分割字符串,取得尾部
* @param {string} str 字符串
* @param {string} split 分割符
* @returns {string}
*/
export function splitStrLast(str, split) {
const fileNameArr = str.split(split)
const last = fileNameArr[fileNameArr.length - 1]
return last
}
\ No newline at end of file
......@@ -14,7 +14,7 @@ export default defineConfig({
cert: fs.readFileSync(path.join(__dirname, './certs/dev.ezijing.com.pem'))
},
proxy: {
'/api': 'https://shop-admin.ezijing.com'
'/api': 'https://marketing-admin2.ezijing.com'
}
},
resolve: {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论