This commit is contained in:
chy
2025-06-26 14:30:57 +08:00
parent db5d707e54
commit 310308c7d9
25 changed files with 3194 additions and 74 deletions

View File

@ -0,0 +1,6 @@
type StoreData struct {
g.Meta `orm:"table:stores"`
Id int `json:"id" orm:"id"`
MerchantId int `json:"merchantId" orm:"merchant_id"`
StoreName string `json:"storeName" orm:"name"`
}

View File

@ -8,3 +8,18 @@ export function feedbackList(params) {
params params
}) })
} }
export function feedbackDetail(id) {
return request({
url: `/x/feedback/${id}`,
method: 'get'
})
}
export function feedbackReply(data) {
return request({
url: '/x/feedback/reply',
method: 'put',
data
})
}

45
src/api/game.js Normal file
View File

@ -0,0 +1,45 @@
import request from '@/utils/request'
// 获取游戏列表,带分页参数
export function getGameList(params) {
return request({
url: '/x/game',
method: 'get',
params
})
}
// 新增游戏
export function addGame(data) {
return request({
url: '/x/game',
method: 'post',
data
})
}
// 编辑游戏
export function editGame(data) {
return request({
url: '/x/game',
method: 'put',
data
})
}
// 删除游戏
export function deleteGame(id) {
return request({
url: `/x/game/${id}`,
method: 'delete'
})
}
// 上传游戏图标
export function uploadGameImg(data) {
return request({
url: '/x/upload/game',
method: 'post',
data
})
}

View File

@ -1,6 +1,5 @@
import request from '@/utils/request' import request from '@/utils/request'
export function merchantList(params){ export function merchantList(params){
return request({ return request({
url: '/x/merchant', url: '/x/merchant',
@ -16,3 +15,19 @@ export function merchantAudit(data) {
data data
}) })
} }
export function merchantCreate(data) {
return request({
url: '/x/merchant/audit',
method: 'post',
data
})
}
export function merchantUpdate(data) {
return request({
url: '/x/merchant/audit',
method: 'post',
data
})
}

50
src/api/reward-type.js Normal file
View File

@ -0,0 +1,50 @@
import request from '@/utils/request'
// 获取奖励类型列表
export function getRewardTypeList(params) {
return request({
url: '/x/rewardType',
method: 'get',
params: {
page: params.page || 1,
size: params.size || 10,
name: params.name,
status: params.status,
storeId: params.storeId
}
})
}
// 新增奖励类型
export function addRewardType(data) {
return request({
url: '/x/rewardType',
method: 'post',
data
})
}
// 编辑奖励类型
export function updateRewardType(data) {
return request({
url: `/x/rewardType`,
method: 'put',
data
})
}
// 删除奖励类型
export function deleteRewardType(id) {
return request({
url: `/x/rewardType/${id}`,
method: 'delete'
})
}
// 获取门店列表
export function getStoreList() {
return request({
url: '/x/store/list',
method: 'get'
})
}

45
src/api/reward.js Normal file
View File

@ -0,0 +1,45 @@
import request from '@/utils/request'
// 获取任务列表
export function getRewardSystemList(params) {
return request({
url: '/x/reward',
method: 'get',
params
})
}
// 新增奖励
export function addReward(data) {
return request({
url: '/x/reward',
method: 'post',
data
})
}
// 编辑奖励
export function updateReward(data) {
return request({
url: '/x/reward',
method: 'put',
data
})
}
// 删除奖励
export function deleteReward(id) {
return request({
url: `/x/reward/${id}`,
method: 'delete'
})
}
// 上传图标
export function uploadReward(data) {
return request({
url: `/x/upload/reward`,
method: 'post',
data
})
}

10
src/api/store.js Normal file
View File

@ -0,0 +1,10 @@
import request from '@/utils/request'
// 获取门店列表
export function getStoreList(params) {
return request({
url: '/x/store',
method: 'get',
params
})
}

40
src/api/task.js Normal file
View File

@ -0,0 +1,40 @@
import request from '@/utils/request'
// 获取商户和门店列表
export function getMerchantAndStoreList() {
return request({
url: '/x/task/selector',
method: 'get'
})
}
// 获取任务列表
export function getTaskList({ netbarAccount, gid, num = 10, pageidx }) {
return request({
url: '/x/task/getNonLoginTaskList',
method: 'get',
params: {
netbarAccount,
gid,
num,
...(pageidx !== undefined ? { pageidx } : {})
}
})
}
/**
* 操作任务奖励
* @param {Object} data
* @param {number} data.type - 操作类型必填1=添加2=删除
* @param {number} data.taskId - 任务ID必填
* @param {number} data.rewardId - 奖励ID必填
* @returns {Promise<{success: boolean}>}
*/
export function operateTaskReward(data) {
return request({
url: '/x/reward/taskReward',
method: 'post',
data
})
}

View File

@ -9,23 +9,6 @@ export function userList(params) {
}) })
} }
// 删除用户
export function deleteUser(id) {
return request({
url: `/api/users/${id}`,
method: 'delete'
})
}
// 批量删除用户
export function batchDeleteUsers(ids) {
return request({
url: '/api/users/batch',
method: 'delete',
data: { ids }
})
}
// 更新用户状态 // 更新用户状态
export function updateUserStatus(id, status) { export function updateUserStatus(id, status) {
return request({ return request({

View File

@ -18,9 +18,9 @@ import '@/permission' // permission control
// set ElementUI lang to EN // set ElementUI lang to EN
Vue.use(ElementUI, { locale }) // Vue.use(ElementUI, { locale })
// 如果想要中文版 element-ui按如下方式声明 // 如果想要中文版 element-ui按如下方式声明
// Vue.use(ElementUI) Vue.use(ElementUI)
Vue.config.productionTip = false Vue.config.productionTip = false

View File

@ -70,6 +70,7 @@ export const constantRoutes = [
redirect: '/agent/merchant-list', redirect: '/agent/merchant-list',
name: 'AgentCenter', name: 'AgentCenter',
meta: { title: '代理商管理', icon: 'el-icon-user' }, meta: { title: '代理商管理', icon: 'el-icon-user' },
alwaysShow: true,
children: [ children: [
{ {
path: 'merchant-list', path: 'merchant-list',
@ -79,7 +80,55 @@ export const constantRoutes = [
} }
] ]
}, },
{
path: '/system',
component: Layout,
redirect: '/system/index',
name: 'SystemConfig',
meta: { title: '配置中心', icon: 'el-icon-setting' },
children: [
{
path: 'game-config',
name: 'GameConfig',
component: () => import('@/views/system/game.vue'),
meta: { title: '游戏配置' }
},
{
path: 'reward-type',
name: 'RewardType',
component: () => import('@/views/system/reward-type.vue'),
meta: { title: '奖励类型配置' }
},
{
path: 'reward-list',
name: 'RewardList',
component: () => import('@/views/system/reward-list.vue'),
meta: { title: '奖励配置' }
},
]
},
{
path: '/activity',
component: Layout,
redirect: '/activity/game-task',
name: 'ActivityCenter',
meta: { title: '活动中心', icon: 'el-icon-star-on' },
alwaysShow: true,
children: [
{
path: 'game-task',
name: 'GameTask',
component: () => import('@/views/activity/game-task'),
meta: { title: '游戏任务' }
},
{
path: 'test',
name: 'Test',
component: () => import('@/views/activity/test'),
meta: { title: '游戏任务' }
}
]
},
{ path: '*', redirect: '/404', hidden: true } { path: '*', redirect: '/404', hidden: true }
] ]

View File

@ -54,9 +54,14 @@ service.interceptors.response.use(
location.reload(); location.reload();
}); });
} }
if (res.code == 7) { if (res.code === 7) {
window.location = "/"; window.location = "/";
} }
if (res.code === 8) {
store.dispatch("user/resetToken").then(() => {
window.location = "/login";
});
}
} }
return res; return res;
}, },

View File

@ -0,0 +1,441 @@
<template>
<el-container>
<el-main>
<div class="game-task-page">
<!-- 二级菜单下拉框 -->
<div class="filter-container">
<el-cascader
v-model="selectedCategories"
:options="categoryOptions"
:props="cascaderProps"
placeholder="请选择商户"
style="width: 300px;"
@change="handleCategoryChange"
@click.native="handleCascaderClick"
/>
<el-button @click="handleReset" style="margin-left: 10px;">重置</el-button>
<el-button class="refresh-btn" @click="handleRefresh" style="margin-left: auto;">
<i class="el-icon-refresh"></i>
<span>刷新</span>
</el-button>
</div>
<el-tabs v-model="activeGame" @tab-click="handleGameChange">
<el-tab-pane
v-for="game in gameList"
:key="game.id"
:label="game.gameName"
:name="game.gameId.toString()"
/>
</el-tabs>
<!-- 任务列表表格 -->
<div class="task-table-container">
<el-table v-loading="loading" :data="taskList" style="width: 100%; max-width: 100%; table-layout: fixed; overflow-x: hidden;" border>
<el-table-column prop="qqNetbarTaskRules" label="任务规则" :resizable="false" align="center" header-align="center" min-width="16%" />
<el-table-column prop="qqNetbarTaskMemo" label="任务描述" :resizable="false" align="center" header-align="center" min-width="16%" />
<el-table-column prop="qqNetbarTaskName" label="任务名称" :resizable="false" align="center" header-align="center" min-width="16%" />
<el-table-column prop="qqNetbarReward" label="奖励名称" :resizable="false" align="center" header-align="center" min-width="16%">
<template slot-scope="scope">
<span>
{{
(scope.row.rewards && scope.row.rewards.filter(r => r.rewardTypeSource === 1).map(r => r.name).join('、')) || '-'
}}
</span>
</template>
</el-table-column>
<el-table-column prop="qqNetbarTargetTime" label="目标次数" :resizable="false" align="center" header-align="center" min-width="16%" />
<el-table-column label="操作" align="center" min-width="16%">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="configureReward(scope.row)">配置奖励</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<el-dialog
title="配置奖励"
:visible.sync="rewardTypeDialogVisible"
width="500px"
:close-on-click-modal="false"
>
<div class="reward-preview-list" style="margin-bottom: 16px;">
<div v-if="currentTask && currentTask.rewards && currentTask.rewards.filter(r => r.rewardTypeSource === 1).length" style="margin-bottom: 8px;">
<div v-for="reward in (currentTask.rewards || []).filter(r => r.rewardTypeSource === 1)" :key="reward.id" class="reward-preview-item" style="display: flex; align-items: center; margin-bottom: 6px;">
<img v-if="reward.imageUrl" :src="reward.imageUrl" alt="奖励图片" style="width: 40px; height: 40px; object-fit: contain; border: 1px solid #eee; border-radius: 6px; margin-right: 10px;" />
<span style="flex:1;">{{ reward.name }}类型{{ reward.rewardTypeName || '-' }}数量{{ reward.grantQuantity || '-' }}</span>
<el-button type="danger" size="mini" @click="handleDeleteReward(reward)">删除</el-button>
</div>
</div>
<div v-else style="color: #999; margin-bottom: 8px;">暂无已配置奖励</div>
</div>
<el-form label-width="100px">
<el-form-item label="奖励类型">
<el-select v-model="selectedRewardTypeId" placeholder="请选择奖励类型" filterable :loading="rewardTypeLoading" style="width: 100%" @change="handleRewardTypeChange">
<el-option v-for="item in rewardTypeList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="奖励">
<el-select v-model="selectedRewardId" placeholder="请选择奖励" filterable :loading="rewardListLoading" style="width: 100%">
<el-option v-for="item in rewardList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="rewardTypeDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleRewardTypeConfirm">确定</el-button>
</div>
</el-dialog>
</el-main>
</el-container>
</template>
<script>
import { getGameList } from '@/api/game'
import { getTaskList, getMerchantAndStoreList, setTaskReward, operateTaskReward } from '@/api/task'
import { getRewardTypeList } from '@/api/reward-type'
import { getRewardSystemList } from '@/api/reward'
export default {
name: 'GameTask',
data() {
return {
activeGame: '',
gameList: [],
loading: false,
// 二级菜单下拉框相关数据
selectedCategories: [],
categoryOptions: [],
cascaderProps: {
expandTrigger: 'hover'
},
// 任务列表相关数据
taskList: [],
taskLoading: false,
rewardTypeDialogVisible: false,
rewardTypeList: [],
rewardTypeLoading: false,
selectedRewardTypeId: null,
rewardList: [],
rewardListLoading: false,
selectedRewardId: null,
currentTask: null,
}
},
created() {
this.initPage()
},
methods: {
async initPage() {
// 先获取商户和门店数据并设置默认
await this.getMerchantAndStoreData(false)
// 再获取游戏列表并设置默认
await this.getGameList(false)
// 下拉框和游戏列表都加载完后,拉取任务列表
await this.getTaskList()
},
// 获取商户和门店数据
async getMerchantAndStoreData(autoGetTask = true) {
try {
const res = await getMerchantAndStoreList()
if (res.code === 0) {
this.categoryOptions = res.data.list.map(merchant => ({
value: merchant.id,
label: merchant.merchantName,
children: merchant.StoreDatas.map(store => ({
value: store.id,
label: store.storeName,
netbarAccount: store.netbarAccount
}))
}))
// 默认选中第一个商户和第一个门店
if (this.categoryOptions.length > 0 && this.categoryOptions[0].children && this.categoryOptions[0].children.length > 0) {
this.selectedCategories = [this.categoryOptions[0].value, this.categoryOptions[0].children[0].value]
if (autoGetTask) this.handleCategoryChange(this.selectedCategories)
}
} else {
this.$message.error(res.message || '获取商户和门店列表失败')
}
} catch (e) {
this.$message.error('获取商户和门店列表失败')
}
},
async getGameList(autoGetTask = true) {
this.loading = true
try {
const res = await getGameList({
page: 1,
size: 1000,
pageidx: ''
})
if (res.code === 0) {
this.gameList = res.data.list
if (this.gameList.length > 0) {
this.activeGame = this.gameList[0].gameId.toString()
}
} else {
this.$message.error(res.message || '获取游戏列表失败')
}
} catch (e) {
this.$message.error('请求失败')
} finally {
this.loading = false
}
},
// 分类选择改变
async handleCategoryChange(value) {
console.log('handleCategoryChange', value)
this.loading = true
await this.getTaskList()
this.loading = false
},
// 下拉框点击事件
async handleCascaderClick() {
await this.getMerchantAndStoreData(false)
},
handleReset() {
this.selectedCategories = []
},
async handleRefresh() {
this.loading = true
await this.getTaskList()
this.loading = false
},
handleGameChange(tab) {
// 更新游戏ID
this.activeGame = tab.name
// 切换游戏后,重新拉取任务列表
this.loading = true
this.getTaskList().finally(() => {
this.loading = false
})
},
handleSizeChange(size) {
},
handleCurrentChange(page) {
},
async getTaskList() {
this.loading = true
// 获取netbarAccount和gid
let netbarAccount = ''
let gid = ''
console.log('selectedCategories', this.selectedCategories)
if (this.selectedCategories.length === 2) {
const merchant = this.categoryOptions.find(m => m.value === this.selectedCategories[0])
if (merchant && merchant.children) {
const store = merchant.children.find(s => s.value === this.selectedCategories[1])
if (store) netbarAccount = store.netbarAccount || ''
}
}
if (this.activeGame) {
gid = this.activeGame
}
console.log('getTaskList params', { netbarAccount, gid })
if (!netbarAccount || !gid) {
this.loading = false
this.taskList = []
return
}
const res = await getTaskList({ netbarAccount, gid, num: 10, pageidx: '' })
console.log('getTaskList result', res)
if (res.code === 0) {
this.taskList = res.data.taskList || []
} else {
this.$message.error(res.message || '获取任务列表失败')
this.taskList = []
}
this.loading = false
},
async configureReward(row) {
// 只保留 rewardTypeSource === 1 的奖励
this.currentTask = {
...row,
rewards: (row.rewards || []).filter(r => r.rewardTypeSource === 1)
}
this.selectedRewardTypeId = null
this.selectedRewardId = null
this.rewardTypeDialogVisible = true
this.rewardTypeLoading = true
this.rewardList = []
this.rewardListLoading = false
// 获取奖励类型
getRewardTypeList({ page: 1, size: 1000 }).then(res => {
if (res.code === 0) {
this.rewardTypeList = res.data.list || []
} else {
this.$message.error(res.message || '获取奖励类型失败')
}
}).catch(() => {
this.$message.error('获取奖励类型失败')
}).finally(() => {
this.rewardTypeLoading = false
})
},
handleRewardTypeChange(val) {
this.selectedRewardId = null
this.rewardList = []
if (!val) return
this.rewardListLoading = true
getRewardSystemList({ rewardTypeId: val, page: 1, size: 100 }).then(res => {
if (res.code === 0) {
this.rewardList = res.data.list || []
} else {
this.$message.error(res.message || '获取奖励列表失败')
}
}).catch(() => {
this.$message.error('获取奖励列表失败')
}).finally(() => {
this.rewardListLoading = false
})
},
handleRewardTypeConfirm() {
if (!this.selectedRewardTypeId || !this.selectedRewardId || !this.currentTask) {
this.$message.error('请选择奖励类型和奖励')
return
}
const taskId = this.currentTask.qqNetbarTaskId || this.currentTask.id
operateTaskReward({
type: 1,
taskId,
rewardId: this.selectedRewardId
}).then(res => {
if (res.code === 0) {
this.$message.success('添加奖励成功')
// 前端追加并过滤,只保留 rewardTypeSource === 1
if (!this.currentTask.rewards) this.currentTask.rewards = []
const added = this.rewardList.find(r => r.id === this.selectedRewardId)
if (added && added.rewardTypeSource === 1) this.currentTask.rewards.push(added)
this.currentTask.rewards = this.currentTask.rewards.filter(r => r.rewardTypeSource === 1)
this.selectedRewardTypeId = null
this.selectedRewardId = null
this.rewardTypeDialogVisible = false
this.getTaskList()
} else {
this.$message.error(res.message || '添加奖励失败')
}
}).catch(() => {
this.$message.error('请求失败')
})
},
handleDeleteReward(reward) {
if (!this.currentTask) return
const taskId = this.currentTask.qqNetbarTaskId || this.currentTask.id
operateTaskReward({
type: 2,
taskId,
rewardId: reward.id
}).then(res => {
if (res.code === 0) {
this.$message.success('删除奖励成功')
// 前端移除,只保留 rewardTypeSource === 1
this.currentTask.rewards = (this.currentTask.rewards || []).filter(r => r.id !== reward.id && r.rewardTypeSource === 1)
} else {
this.$message.error(res.message || '删除奖励失败')
}
}).catch(() => {
this.$message.error('请求失败')
})
},
}
}
</script>
<style scoped>
.game-task-page {
background: #fff;
padding: 20px 24px 12px 24px;
border-radius: 10px;
min-height: 500px;
}
.filter-container {
margin-bottom: 20px;
display: flex;
align-items: center;
}
.task-table-container {
margin-top: 20px;
overflow-x: hidden;
position: relative;
}
/* 禁止el-table横向滚动body和header都不允许 */
.task-table-container .el-table__body-wrapper,
.task-table-container .el-table__header-wrapper,
.task-table-container .el-table__footer-wrapper {
overflow-x: hidden !important;
}
.task-table-container .el-table__body,
.task-table-container .el-table__header,
.task-table-container .el-table__footer {
overflow-x: hidden !important;
max-width: 100% !important;
}
.refresh-btn {
background: #f7f8fa;
border: 1px solid #e3e6f0;
border-radius: 6px;
height: 36px;
padding: 0 16px;
display: flex;
align-items: center;
gap: 4px;
transition: all 0.3s ease;
&:hover {
background: #556ff6;
color: #fff;
border-color: #556ff6;
i {
transform: rotate(180deg);
}
}
i {
font-size: 14px;
transition: transform 0.3s ease;
}
span {
font-size: 14px;
font-weight: 500;
}
}
.status-tag {
display: inline-block;
padding: 2px 16px;
border-radius: 16px;
font-size: 14px;
font-weight: 500;
background: #e8f7ea;
color: #3fc06d;
}
.status-block {
background: #fbeaea;
color: #f56c6c;
}
/* 强制彻底禁止el-table横向滚动 */
::v-deep .task-table-container .el-table__body-wrapper,
::v-deep .task-table-container .el-table__header-wrapper,
::v-deep .task-table-container .el-table__footer-wrapper,
::v-deep .task-table-container .el-table__body,
::v-deep .task-table-container .el-table__header,
::v-deep .task-table-container .el-table__footer {
overflow-x: hidden !important;
max-width: 100% !important;
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<div class="slider-center">
<div id="nc"></div>
</div>
</template>
<script>
export default {
name: 'AliyunSlider',
mounted() {
// 动态加载阿里云滑动验证码js
if (!window.AWSC) {
const script = document.createElement('script')
script.src = 'https://g.alicdn.com/AWSC/AWSC/awsc.js'
script.onload = this.initSlider
document.body.appendChild(script)
} else {
this.initSlider()
}
},
methods: {
initSlider() {
if (!window.AWSC) return
window.AWSC.use('nc', (state, module) => {
window.nc = module.init({
appkey: 'CF_APP_1',
scene: 'register',
renderTo: 'nc',
success: function (data) {
window.console && console.log(data.sessionId)
window.console && console.log(data.sig)
window.console && console.log(data.token)
},
fail: function (failCode) {
window.console && console.log(failCode)
},
error: function (errorCode) {
window.console && console.log(errorCode)
}
})
})
}
}
}
</script>
<style scoped>
.slider-center {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@ -11,8 +11,6 @@
<el-table-column prop="name" label="商户名称" header-align="center" align="center" /> <el-table-column prop="name" label="商户名称" header-align="center" align="center" />
<el-table-column prop="contactName" label="联系人" header-align="center" align="center" /> <el-table-column prop="contactName" label="联系人" header-align="center" align="center" />
<el-table-column prop="contactPhone" label="联系电话" header-align="center" align="center" /> <el-table-column prop="contactPhone" label="联系电话" header-align="center" align="center" />
<el-table-column prop="contactEmail" label="联系邮箱" header-align="center" align="center" />
<el-table-column prop="address" label="地址" header-align="center" align="center" />
<el-table-column prop="auditAt" label="通过时间" header-align="center" align="center" /> <el-table-column prop="auditAt" label="通过时间" header-align="center" align="center" />
<el-table-column prop="expireAt" label="服务到期时间" header-align="center" align="center" /> <el-table-column prop="expireAt" label="服务到期时间" header-align="center" align="center" />
<el-table-column prop="status" label="状态" header-align="center" align="center"> <el-table-column prop="status" label="状态" header-align="center" align="center">
@ -44,11 +42,134 @@
:merchant-data="currentMerchant" :merchant-data="currentMerchant"
@success="handleEditSuccess" @success="handleEditSuccess"
/> />
<!-- 门店列表弹窗 -->
<el-dialog
:title="currentMerchant.name + ' - 门店列表'"
:visible.sync="storeDialogVisible"
:close-on-click-modal="false"
class="store-dialog"
width="100%"
:fullscreen="true"
:show-close="false"
:append-to-body="true"
>
<div class="store-list-container">
<div class="store-list-header">
<div class="left">
<div class="back-btn" @click="handleBack">
<i class="el-icon-arrow-left"></i>
<span>返回</span>
</div>
</div>
<el-button type="primary" @click="handleAddStore">新增门店</el-button>
</div>
<el-table
v-loading="storeLoading"
:data="storeList"
border={false}
class="custom-table"
header-row-class-name="custom-header"
row-class-name="custom-row"
>
<el-table-column prop="name" label="门店名称" header-align="center" align="center" />
<el-table-column prop="storeCode" label="门店编号" header-align="center" align="center" />
<el-table-column prop="address" label="门店地址" header-align="center" align="center">
<template slot-scope="scope">
{{ scope.row.address || '-' }}
</template>
</el-table-column>
<el-table-column prop="contactName" label="联系人姓名" header-align="center" align="center">
<template slot-scope="scope">
{{ scope.row.contactName || '-' }}
</template>
</el-table-column>
<el-table-column prop="contactPhone" label="联系人电话" header-align="center" align="center">
<template slot-scope="scope">
{{ scope.row.contactPhone || '-' }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" header-align="center" align="center">
<template slot-scope="scope">
<span :class="['status-tag', scope.row.status === 1 ? 'status-normal' : 'status-block']">
{{ scope.row.status === 1 ? '营业中' : '已关闭' }}
</span>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" header-align="center" align="center">
<template slot-scope="scope">
{{ scope.row.createdAt || '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" header-align="center" align="center">
<template slot-scope="scope">
<el-button type="text" size="mini" class="edit-btn" @click="handleEditStore(scope.row)">编辑</el-button>
<el-button type="text" size="mini" class="delete-btn" @click="handleDeleteStore(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
background
layout="prev, pager, next"
:total="storeTotal"
:page-size="storeQuery.size"
:current-page="storeQuery.page"
@current-change="handleStorePageChange"
/>
</div>
</div>
</el-dialog>
<!-- 门店编辑弹窗 -->
<el-dialog
:title="storeDialogType === 'add' ? '新增门店' : '编辑门店'"
:visible.sync="storeEditDialogVisible"
width="500px"
:close-on-click-modal="false"
@closed="handleStoreDialogClosed"
class="store-edit-dialog"
>
<el-form
ref="storeForm"
:model="storeForm"
:rules="storeRules"
label-width="100px"
size="medium"
>
<el-form-item label="门店名称" prop="name">
<el-input v-model="storeForm.name" placeholder="请输入门店名称" />
</el-form-item>
<el-form-item label="门店编号" prop="storeCode" v-if="storeDialogType === 'edit'">
<el-input v-model="storeForm.storeCode" placeholder="请输入门店编号" disabled />
</el-form-item>
<el-form-item label="门店地址" prop="address">
<el-input v-model="storeForm.address" placeholder="请输入门店地址" />
</el-form-item>
<el-form-item label="联系人姓名" prop="contactName">
<el-input v-model="storeForm.contactName" placeholder="请输入联系人姓名" />
</el-form-item>
<el-form-item label="联系人电话" prop="contactPhone">
<el-input v-model="storeForm.contactPhone" placeholder="请输入联系人电话" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="storeForm.status">
<el-radio :label="1">营业中</el-radio>
<el-radio :label="0">已关闭</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="storeEditDialogVisible = false"> </el-button>
<el-button type="primary" :loading="storeSubmitLoading" @click="handleStoreSubmit"> </el-button>
</div>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import { merchantList } from '@/api/merchant' import { merchantList } from '@/api/merchant'
import { getStoreList } from '@/api/store'
import MerchantEdit from './merchant-edit.vue' import MerchantEdit from './merchant-edit.vue'
export default { export default {
@ -61,7 +182,52 @@ export default {
loading: false, loading: false,
tableData: [], tableData: [],
editDialogVisible: false, editDialogVisible: false,
currentMerchant: {} currentMerchant: {},
// 门店列表相关数据
storeDialogVisible: false,
storeEditDialogVisible: false,
storeLoading: false,
storeList: [],
storeTotal: 0,
storeQuery: {
page: 1,
size: 10,
merchantId: undefined
},
// 门店弹窗相关数据
storeDialogType: 'add', // add 或 edit
storeSubmitLoading: false,
storeForm: {
name: '',
storeCode: '',
address: '',
contactName: '',
contactPhone: '',
status: 1
},
storeRules: {
name: [
{ required: true, message: '请输入门店名称', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
storeCode: [
{ required: true, message: '请输入门店编号', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
address: [
{ required: true, message: '请输入门店地址', trigger: 'blur' }
],
contactName: [
{ required: true, message: '请输入联系人姓名', trigger: 'blur' }
],
contactPhone: [
{ required: true, message: '请输入联系人电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
status: [
{ required: true, message: '请选择状态', trigger: 'change' }
]
}
} }
}, },
created() { created() {
@ -90,8 +256,36 @@ export default {
this.editDialogVisible = true this.editDialogVisible = true
}, },
handleViewStores(row) { handleViewStores(row) {
// TODO: 实现查看门店功能 this.currentMerchant = row
this.$message.info('查看门店功能开发中') this.storeQuery.merchantId = row.id
this.storeDialogVisible = true
this.getStoreList()
},
handleBack() {
this.storeDialogVisible = false
this.storeList = []
this.storeTotal = 0
this.currentMerchant = {}
},
async getStoreList() {
this.storeLoading = true
try {
const res = await getStoreList(this.storeQuery)
if (res.code === 0) {
this.storeList = res.data.list
this.storeTotal = res.data.total
} else {
this.$message.error(res.message || '获取门店列表失败')
}
} catch (error) {
this.$message.error('获取门店列表失败')
} finally {
this.storeLoading = false
}
},
handleStorePageChange(page) {
this.storeQuery.page = page
this.getStoreList()
}, },
async handleToggleStatus(row) { async handleToggleStatus(row) {
try { try {
@ -110,6 +304,61 @@ export default {
}, },
handleEditSuccess() { handleEditSuccess() {
this.getList() this.getList()
},
handleAddStore() {
this.storeDialogType = 'add'
this.storeEditDialogVisible = true
},
handleEditStore(row) {
this.storeDialogType = 'edit'
this.storeForm = {
id: row.id,
name: row.name,
storeCode: row.storeCode,
address: row.address,
contactName: row.contactName,
contactPhone: row.contactPhone,
status: row.status
}
this.storeEditDialogVisible = true
},
handleDeleteStore(row) {
this.$confirm('确认删除该门店吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$message.info('删除门店功能待实现ID' + row.id)
}).catch(() => {})
},
handleStoreDialogClosed() {
this.$refs.storeForm && this.$refs.storeForm.resetFields()
this.storeForm = {
name: '',
storeCode: '',
address: '',
contactName: '',
contactPhone: '',
status: 1
}
},
handleStoreSubmit() {
this.$refs.storeForm.validate(async valid => {
if (!valid) return
this.storeSubmitLoading = true
try {
// TODO: 调用新增/编辑门店接口
const action = this.storeDialogType === 'add' ? '新增' : '编辑'
this.$message.success(`${action}成功`)
this.storeEditDialogVisible = false
this.getStoreList()
} catch (error) {
this.$message.error('操作失败')
} finally {
this.storeSubmitLoading = false
}
})
} }
} }
} }
@ -183,4 +432,91 @@ export default {
font-weight: 500; font-weight: 500;
margin-left: 8px; margin-left: 8px;
} }
.store-list-container {
padding: 20px;
height: 100%;
overflow-y: auto;
}
.store-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
background: #fff;
position: sticky;
top: 0;
z-index: 1;
.left {
display: flex;
align-items: center;
}
}
.back-btn {
display: flex;
align-items: center;
cursor: pointer;
font-size: 14px;
color: #303133;
padding: 8px 0;
i {
margin-right: 4px;
font-size: 16px;
}
&:hover {
color: #409EFF;
}
}
.merchant-name {
margin-left: 16px;
font-size: 16px;
font-weight: 500;
color: #303133;
}
.store-dialog {
::v-deep .el-dialog__header {
display: none;
}
::v-deep .el-dialog__body {
padding: 0;
height: 100%;
}
}
.store-edit-dialog {
::v-deep .el-dialog__body {
padding: 20px 30px;
}
::v-deep .el-form-item__label {
text-align: left;
}
::v-deep .el-input__inner {
text-align: left;
}
::v-deep .el-radio-group {
text-align: left;
}
}
.pagination-container {
margin-top: 20px;
text-align: right;
}
.dialog-footer {
text-align: right;
}
</style> </style>

View File

@ -11,8 +11,7 @@
<el-table-column prop="name" label="商户名称" header-align="center" align="center" /> <el-table-column prop="name" label="商户名称" header-align="center" align="center" />
<el-table-column prop="contactName" label="联系人" header-align="center" align="center" /> <el-table-column prop="contactName" label="联系人" header-align="center" align="center" />
<el-table-column prop="contactPhone" label="联系电话" header-align="center" align="center" /> <el-table-column prop="contactPhone" label="联系电话" header-align="center" align="center" />
<el-table-column prop="contactEmail" label="联系邮箱" header-align="center" align="center" /> <el-table-column prop="applicationReason" label="申请原因" header-align="center" align="center" />
<el-table-column prop="address" label="地址" header-align="center" align="center" />
<el-table-column prop="createdAt" label="申请时间" header-align="center" align="center" /> <el-table-column prop="createdAt" label="申请时间" header-align="center" align="center" />
<el-table-column prop="status" label="状态" header-align="center" align="center"> <el-table-column prop="status" label="状态" header-align="center" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
@ -66,12 +65,27 @@ export default {
async handleApprove(row) { async handleApprove(row) {
try { try {
await this.$confirm('确认通过该商户的审核吗?', '提示', { await this.$confirm('确认通过该商户的审核吗?', '提示', {
type: 'warning' type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消'
}) })
// 选择服务到期时间
const { value: serviceExpire } = await this.$prompt(
'请选择服务到期时间',
'服务到期时间',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
inputType: 'date',
inputPattern: /.*/, // 允许为空
inputValue: ''
}
)
const res = await merchantAudit({ const res = await merchantAudit({
id: row.id, id: row.id,
auditStatus: 2, auditStatus: 2,
auditRemark: '审核通过' auditRemark: '审核通过',
serviceExpire
}) })
if (res.code === 0) { if (res.code === 0) {
this.$message.success('审核通过成功') this.$message.success('审核通过成功')

View File

@ -11,8 +11,6 @@
<el-table-column prop="name" label="商户名称" header-align="center" align="center" /> <el-table-column prop="name" label="商户名称" header-align="center" align="center" />
<el-table-column prop="contactName" label="联系人" header-align="center" align="center" /> <el-table-column prop="contactName" label="联系人" header-align="center" align="center" />
<el-table-column prop="contactPhone" label="联系电话" header-align="center" align="center" /> <el-table-column prop="contactPhone" label="联系电话" header-align="center" align="center" />
<el-table-column prop="contactEmail" label="联系邮箱" header-align="center" align="center" />
<el-table-column prop="address" label="地址" header-align="center" align="center" />
<el-table-column prop="auditAt" label="拒绝时间" header-align="center" align="center" /> <el-table-column prop="auditAt" label="拒绝时间" header-align="center" align="center" />
<el-table-column prop="rejectReason" label="拒绝原因" header-align="center" align="center" /> <el-table-column prop="rejectReason" label="拒绝原因" header-align="center" align="center" />
<el-table-column prop="status" label="状态" header-align="center" align="center"> <el-table-column prop="status" label="状态" header-align="center" align="center">

View File

@ -91,6 +91,8 @@ export default {
padding: 20px; padding: 20px;
background: #fff; background: #fff;
border-radius: 4px; border-radius: 4px;
position: relative;
min-height: 100vh;
} }
.search-area { .search-area {
margin-bottom: 20px; margin-bottom: 20px;
@ -120,4 +122,29 @@ export default {
padding: 20px; padding: 20px;
border-radius: 4px; border-radius: 4px;
} }
::v-deep .el-dialog {
margin: 0 !important;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100% !important;
height: 100%;
border-radius: 0;
.el-dialog__header {
padding: 20px;
border-bottom: 1px solid #e4e7ed;
}
.el-dialog__body {
height: calc(100% - 120px);
padding: 20px;
overflow-y: auto;
}
.el-dialog__footer {
padding: 20px;
border-top: 1px solid #e4e7ed;
}
}
</style> </style>

View File

508
src/views/system/game.vue Normal file
View File

@ -0,0 +1,508 @@
<template>
<el-container>
<el-main>
<div class="game-config-page">
<div class="header-row">
<div class="search-form">
<el-form :inline="true" :model="query" size="small">
<el-form-item label="游戏名称">
<el-input v-model="query.gameName" placeholder="请输入游戏名称" clearable @keyup.enter.native="handleSearch" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="action-buttons">
<el-button class="refresh-btn" @click="getList">
<i class="el-icon-refresh"></i>
<span>刷新</span>
</el-button>
<el-button type="primary" @click="addDialogVisible = true">新增游戏</el-button>
</div>
</div>
<el-table :data="list" style="width: 100%" v-loading="loading" size="small" :row-class-name="'game-table-row'"
:cell-style="{ padding: '12px 8px' }">
<el-table-column label="游戏图标" width="80" align="center">
<template slot-scope="scope">
<el-avatar v-if="scope.row.avatar" :src="scope.row.avatar" size="small" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="gameName" label="游戏名称" min-width="120" align="center">
<template slot-scope="scope">
<el-tooltip v-if="scope.row.gameName && scope.row.gameName.length > 20" :content="scope.row.gameName" placement="top">
<span>{{ scope.row.gameName.slice(0, 20) + '...' }}</span>
</el-tooltip>
<span v-else>{{ scope.row.gameName }}</span>
</template>
</el-table-column>
<el-table-column prop="gameCode" label="游戏编码" min-width="120" align="center">
<template slot-scope="scope">
<el-tooltip v-if="scope.row.gameCode && scope.row.gameCode.length > 20" :content="scope.row.gameCode" placement="top">
<span>{{ scope.row.gameCode.slice(0, 20) + '...' }}</span>
</el-tooltip>
<span v-else>{{ scope.row.gameCode }}</span>
</template>
</el-table-column>
<el-table-column prop="gameId" label="游戏ID" width="100" align="center">
<template slot-scope="scope">
<el-tooltip v-if="String(scope.row.gameId).length > 20" :content="scope.row.gameId" placement="top">
<span>{{ String(scope.row.gameId).slice(0, 20) + '...' }}</span>
</el-tooltip>
<span v-else>{{ scope.row.gameId }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="160" align="center">
<template slot-scope="scope">
<div class="action-buttons">
<el-button type="primary" size="mini" class="action-btn edit-btn" @click="onEdit(scope.row)">
<i class="el-icon-edit"></i>
编辑
</el-button>
<el-button type="danger" size="mini" class="action-btn delete-btn" @click="onDelete(scope.row)">
<i class="el-icon-delete"></i>
删除
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div class="table-footer">
<el-pagination
background
layout="prev, pager, next"
:total="total"
:page-size="query.size"
:current-page="query.page"
@current-change="handlePageChange"
class="custom-pagination"
/>
</div>
<el-dialog title="新增游戏" :visible.sync="addDialogVisible" width="400px" :close-on-click-modal="false">
<el-form :model="addForm" label-width="100px" label-position="left" class="add-form">
<el-form-item label="游戏ID" required>
<el-input v-model="addForm.gameId" placeholder="请输入游戏ID" maxlength="20" />
</el-form-item>
<el-form-item label="游戏名称" required>
<el-input v-model="addForm.gameName" placeholder="请输入游戏名称" maxlength="20" />
</el-form-item>
<el-form-item label="游戏编码" required>
<el-input v-model="addForm.gameCode" placeholder="请输入游戏编码" maxlength="20" />
</el-form-item>
<el-form-item label="绑定类型" required>
<el-select v-model="addForm.boundType" placeholder="请选择绑定类型" style="width: 100%">
<el-option label="QQ" :value="1"></el-option>
<el-option label="微信" :value="2"></el-option>
<el-option label="全部" :value="3"></el-option>
</el-select>
</el-form-item>
<el-form-item label="游戏图标" required>
<el-upload
class="avatar-uploader"
action="/x/upload/game"
:show-file-list="false"
:http-request="handleGameImgRequest"
:before-upload="beforeAvatarUpload"
>
<img v-if="addForm.avatar" :src="addForm.avatar" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="addDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleAdd">确定</el-button>
</div>
</el-dialog>
<el-dialog title="编辑游戏" :visible.sync="editDialogVisible" width="400px" :close-on-click-modal="false">
<el-form :model="editForm" label-width="100px" label-position="left" class="add-form">
<el-form-item label="游戏ID" required>
<el-input v-model="editForm.gameId" placeholder="请输入游戏ID" maxlength="20" />
</el-form-item>
<el-form-item label="游戏名称" required>
<el-input v-model="editForm.gameName" placeholder="请输入游戏名称" maxlength="20" />
</el-form-item>
<el-form-item label="游戏编码" required>
<el-input v-model="editForm.gameCode" placeholder="请输入游戏编码" maxlength="20" />
</el-form-item>
<el-form-item label="绑定类型" required>
<el-select v-model="editForm.boundType" placeholder="请选择绑定类型" style="width: 100%">
<el-option label="QQ" :value="1"></el-option>
<el-option label="微信" :value="2"></el-option>
<el-option label="全部" :value="3"></el-option>
</el-select>
</el-form-item>
<el-form-item label="游戏图标" required>
<el-upload
class="avatar-uploader"
action="/x/upload/game"
:show-file-list="false"
:http-request="handleEditGameImgRequest"
:before-upload="beforeAvatarUpload"
>
<img v-if="editForm.avatar" :src="editForm.avatar" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleEdit">保存</el-button>
</div>
</el-dialog>
</div>
</el-main>
</el-container>
</template>
<script>
import { getGameList, addGame, editGame, deleteGame, uploadGameImg } from '@/api/game'
export default {
name: 'GameConfig',
data() {
return {
list: [],
total: 0,
loading: false,
query: {
page: 1,
size: 10,
gameName: ''
},
addDialogVisible: false,
addForm: {
gameId: '',
gameName: '',
gameCode: '',
boundType: '',
avatar: ''
},
editDialogVisible: false,
editForm: {
id: '',
gameId: '',
gameName: '',
gameCode: '',
boundType: '',
avatar: ''
},
editOrigin: null
}
},
created() {
this.getList()
},
methods: {
async getList() {
this.loading = true
try {
const res = await getGameList(this.query)
if (res.code === 0) {
this.list = res.data.list
this.total = res.data.total
} else {
this.$message.error(res.message || '获取游戏列表失败')
}
} catch (e) {
this.$message.error('请求失败')
} finally {
this.loading = false
}
},
handlePageChange(page) {
this.query.page = page
this.getList()
},
handleAvatarSuccess(res) {
this.addForm.avatar = res.url
},
handleGameImgRequest(option) {
const formData = new FormData()
formData.append('file', option.file)
uploadGameImg(formData).then(res => {
if (res && res.code === 0 && res.data && res.data.url) {
this.addForm.avatar = res.data.url
this.$message.success('上传成功')
option.onSuccess(res)
} else {
this.$message.error(res.message || '上传失败')
option.onError(new Error(res.message || '上传失败'))
}
}).catch(err => {
this.$message.error('上传失败')
option.onError(err)
})
},
beforeAvatarUpload(file) {
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) {
this.$message.error('只能上传图片文件!')
}
if (!isLt2M) {
this.$message.error('图片大小不能超过2MB')
}
return isImage && isLt2M
},
async handleAdd() {
if (!this.addForm.gameId || !this.addForm.gameName || !this.addForm.gameCode || !this.addForm.boundType) {
this.$message.warning('请填写完整信息')
return
}
try {
const res = await addGame(this.addForm)
if (res.code === 0 && res.data.success) {
this.$message.success('新增成功')
this.addDialogVisible = false
this.addForm = { gameId: '', gameName: '', gameCode: '', boundType: '', avatar: '' }
this.getList()
} else {
this.$message.error(res.message || '新增失败')
}
} catch (e) {
this.$message.error('请求失败')
}
},
onDetail(row) {
this.$message.info('详情功能待实现ID' + row.gameId)
},
onEdit(row) {
this.editOrigin = { ...row }
this.editForm = { ...row }
this.editDialogVisible = true
},
handleEditAvatarSuccess(res) {
this.editForm.avatar = res.url
},
handleEditGameImgRequest(option) {
const formData = new FormData()
formData.append('file', option.file)
uploadGameImg(formData).then(res => {
if (res && res.code === 0 && res.data && res.data.url) {
this.editForm.avatar = res.data.url
this.$message.success('上传成功')
option.onSuccess(res)
} else {
this.$message.error(res.message || '上传失败')
option.onError(new Error(res.message || '上传失败'))
}
}).catch(err => {
this.$message.error('上传失败')
option.onError(err)
})
},
async handleEdit() {
if (!this.editForm.gameId || !this.editForm.gameName || !this.editForm.gameCode || !this.editForm.boundType) {
this.$message.warning('请填写完整信息')
return
}
const compareFields = ['gameId', 'gameName', 'gameCode']
const submitData = { id: this.editForm.id }
compareFields.forEach(key => {
const oldVal = this.editOrigin[key]
const newVal = this.editForm[key]
if (newVal !== oldVal) {
submitData[key] = newVal
}
})
submitData.boundType = this.editForm.boundType
submitData.avatar = this.editForm.avatar
try {
const res = await editGame(submitData)
if (res.code === 0 && res.data.success) {
this.$message.success('编辑成功')
this.editDialogVisible = false
this.getList()
} else {
this.$message.error(res.message || '编辑失败')
}
} catch (e) {
this.$message.error('请求失败')
}
},
onDelete(row) {
this.$confirm('确认删除该游戏吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteGame(row.id)
if (res.code === 0 && res.data.success) {
this.$message.success('删除成功')
this.getList()
} else {
this.$message.error(res.message || '删除失败')
}
} catch (e) {
this.$message.error('请求失败')
}
}).catch(() => {})
},
handleSearch() {
this.query.page = 1
this.getList()
},
handleReset() {
this.query = {
page: 1,
size: 10,
gameName: ''
}
this.getList()
}
},
editGame
}
</script>
<style scoped>
.game-config-page {
background: #fff;
padding: 20px 24px 12px 24px;
border-radius: 10px;
min-height: 500px;
}
.header-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 10px;
}
.search-form {
flex: 1;
margin-right: 16px;
}
.action-buttons {
display: flex;
justify-content: center;
gap: 8px;
}
.refresh-btn {
background: #f7f8fa;
border: 1px solid #e3e6f0;
border-radius: 6px;
height: 40px;
padding: 0 20px;
display: flex;
align-items: center;
gap: 4px;
transition: all 0.3s ease;
font-size: 14px;
line-height: 40px;
&:hover {
background: #556ff6;
color: #fff;
border-color: #556ff6;
i {
transform: rotate(180deg);
}
}
i {
font-size: 14px;
transition: transform 0.3s ease;
}
span {
font-size: 14px;
font-weight: 500;
}
}
.el-table {
margin-bottom: 8px;
}
.game-table-row td {
padding-top: 8px !important;
padding-bottom: 8px !important;
font-size: 14px;
text-align: center !important;
}
.el-table .cell {
padding-left: 8px;
padding-right: 8px;
text-align: center !important;
}
.table-footer {
margin-top: 8px;
text-align: right;
}
.custom-pagination {
margin: 0;
}
.add-form .el-form-item {
margin-bottom: 16px;
}
.add-form .el-input {
font-size: 14px;
width: 100%;
}
.add-form .el-form-item__content {
width: calc(100% - 100px);
}
.add-form .el-form-item__label {
text-align: left;
padding-right: 12px;
font-weight: normal;
width: 100px !important;
}
.add-form .el-form-item__label::before {
content: '*';
color: transparent;
margin-right: 4px;
}
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 80px;
height: 80px;
line-height: 80px;
text-align: center;
}
.avatar {
width: 80px;
height: 80px;
display: block;
}
.action-btn {
padding: 6px 12px;
border-radius: 4px;
font-size: 13px;
display: inline-flex;
align-items: center;
gap: 4px;
i {
font-size: 14px;
}
&.edit-btn {
background: #556ff6;
border-color: #556ff6;
&:hover {
background: #3d4fd8;
border-color: #3d4fd8;
}
}
&.delete-btn {
background: #f56c6c;
border-color: #f56c6c;
&:hover {
background: #e64242;
border-color: #e64242;
}
}
}
</style>

View File

@ -0,0 +1,597 @@
<template>
<div class="reward-list-container">
<div class="game-task-page">
<div class="header-row">
<div class="search-form">
<el-form :inline="true" :model="query" size="small">
<el-form-item label="奖励名称" label-width="120px">
<el-input v-model="query.name" placeholder="请输入奖励名称" clearable @keyup.enter.native="handleSearch" />
</el-form-item>
<el-form-item label="状态" label-width="120px">
<el-select v-model="query.status" placeholder="请选择状态" clearable>
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="2" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="action-buttons">
<el-button class="refresh-btn" @click="getTaskList">
<i class="el-icon-refresh"></i>
<span>刷新</span>
</el-button>
<el-button type="primary" @click="handleAddReward">添加奖励</el-button>
</div>
</div>
<div class="table-container">
<el-table :data="taskList" style="width: 100%" v-loading="loading" size="small" :row-class-name="'task-table-row'"
:cell-style="{ padding: '12px 8px' }">
<el-table-column prop="name" label="奖励名称" min-width="120" align="center" />
<el-table-column prop="rewardTypeName" label="奖励类型" min-width="120" align="center">
<template slot-scope="scope">
<span>{{ scope.row.rewardTypeName || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'info'">
{{ scope.row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="expireType" label="过期类型" width="100" align="center">
<template slot-scope="scope">
<span v-if="scope.row.expireType === 1">时间范围</span>
<span v-else-if="scope.row.expireType === 2">固定时间</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="有效期" min-width="180" align="center">
<template slot-scope="scope">
<span v-if="scope.row.expireType === 1">
{{ formatDate(scope.row.validFrom) || '-' }} ~ {{ formatDate(scope.row.validTo) || '-' }}
</span>
<span v-else-if="scope.row.expireType === 2">
领取后{{ scope.row.expireDays || '-' }}天内有效
</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="dailyTotalLimit" label="每日总限量" min-width="100" align="center" />
<el-table-column prop="totalLimit" label="总限量" min-width="100" align="center" />
<el-table-column prop="userDailyLimit" label="用户每日限量" min-width="120" align="center" />
<el-table-column prop="userTotalLimit" label="用户总限量" min-width="120" align="center" />
<el-table-column prop="receivedNum" label="已领取数量" min-width="100" align="center" />
<el-table-column prop="grantQuantity" label="发放数量" min-width="100" align="center" />
<el-table-column label="操作" width="180" align="center">
<template slot-scope="scope">
<div class="action-buttons">
<el-button type="primary" size="mini" class="action-btn edit-btn" @click="onEdit(scope.row)">
<i class="el-icon-edit"></i>
编辑
</el-button>
<el-button type="danger" size="mini" class="action-btn delete-btn" @click="onDelete(scope.row)">
<i class="el-icon-delete"></i>
删除
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div class="table-footer">
<el-pagination
background
layout="prev, pager, next"
:total="total"
:page-size="query.size"
:current-page="query.page"
@current-change="handlePageChange"
class="custom-pagination"
/>
</div>
</div>
</div>
<!-- 新增/编辑奖励弹窗 -->
<el-dialog
:title="dialogType === 'add' ? '新增奖励' : '编辑奖励'"
:visible.sync="addRewardDialogVisible"
width="600px"
@close="resetAddRewardForm"
>
<el-form
ref="addRewardForm"
:model="addRewardForm"
:rules="addRewardRules"
label-width="120px"
class="add-reward-form"
>
<el-form-item label="奖励名称" prop="name" label-width="120px">
<el-input v-model="addRewardForm.name" placeholder="请输入奖励名称" />
</el-form-item>
<el-form-item label="奖励类型" prop="rewardTypeId" label-width="120px">
<el-select v-model="addRewardForm.rewardTypeId" placeholder="请选择奖励类型" filterable style="width: 100%">
<el-option v-for="item in rewardTypeList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="游戏ID" prop="gameId" label-width="120px">
<el-input-number v-model="addRewardForm.gameId" :min="1" style="width: 100%" />
</el-form-item>
<el-form-item label="奖励图片" prop="rewardImg" label-width="120px">
<el-upload
class="reward-img-uploader"
action="/x/upload/reward"
:show-file-list="false"
:http-request="handleRewardImgRequest"
:before-upload="beforeRewardImgUpload"
>
<img v-if="addRewardForm.rewardImg" :src="addRewardForm.rewardImg" class="reward-img-preview" style="width: 80px; height: 80px; object-fit: contain; border: 1px solid #eee; border-radius: 6px;" />
<i v-else class="el-icon-plus reward-img-upload-icon" style="font-size: 32px; color: #bbb; width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; border: 1px dashed #d9d9d9; border-radius: 6px;" />
</el-upload>
</el-form-item>
<el-form-item label="QQ网吧物品ID" prop="qqGoodsId" label-width="120px">
<el-input v-model="addRewardForm.qqGoodsId" placeholder="请输入QQ网吧物品ID" />
</el-form-item>
<el-form-item label="QQ网吧物品ID字符串" prop="qqGoodsIdStr" label-width="120px">
<el-input v-model="addRewardForm.qqGoodsIdStr" placeholder="请输入QQ网吧物品ID字符串" />
</el-form-item>
<el-form-item label="状态" prop="status" label-width="120px">
<el-select v-model="addRewardForm.status" placeholder="请选择状态" style="width: 100%">
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="过期类型" prop="expireType" label-width="120px">
<el-select v-model="addRewardForm.expireType" placeholder="请选择过期类型" style="width: 100%">
<el-option label="时间范围" :value="1" />
<el-option label="固定时间" :value="2" />
</el-select>
</el-form-item>
<el-form-item v-if="addRewardForm.expireType === 1" label="有效开始时间" prop="validFrom" label-width="120px">
<el-date-picker v-model="addRewardForm.validFrom" type="datetime" placeholder="请选择开始时间" style="width: 100%" />
</el-form-item>
<el-form-item v-if="addRewardForm.expireType === 1" label="有效结束时间" prop="validTo" label-width="120px">
<el-date-picker v-model="addRewardForm.validTo" type="datetime" placeholder="请选择结束时间" style="width: 100%" />
</el-form-item>
<el-form-item v-if="addRewardForm.expireType === 2" label="领取后过期天数" prop="expireDays" label-width="120px">
<div style="display: flex; align-items: center;">
<el-input-number v-model="addRewardForm.expireDays" :min="1" placeholder="请输入天数" style="width: 180px;" />
<span style="margin-left: 8px;"></span>
</div>
</el-form-item>
<el-form-item label="每日总限量" prop="dailyTotalLimit" label-width="120px">
<el-input-number v-model="addRewardForm.dailyTotalLimit" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item label="总限量" prop="totalLimit" label-width="120px">
<el-input-number v-model="addRewardForm.totalLimit" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item label="用户每日限量" prop="userDailyLimit" label-width="120px">
<el-input-number v-model="addRewardForm.userDailyLimit" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item label="用户总限量" prop="userTotalLimit" label-width="120px">
<el-input-number v-model="addRewardForm.userTotalLimit" :min="0" style="width: 100%" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="addRewardDialogVisible = false"> </el-button>
<el-button type="primary" @click="submitAddReward"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getRewardSystemList, addReward, updateReward, deleteReward } from '@/api/reward'
import { getRewardTypeList } from '@/api/reward-type'
import { uploadReward } from '@/api/reward'
export default {
name: 'GameTask',
created() {
this.getTaskList()
},
data() {
return {
taskList: [],
total: 0,
loading: false,
query: {
page: 1,
size: 10,
name: '',
status: undefined,
rewardTypeId: undefined
},
addRewardDialogVisible: false,
rewardTypeList: [],
dialogType: 'add',
addRewardForm: {
id: undefined,
name: '',
rewardTypeId: undefined,
gameId: undefined,
rewardImg: '',
qqGoodsId: '',
qqGoodsIdStr: '',
status: 1,
expireType: undefined,
validFrom: '',
validTo: '',
expireDays: undefined,
dailyTotalLimit: undefined,
totalLimit: undefined,
userDailyLimit: undefined,
userTotalLimit: undefined,
source: 1
},
addRewardRules: {
rewardTypeId: [{ required: true, message: '请选择奖励类型', trigger: 'change' }],
name: [{ required: true, message: '请输入奖励名称', trigger: 'blur' }],
description: [{ required: true, message: '请输入奖励描述', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
value: [{ required: true, message: '请输入奖励值', trigger: 'blur' }],
expireType: [{ required: true, message: '请选择过期类型', trigger: 'change' }],
validFrom: [
{ required: true, message: '请选择有效开始时间', trigger: 'change',
validator: (rule, value, callback, source) => {
if (source.expireType === 1 && !value) callback(new Error('请选择有效开始时间'));
else callback();
}
}
],
validTo: [
{ required: true, message: '请选择有效结束时间', trigger: 'change',
validator: (rule, value, callback, source) => {
if (source.expireType === 1 && !value) callback(new Error('请选择有效结束时间'));
else callback();
}
}
],
expireDays: [
{ required: true, message: '请输入天数', trigger: 'blur',
validator: (rule, value, callback, source) => {
if (source.expireType === 2 && (!value || value < 1)) callback(new Error('请输入天数'));
else callback();
}
}
]
}
}
},
methods: {
handleSearch() {
this.query.page = 1
this.getTaskList()
},
handleReset() {
this.query = {
page: 1,
size: 10,
name: '',
status: undefined,
rewardTypeId: undefined
}
this.getTaskList()
},
async getTaskList() {
this.loading = true
try {
const res = await getRewardSystemList(this.query)
if (res.code === 0) {
this.taskList = res.data.list
this.total = res.data.total
} else {
this.$message.error(res.message || '获取任务列表失败')
}
} catch (e) {
this.$message.error('请求失败')
} finally {
this.loading = false
}
},
handlePageChange(page) {
this.query.page = page
this.getTaskList()
},
onDelete(row) {
this.$confirm('确认删除该奖励吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteReward(row.id)
if (res.code === 0) {
this.$message.success('删除成功')
this.getTaskList()
} else {
this.$message.error(res.message || '删除失败')
}
} catch (e) {
this.$message.error('删除失败')
}
}).catch(() => {})
},
formatDate(date) {
if (!date) return '';
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
const seconds = String(d.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
handleAddReward() {
this.dialogType = 'add'
this.addRewardDialogVisible = true
this.getRewardTypeList()
},
async getRewardTypeList() {
try {
const res = await getRewardTypeList({
page: 1,
size: 1000
})
if (res.code === 0) {
this.rewardTypeList = res.data.list || []
} else {
this.$message.error(res.message || '获取奖励类型列表失败')
}
} catch (e) {
this.$message.error('获取奖励类型列表失败')
}
},
onEdit(row) {
this.dialogType = 'edit'
this.addRewardForm = {
id: row.id,
name: row.name,
rewardTypeId: row.rewardTypeId,
gameId: row.gameId,
rewardImg: row.rewardImg || row.imageUrl || '',
qqGoodsId: row.qqGoodsId,
qqGoodsIdStr: row.qqGoodsIdStr,
status: row.status,
expireType: row.expireType || 1,
validFrom: row.validFrom || row.startTime || '',
validTo: row.validTo || row.endTime || '',
expireDays: row.expireDays || '',
dailyTotalLimit: row.dailyTotalLimit,
totalLimit: row.totalLimit,
userDailyLimit: row.userDailyLimit,
userTotalLimit: row.userTotalLimit,
source: 1
}
if (this.addRewardForm.expireType === 1) {
this.addRewardForm.expireDays = ''
} else if (this.addRewardForm.expireType === 2) {
this.addRewardForm.validFrom = ''
this.addRewardForm.validTo = ''
}
this.addRewardDialogVisible = true
this.getRewardTypeList()
},
async submitAddReward() {
try {
await this.$refs.addRewardForm.validate()
const api = this.dialogType === 'add' ? addReward : updateReward
const formData = { ...this.addRewardForm, source: 1 }
const res = await api(formData)
if (res.code === 0) {
this.$message.success(this.dialogType === 'add' ? '添加奖励成功' : '编辑奖励成功')
this.addRewardDialogVisible = false
this.getTaskList()
} else {
this.$message.error(res.message || (this.dialogType === 'add' ? '添加奖励失败' : '编辑奖励失败'))
}
} catch (e) {
if (e === false) {
this.$message.error('请完善表单信息')
} else {
this.$message.error('请求失败')
}
}
},
resetAddRewardForm() {
this.addRewardForm = {
id: undefined,
name: '',
rewardTypeId: undefined,
gameId: undefined,
rewardImg: '',
qqGoodsId: '',
qqGoodsIdStr: '',
status: 1,
expireType: undefined,
validFrom: '',
validTo: '',
expireDays: undefined,
dailyTotalLimit: undefined,
totalLimit: undefined,
userDailyLimit: undefined,
userTotalLimit: undefined,
source: 1
}
},
handleRewardImgRequest(option) {
const formData = new FormData()
formData.append('file', option.file)
uploadReward(formData).then(res => {
if (res && res.code === 0 && res.data && res.data.url) {
this.addRewardForm.rewardImg = res.data.url
this.$message.success('上传成功')
option.onSuccess(res)
} else {
this.$message.error(res.message || '上传失败')
option.onError(new Error(res.message || '上传失败'))
}
}).catch(err => {
this.$message.error('上传失败')
option.onError(err)
})
},
beforeRewardImgUpload(file) {
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) {
this.$message.error('只能上传图片文件!')
}
if (!isLt2M) {
this.$message.error('图片大小不能超过 2MB!')
}
return isImage && isLt2M
}
}
}
</script>
<style scoped>
.game-task-page {
background: #fff;
padding: 20px 24px 12px 24px;
border-radius: 10px;
min-height: 500px;
}
.header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.search-form {
flex: 1;
margin-right: 16px;
}
.search-form .el-form {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.search-form .el-form-item {
margin-bottom: 0;
margin-right: 0;
}
.action-buttons {
display: flex;
align-items: center;
gap: 8px;
}
.refresh-btn {
background: #f7f8fa;
border: 1px solid #e3e6f0;
border-radius: 6px;
height: 40px;
padding: 0 20px;
display: flex;
align-items: center;
gap: 4px;
transition: all 0.3s ease;
font-size: 14px;
line-height: 40px;
&:hover {
background: #556ff6;
color: #fff;
border-color: #556ff6;
i {
transform: rotate(180deg);
}
}
i {
font-size: 14px;
transition: transform 0.3s ease;
}
span {
font-size: 14px;
font-weight: 500;
}
}
.el-table {
margin-bottom: 8px;
}
.task-table-row td {
padding-top: 8px !important;
padding-bottom: 8px !important;
font-size: 14px;
text-align: center !important;
}
.el-table .cell {
padding-left: 8px;
padding-right: 8px;
text-align: center !important;
}
.table-footer {
margin-top: 8px;
text-align: right;
}
.custom-pagination {
margin: 0;
}
.action-buttons {
display: flex;
justify-content: center;
gap: 8px;
}
.action-btn {
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
display: inline-flex;
align-items: center;
gap: 4px;
transition: all 0.3s ease;
i {
font-size: 14px;
}
&.edit-btn {
background: #556ff6;
border-color: #556ff6;
&:hover {
background: #3d4fd8;
border-color: #3d4fd8;
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(85, 111, 246, 0.2);
}
}
&.delete-btn {
background: #f56c6c;
border-color: #f56c6c;
&:hover {
background: #e64242;
border-color: #e64242;
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(245, 108, 108, 0.2);
}
}
}
.add-reward-form {
padding: 0 20px;
}
.add-reward-form .el-form-item {
margin-bottom: 22px;
}
.add-reward-form .el-input,
.add-reward-form .el-select,
.add-reward-form .el-date-picker,
.add-reward-form .el-input-number {
width: 100%;
}
.add-reward-form .el-form-item__label {
text-align: left;
}
</style>

View File

@ -0,0 +1,379 @@
<template>
<el-container>
<el-main>
<div class="reward-type-page">
<div class="header-row">
<div class="search-form">
<el-form :inline="true" :model="query" size="small">
<el-form-item label="名称">
<el-input v-model="query.name" placeholder="请输入名称" clearable @keyup.enter.native="handleSearch" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="action-buttons">
<el-button class="refresh-btn" @click="getList">
<i class="el-icon-refresh"></i>
<span>刷新</span>
</el-button>
<el-button class="add-btn" @click="dialogVisible = true">
<i class="el-icon-plus"></i>
<span>新增奖励类型</span>
</el-button>
</div>
</div>
<el-table :data="list" style="width: 100%" v-loading="loading" size="small" :row-class-name="'reward-table-row'"
:cell-style="{ padding: '12px 8px' }">
<el-table-column prop="name" label="名称" min-width="120" align="center" />
<el-table-column prop="tencentTypeId" label="腾讯奖励类型ID" min-width="120" align="center" />
<el-table-column label="操作" width="160" align="center">
<template slot-scope="scope">
<template v-if="!scope.row.storeName">
<div class="action-buttons">
<el-button type="primary" size="mini" class="action-btn edit-btn" @click="onEdit(scope.row)">
<i class="el-icon-edit"></i>
编辑
</el-button>
<el-button type="danger" size="mini" class="action-btn delete-btn" @click="onDelete(scope.row)">
<i class="el-icon-delete"></i>
删除
</el-button>
</div>
</template>
<span v-else>-</span>
</template>
</el-table-column>
</el-table>
<div class="table-footer">
<el-pagination
background
layout="prev, pager, next"
:total="total"
:page-size="query.size"
:current-page="query.page"
@current-change="handlePageChange"
class="custom-pagination"
/>
</div>
</div>
<el-dialog
:title="dialogType === 'add' ? '新增奖励类型' : '编辑奖励类型'"
:visible.sync="dialogVisible"
width="500px"
:close-on-click-modal="false"
@closed="handleDialogClosed"
>
<el-form :model="form" :rules="rules" ref="form" label-width="80px" label-position="left" class="add-form">
<el-form-item label="名称" prop="name" required>
<el-input v-model="form.name" placeholder="请输入名称" maxlength="50" style="width: 360px;" />
</el-form-item>
<el-form-item label="腾讯奖励类型ID" prop="tencentTypeId" required>
<el-input-number v-model="form.tencentTypeId" :min="1" style="width: 360px;" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer" style="text-align: left;">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</div>
</el-dialog>
</el-main>
</el-container>
</template>
<script>
import { getRewardTypeList, addRewardType, updateRewardType, deleteRewardType } from '@/api/reward-type'
export default {
name: 'RewardType',
data() {
return {
list: [],
total: 0,
loading: false,
query: {
page: 1,
size: 10,
name: '',
status: undefined,
storeId: undefined
},
dialogVisible: false,
dialogType: 'add', // add 或 edit
form: {
name: '',
tencentTypeId: undefined,
source: 1
},
rules: {
name: [
{ required: true, message: '请输入名称', trigger: 'blur' },
{ min: 1, max: 50, message: '长度在 1 到 50 个字符', trigger: 'blur' }
],
tencentTypeId: [
{ required: true, message: '请输入腾讯奖励类型ID', trigger: 'blur' }
]
}
}
},
created() {
this.getList()
},
methods: {
async getList() {
this.loading = true
try {
const res = await getRewardTypeList(this.query)
if (res.code === 0) {
this.list = res.data.list
this.total = res.data.total
} else {
this.$message.error(res.message || '获取奖励类型列表失败')
}
} catch (e) {
this.$message.error('请求失败')
} finally {
this.loading = false
}
},
handlePageChange(page) {
this.query.page = page
this.getList()
},
onEdit(row) {
this.dialogType = 'edit'
this.form = {
id: row.id,
name: row.name,
tencentTypeId: row.tencentTypeId,
source: row.source
}
this.dialogVisible = true
},
onDelete(row) {
this.$confirm('确认删除该奖励类型吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteRewardType(row.id)
if (res.code === 0) {
this.$message.success('删除成功')
this.getList()
} else {
this.$message.error(res.message || '删除失败')
}
} catch (e) {
this.$message.error('删除失败')
}
}).catch(() => {})
},
handleDialogClosed() {
this.$refs.form && this.$refs.form.resetFields()
this.form = {
name: '',
tencentTypeId: undefined,
source: 1
}
this.dialogType = 'add' // 重置为新增状态
},
async handleSubmit() {
this.$refs.form.validate(async (valid) => {
if (valid) {
try {
const api = this.dialogType === 'add' ? addRewardType : updateRewardType
const submitData = { ...this.form }
const res = await api(submitData)
if (res.code === 0) {
this.$message.success(this.dialogType === 'add' ? '新增成功' : '编辑成功')
this.dialogVisible = false
this.getList()
} else {
this.$message.error(res.message || (this.dialogType === 'add' ? '新增失败' : '编辑失败'))
}
} catch (e) {
this.$message.error('请求失败')
}
}
})
},
handleSearch() {
this.query.page = 1
this.getList()
},
handleReset() {
this.query = {
page: 1,
size: 10,
name: '',
status: undefined,
storeId: undefined
}
this.getList()
}
}
}
</script>
<style scoped>
.reward-type-page {
background: #fff;
padding: 20px 24px 12px 24px;
border-radius: 10px;
min-height: 500px;
}
.header-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 10px;
}
.search-form {
flex: 1;
margin-right: 16px;
}
.search-form .el-form {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.search-form .el-form-item {
margin-bottom: 0;
}
.action-buttons {
display: flex;
justify-content: center;
gap: 8px;
}
.refresh-btn,
.add-btn {
background: #f7f8fa;
border: 1px solid #e3e6f0;
border-radius: 6px;
height: 36px;
padding: 0 16px;
display: flex;
align-items: center;
gap: 4px;
transition: all 0.3s ease;
&:hover {
background: #556ff6;
color: #fff;
border-color: #556ff6;
i {
transform: rotate(180deg);
}
}
i {
font-size: 14px;
transition: transform 0.3s ease;
}
span {
font-size: 14px;
font-weight: 500;
}
}
.add-btn {
background: #556ff6;
border-color: #556ff6;
color: #fff;
&:hover {
background: #3d4fd8;
border-color: #3d4fd8;
}
}
.el-table {
margin-bottom: 8px;
}
.reward-table-row td {
padding-top: 8px !important;
padding-bottom: 8px !important;
font-size: 14px;
text-align: center !important;
}
.el-table .cell {
padding-left: 8px;
padding-right: 8px;
text-align: center !important;
}
.table-footer {
margin-top: 8px;
text-align: right;
}
.custom-pagination {
margin: 0;
}
.action-btn {
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
display: inline-flex;
align-items: center;
gap: 4px;
transition: all 0.3s ease;
i {
font-size: 14px;
}
&.edit-btn {
background: #556ff6;
border-color: #556ff6;
&:hover {
background: #3d4fd8;
border-color: #3d4fd8;
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(85, 111, 246, 0.2);
}
}
&.delete-btn {
background: #f56c6c;
border-color: #f56c6c;
&:hover {
background: #e64242;
border-color: #e64242;
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(245, 108, 108, 0.2);
}
}
}
.add-form .el-form-item {
margin-bottom: 20px;
}
.add-form .el-form-item__label {
text-align: left;
padding-right: 12px;
}
.add-form .el-input {
font-size: 14px;
}
.add-form .el-form-item__content {
text-align: left;
}
.add-form .el-input__inner {
text-align: left;
}
.add-form .el-textarea__inner {
text-align: left;
}
.add-form .el-radio-group {
text-align: left;
}
.status-tag {
display: inline-block;
padding: 2px 16px;
border-radius: 16px;
font-size: 14px;
font-weight: 500;
background: #e8f7ea;
color: #3fc06d;
}
.status-block {
background: #fbeaea;
color: #f56c6c;
}
</style>

View File

@ -0,0 +1,475 @@
<template>
<div class="feedback-list-page">
<div class="search-row">
<el-form :inline="true" @submit.native.prevent>
<el-form-item label="状态">
<el-select v-model="query.status" placeholder="请选择状态">
<el-option label="全部" :value="0" />
<el-option label="未处理" :value="1" />
<el-option label="已处理" :value="2" />
<el-option label="已驳回" :value="3" />
<el-option label="已关闭" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="反馈类型" required>
<el-select v-model="query.feedbackType" placeholder="请选择反馈类型" clearable>
<el-option label="全部" :value="0" />
<el-option label="BUG" :value="1" />
<el-option label="建议" :value="2" />
<el-option label="投诉" :value="3" />
<el-option label="其他" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="标题">
<el-input v-model="query.title" placeholder="标题" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getList">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<div class="feedback-action">
<el-button class="refresh-btn" @click="handleRefresh">
<i class="el-icon-refresh"></i>
<span>刷新</span>
</el-button>
</div>
</div>
<div class="table-card">
<el-table
:data="list"
border={false}
class="custom-table"
header-row-class-name="custom-header"
row-class-name="custom-row"
>
<el-table-column prop="userId" label="用户" width="80" />
<el-table-column prop="title" label="反馈标题" />
<el-table-column prop="content" label="反馈内容" />
<el-table-column prop="feedbackType" label="反馈类型" :formatter="feedbackTypeFormatter" />
<el-table-column prop="createdAt" label="反馈时间" />
<el-table-column prop="status" label="状态" :formatter="statusFormatter">
<template slot-scope="scope">
<span :class="['status-tag', getStatusClass(scope.row.status)]">
{{ statusFormatter(scope.row) }}
</span>
</template>
</el-table-column>
<el-table-column prop="reply" label="回复" />
<el-table-column label="操作" width="240" align="center">
<template slot-scope="scope">
<div class="action-buttons">
<el-button type="primary" size="mini" class="action-btn detail-btn" @click="onDetail(scope.row)">
<i class="el-icon-view"></i>
详情
</el-button>
<el-button type="success" size="mini" class="action-btn reply-btn" @click="onReply(scope.row)">
<i class="el-icon-chat-dot-round"></i>
回复
</el-button>
<el-button type="danger" size="mini" class="action-btn delete-btn" @click="onDelete(scope.row)">
<i class="el-icon-delete"></i>
删除
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div class="table-footer">
<div class="table-summary">
显示第 {{ (query.page - 1) * query.size + 1 }}
{{ Math.min(query.page * query.size, total) }} {{ total }} 条记录
</div>
<el-pagination
background
layout="prev, pager, next"
:current-page="query.page"
:page-size="query.size"
:total="total"
@current-change="handlePageChange"
class="custom-pagination"
/>
</div>
</div>
<el-dialog
title="反馈详情"
:visible.sync="detailDialogVisible"
width="500px"
:close-on-click-modal="false"
>
<el-descriptions :column="1" border>
<el-descriptions-item label="标题">{{ detailData.title }}</el-descriptions-item>
<el-descriptions-item label="内容">{{ detailData.content }}</el-descriptions-item>
<el-descriptions-item label="反馈类型">{{ feedbackTypeFormatter(detailData) }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ statusFormatter(detailData) }}</el-descriptions-item>
<el-descriptions-item label="回复">{{ detailData.reply }}</el-descriptions-item>
<el-descriptions-item label="用户ID">{{ detailData.userId }}</el-descriptions-item>
<el-descriptions-item label="商户ID">{{ detailData.merchantId }}</el-descriptions-item>
<el-descriptions-item label="门店ID">{{ detailData.storeId }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
<el-dialog
title="回复反馈"
:visible.sync="replyDialogVisible"
width="400px"
:close-on-click-modal="false"
>
<el-form :model="replyForm" label-width="60px">
<el-form-item label="回复">
<el-input
type="textarea"
v-model="replyForm.reply"
placeholder="请输入回复内容"
:rows="4"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="replyForm.status" placeholder="请选择状态">
<el-option label="未处理" :value="1" />
<el-option label="已处理" :value="2" />
<el-option label="已驳回" :value="3" />
<el-option label="已关闭" :value="4" />
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="replyDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleReply">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { feedbackList, feedbackDetail, feedbackReply } from '@/api/feedback'
export default {
name: 'FeedbackList',
data() {
return {
list: [],
total: 0,
query: {
status: 0,
page: 1,
size: 10,
feedbackType: 0,
title: ''
},
detailDialogVisible: false,
detailData: {},
replyDialogVisible: false,
replyForm: {
id: null,
reply: '',
status: 2 // 默认已处理
}
}
},
created() {
this.getList()
},
methods: {
async getList() {
try {
const res = await feedbackList(this.query)
if (res.code === 0) {
this.list = res.data.list
this.total = res.data.total
} else {
this.$message.error(res.message || '获取数据失败')
}
} catch (e) {
this.$message.error('请求失败')
}
},
handlePageChange(page) {
this.query.page = page
this.getList()
},
handleRefresh() {
this.query = {
status: 0,
page: 1,
size: 10,
feedbackType: 0,
title: ''
}
this.getList()
},
handleReset() {
this.query = {
status: 0,
page: 1,
size: 10,
feedbackType: 0,
title: ''
}
this.getList()
},
feedbackTypeFormatter(row) {
const map = {
1: 'BUG',
2: '建议',
3: '投诉',
4: '其他',
0: '全部'
}
return map[row.feedbackType] || row.feedbackType
},
statusFormatter(row) {
const map = {
0: '全部',
1: '未处理',
2: '已处理',
3: '已驳回',
4: '已关闭'
}
return map[row.status] || row.status
},
getStatusClass(status) {
const map = {
0: 'status-all',
1: 'status-pending',
2: 'status-processed',
3: 'status-rejected',
4: 'status-closed'
}
return map[status] || ''
},
async onDetail(row) {
try {
const res = await feedbackDetail(row.id)
if (res.code === 0) {
this.detailData = res.data
this.detailDialogVisible = true
} else {
this.$message.error(res.message || '获取详情失败')
}
} catch (e) {
this.$message.error('请求失败')
}
},
onReply(row) {
this.replyForm.id = row.id
this.replyForm.reply = row.reply || ''
this.replyForm.status = 2 // 默认"已处理"
this.replyDialogVisible = true
},
async handleReply() {
try {
const res = await feedbackReply(this.replyForm)
if (res.code === 0 && res.data.success) {
this.$message.success('回复成功')
this.replyDialogVisible = false
this.getList()
} else {
this.$message.error(res.message || '回复失败')
}
} catch (e) {
this.$message.error('请求失败')
}
},
onDelete(row) {
this.$confirm('确认删除该反馈吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$message.success('删除功能待实现')
// 这里可以调用删除接口
}).catch(() => {})
}
}
}
</script>
<style lang="scss" scoped>
.feedback-list-page {
background: #fff;
min-height: 100vh;
padding: 0 32px 32px 32px;
}
.search-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 20px;
margin-bottom: 18px;
::v-deep .el-form-item {
margin-bottom: 0;
margin-right: 16px;
&:last-child {
margin-right: 0;
}
}
}
.feedback-action {
display: flex;
align-items: center;
gap: 12px;
}
.feedback-action .refresh-btn {
background: #f7f8fa;
border: 1px solid #e3e6f0;
border-radius: 6px;
height: 36px;
padding: 0 16px;
display: flex;
align-items: center;
gap: 4px;
transition: all 0.3s ease;
&:hover {
background: #556ff6;
color: #fff;
border-color: #556ff6;
i {
transform: rotate(180deg);
}
}
i {
font-size: 14px;
transition: transform 0.3s ease;
}
span {
font-size: 14px;
font-weight: 500;
}
}
.table-card {
background: #fff;
border-radius: 10px;
box-shadow: none;
padding: 0 0 12px 0;
}
.custom-table {
background: #fff;
border: none;
font-size: 15px;
::v-deep .el-table__body-wrapper td,
::v-deep .el-table__header-wrapper th {
border-right: none !important;
border-left: none !important;
}
}
.custom-header th {
background: #f7f8fa !important;
color: #888;
font-weight: 500;
border-bottom: 1px solid #f0f0f0 !important;
border-top: 1px solid #f0f0f0 !important;
padding-top: 16px !important;
padding-bottom: 16px !important;
height: 56px !important;
line-height: 24px !important;
}
.custom-row td {
border-bottom: 1px solid #f0f0f0 !important;
border-top: none !important;
padding-top: 14px !important;
padding-bottom: 14px !important;
height: 52px !important;
line-height: 22px !important;
vertical-align: middle !important;
}
.table-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
padding: 0 12px;
}
.table-summary {
color: #888;
font-size: 13px;
}
.custom-pagination {
.el-pager li {
border-radius: 6px;
min-width: 32px;
height: 32px;
line-height: 32px;
font-size: 15px;
}
}
.status-tag {
display: inline-block;
padding: 4px 12px;
border-radius: 16px;
font-size: 13px;
font-weight: 500;
&.status-all {
background: #f0f2f5;
color: #666;
}
&.status-pending {
background: #e6f7ff;
color: #1890ff;
}
&.status-processed {
background: #f6ffed;
color: #52c41a;
}
&.status-rejected {
background: #fff2f0;
color: #ff4d4f;
}
&.status-closed {
background: #f5f5f5;
color: #999;
}
}
.action-buttons {
display: flex;
justify-content: center;
gap: 8px;
}
.action-btn {
padding: 6px 12px;
border-radius: 4px;
font-size: 13px;
display: inline-flex;
align-items: center;
gap: 4px;
i {
font-size: 14px;
}
&.detail-btn {
background: #556ff6;
border-color: #556ff6;
&:hover {
background: #3d4fd8;
border-color: #3d4fd8;
}
}
&.reply-btn {
background: #67c23a;
border-color: #67c23a;
&:hover {
background: #529b2e;
border-color: #529b2e;
}
}
&.delete-btn {
background: #f56c6c;
border-color: #f56c6c;
&:hover {
background: #e64242;
border-color: #e64242;
}
}
}
</style>

View File

@ -10,7 +10,10 @@
clearable clearable
/> />
<div class="user-action"> <div class="user-action">
<el-button type="primary" class="add-btn" @click="onAdd">添加用户</el-button> <el-button class="refresh-btn" @click="handleRefresh">
<i class="el-icon-refresh"></i>
<span>刷新</span>
</el-button>
</div> </div>
</div> </div>
<div class="table-card"> <div class="table-card">
@ -49,14 +52,18 @@
</el-table-column> </el-table-column>
<el-table-column prop="birthday" label="注册时间" width="200" align="center"> <el-table-column prop="birthday" label="注册时间" width="200" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
{{ scope.row.birthday ? scope.row.birthday : '-' }} {{ scope.row.firstVisitAt ? scope.row.firstVisitAt : '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="lastLoginStoreName" label="最近登录门店" width="200" align="center" /> <el-table-column prop="lastLoginStoreName" label="最近登录门店" width="200" align="center" />
<el-table-column label="操作" width="120" align="center"> <el-table-column label="操作" width="160" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button type="text" size="mini" class="edit-btn" @click="onEdit(scope.row)">编辑</el-button> <div class="action-buttons">
<el-button type="text" size="mini" class="delete-btn" @click="onDeleteSingle(scope.row)">删除</el-button> <el-button type="primary" size="mini" class="action-btn edit-btn" @click="onEdit(scope.row)">
<i class="el-icon-edit"></i>
编辑
</el-button>
</div>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -80,7 +87,7 @@
</template> </template>
<script> <script>
import { userList, deleteUser, batchDeleteUsers } from '@/api/user' import { userList } from '@/api/user'
export default { export default {
name: 'UserList', name: 'UserList',
@ -125,34 +132,12 @@ export default {
this.searchForm.page = val this.searchForm.page = val
this.getList() this.getList()
}, },
onAdd() {
// TODO: 实现添加用户弹窗
this.$message.info('添加用户功能待实现')
},
onEdit(row) { onEdit(row) {
// TODO: 实现编辑功能 // TODO: 实现编辑功能
this.$message.info('编辑功能待实现') this.$message.info('编辑功能待实现')
}, },
async onDeleteSingle(row) { handleRefresh() {
try { this.getList()
await this.$confirm('确认删除该用户吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const res = await deleteUser(row.id)
if (res.code === 0) {
this.$message.success('删除成功')
this.getList()
} else {
this.$message.error(res.message || '删除失败')
}
} catch (error) {
if (error !== 'cancel') {
this.$message.error('删除失败')
console.error(error)
}
}
} }
} }
} }
@ -176,11 +161,37 @@ export default {
background: #fff; background: #fff;
border-radius: 6px; border-radius: 6px;
} }
.user-action .add-btn { .user-action {
background: #556ff6; display: flex;
border: none; align-items: center;
gap: 12px;
}
.user-action .refresh-btn {
background: #f7f8fa;
border: 1px solid #e3e6f0;
border-radius: 6px; border-radius: 6px;
font-weight: 500; height: 36px;
padding: 0 16px;
display: flex;
align-items: center;
gap: 4px;
transition: all 0.3s ease;
&:hover {
background: #556ff6;
color: #fff;
border-color: #556ff6;
i {
transform: rotate(180deg);
}
}
i {
font-size: 14px;
transition: transform 0.3s ease;
}
span {
font-size: 14px;
font-weight: 500;
}
} }
.table-card { .table-card {
background: #fff; background: #fff;
@ -271,14 +282,31 @@ export default {
font-size: 15px; font-size: 15px;
} }
} }
.edit-btn { .action-buttons {
color: #556ff6; display: flex;
font-weight: 500; justify-content: center;
gap: 8px;
} }
.delete-btn { .action-btn {
color: #f56c6c; padding: 6px 12px;
font-weight: 500; border-radius: 4px;
margin-left: 8px; font-size: 13px;
display: inline-flex;
align-items: center;
gap: 4px;
i {
font-size: 14px;
}
&.edit-btn {
background: #556ff6;
border-color: #556ff6;
&:hover {
background: #3d4fd8;
border-color: #3d4fd8;
}
}
} }
::v-deep .el-breadcrumb__inner, ::v-deep .el-breadcrumb__inner,
::v-deep .el-breadcrumb__inner a { ::v-deep .el-breadcrumb__inner a {