1 parent
96b213354b
commit
88db96c374
2 files changed
+709
No files matched your search
@@ -0,0 +1,93 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import request from '@/axios'
|
||||||
|
import type { PageResult } from './types'
|
||||||
|
|
||||||
|
export interface RewardTaskItem {
|
||||||
|
id: string
|
||||||
|
code: string
|
||||||
|
sectionCode: string
|
||||||
|
type: string
|
||||||
|
verifyMode: string
|
||||||
|
targetValue: number
|
||||||
|
rewardCoin: number
|
||||||
|
cycleType: string
|
||||||
|
cycleLimit: number
|
||||||
|
actionType: string
|
||||||
|
actionUrl: string
|
||||||
|
iconUrl: string
|
||||||
|
titleId: string
|
||||||
|
titleEn: string
|
||||||
|
titleZh: string
|
||||||
|
subtitleId: string
|
||||||
|
subtitleEn: string
|
||||||
|
subtitleZh: string
|
||||||
|
ctaId: string
|
||||||
|
ctaEn: string
|
||||||
|
ctaZh: string
|
||||||
|
sectionTitleId: string
|
||||||
|
sectionTitleEn: string
|
||||||
|
sectionTitleZh: string
|
||||||
|
configJson: string
|
||||||
|
sort: number
|
||||||
|
status: number
|
||||||
|
statusText: string
|
||||||
|
startAt?: string
|
||||||
|
endAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RewardTaskPageParams {
|
||||||
|
pageIndex?: number
|
||||||
|
pageSize?: number
|
||||||
|
keyword?: string
|
||||||
|
type?: string
|
||||||
|
sectionCode?: string
|
||||||
|
status?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RewardTaskSaveData = Omit<RewardTaskItem, 'id' | 'status' | 'statusText'>
|
||||||
|
|
||||||
|
export interface RewardRuleDay {
|
||||||
|
dayIndex: number
|
||||||
|
rewardCoin: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RewardGrantItem {
|
||||||
|
id: string
|
||||||
|
userId: string
|
||||||
|
taskId: string
|
||||||
|
cycleKey: string
|
||||||
|
amount: number
|
||||||
|
status: string
|
||||||
|
idempotentKey: string
|
||||||
|
coinLedgerId: string
|
||||||
|
source: string
|
||||||
|
operatorId: string
|
||||||
|
failureReason: string
|
||||||
|
grantedAt?: string
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageRewardTasksApi = (params: RewardTaskPageParams) =>
|
||||||
|
request.get<PageResult<RewardTaskItem>>({ url: '/admin/rewards/tasks/page', params })
|
||||||
|
|
||||||
|
export const createRewardTaskApi = (data: RewardTaskSaveData) =>
|
||||||
|
request.post<string>({ url: '/admin/rewards/tasks', data })
|
||||||
|
|
||||||
|
export const updateRewardTaskApi = (id: string, data: RewardTaskSaveData) =>
|
||||||
|
request.put<void>({ url: `/admin/rewards/tasks/${id}`, data })
|
||||||
|
|
||||||
|
export const updateRewardTaskStatusApi = (id: string, status: number) =>
|
||||||
|
request.put<void>({ url: `/admin/rewards/tasks/${id}/status`, params: { status } })
|
||||||
|
|
||||||
|
export const getRewardRulesApi = () =>
|
||||||
|
request.get<RewardRuleDay[]>({ url: '/admin/rewards/check-in/rules' })
|
||||||
|
|
||||||
|
export const saveRewardRulesApi = (days: RewardRuleDay[]) =>
|
||||||
|
request.put<void>({ url: '/admin/rewards/check-in/rules', data: { days } })
|
||||||
|
|
||||||
|
export const pageRewardGrantsApi = (params: Record<string, unknown>) =>
|
||||||
|
request.get<PageResult<RewardGrantItem>>({ url: '/admin/rewards/grants/page', params })
|
||||||
|
|
||||||
|
export const manualRewardGrantApi = (data: Record<string, unknown>) =>
|
||||||
|
request.post<RewardGrantItem>({ url: '/admin/rewards/grants/manual', data })
|
||||||
@@ -0,0 +1,616 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import PageHeading from '@/views/components/PageHeading.vue'
|
||||||
|
import { useRbac } from '@/stores/rbac'
|
||||||
|
import { dateCell, formatCount } from '@/utils/format'
|
||||||
|
import {
|
||||||
|
createRewardTaskApi,
|
||||||
|
getRewardRulesApi,
|
||||||
|
manualRewardGrantApi,
|
||||||
|
pageRewardGrantsApi,
|
||||||
|
pageRewardTasksApi,
|
||||||
|
saveRewardRulesApi,
|
||||||
|
updateRewardTaskApi,
|
||||||
|
updateRewardTaskStatusApi
|
||||||
|
} from '@/api/admin/reward'
|
||||||
|
|
||||||
|
const { can } = useRbac()
|
||||||
|
const activeTab = ref('tasks')
|
||||||
|
const typeOptions = [
|
||||||
|
['DAILY_CHECK_IN', '每日签到'],
|
||||||
|
['WATCH_DURATION', '观看时长'],
|
||||||
|
['WATCH_EPISODES', '观看集数'],
|
||||||
|
['FIRST_LOGIN', '首次登录'],
|
||||||
|
['COMPLETE_PROFILE', '完善资料'],
|
||||||
|
['FIRST_PURCHASE', '首次购买'],
|
||||||
|
['FOLLOW_SOCIAL', '关注社交账号'],
|
||||||
|
['INVITE_FRIEND', '邀请好友'],
|
||||||
|
['CUSTOM_LINK', '自定义任务']
|
||||||
|
]
|
||||||
|
const cycleOptions = [
|
||||||
|
['ONCE', '一次性'],
|
||||||
|
['DAILY', '每日'],
|
||||||
|
['WEEKLY', '每周']
|
||||||
|
]
|
||||||
|
const sectionOptions = [
|
||||||
|
['CHECK_IN', '签到奖励'],
|
||||||
|
['DAILY', '每日任务'],
|
||||||
|
['NEWBIE', '新手任务'],
|
||||||
|
['ACTIVITY', '活动任务']
|
||||||
|
]
|
||||||
|
const actionOptions = [
|
||||||
|
['NONE', '无动作'],
|
||||||
|
['OPEN_HOME', '打开首页'],
|
||||||
|
['OPEN_PROFILE', '打开个人资料'],
|
||||||
|
['OPEN_WALLET', '打开钱包'],
|
||||||
|
['OPEN_URL', '打开链接']
|
||||||
|
]
|
||||||
|
|
||||||
|
const tasks = reactive({ rows: [], total: 0, page: 1, size: 10, loading: false })
|
||||||
|
const filters = reactive({
|
||||||
|
keyword: '',
|
||||||
|
type: undefined,
|
||||||
|
sectionCode: undefined,
|
||||||
|
status: undefined
|
||||||
|
})
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const editingId = ref(null)
|
||||||
|
const emptyForm = () => ({
|
||||||
|
code: '',
|
||||||
|
sectionCode: 'DAILY',
|
||||||
|
type: 'WATCH_DURATION',
|
||||||
|
verifyMode: 'SERVER',
|
||||||
|
targetValue: 1,
|
||||||
|
rewardCoin: 10,
|
||||||
|
cycleType: 'DAILY',
|
||||||
|
cycleLimit: 1,
|
||||||
|
actionType: 'OPEN_HOME',
|
||||||
|
actionUrl: '',
|
||||||
|
iconUrl: '',
|
||||||
|
titleId: '',
|
||||||
|
titleEn: '',
|
||||||
|
titleZh: '',
|
||||||
|
subtitleId: '',
|
||||||
|
subtitleEn: '',
|
||||||
|
subtitleZh: '',
|
||||||
|
ctaId: 'Mulai',
|
||||||
|
ctaEn: 'Start',
|
||||||
|
ctaZh: '去完成',
|
||||||
|
sectionTitleId: 'Tugas harian',
|
||||||
|
sectionTitleEn: 'Daily tasks',
|
||||||
|
sectionTitleZh: '每日任务',
|
||||||
|
configJson: '',
|
||||||
|
sort: 0,
|
||||||
|
startAt: null,
|
||||||
|
endAt: null
|
||||||
|
})
|
||||||
|
const form = reactive(emptyForm())
|
||||||
|
|
||||||
|
async function loadTasks() {
|
||||||
|
tasks.loading = true
|
||||||
|
try {
|
||||||
|
const res = await pageRewardTasksApi({
|
||||||
|
pageIndex: tasks.page,
|
||||||
|
pageSize: tasks.size,
|
||||||
|
keyword: filters.keyword.trim() || undefined,
|
||||||
|
type: filters.type,
|
||||||
|
sectionCode: filters.sectionCode,
|
||||||
|
status: filters.status
|
||||||
|
})
|
||||||
|
tasks.rows = res.data?.list || []
|
||||||
|
tasks.total = res.data?.total || 0
|
||||||
|
} finally {
|
||||||
|
tasks.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function searchTasks() {
|
||||||
|
tasks.page = 1
|
||||||
|
loadTasks()
|
||||||
|
}
|
||||||
|
function resetFilters() {
|
||||||
|
Object.assign(filters, {
|
||||||
|
keyword: '',
|
||||||
|
type: undefined,
|
||||||
|
sectionCode: undefined,
|
||||||
|
status: undefined
|
||||||
|
})
|
||||||
|
searchTasks()
|
||||||
|
}
|
||||||
|
function openCreate() {
|
||||||
|
editingId.value = null
|
||||||
|
Object.assign(form, emptyForm(), { sort: tasks.total + 1 })
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
function openEdit(row) {
|
||||||
|
editingId.value = row.id
|
||||||
|
Object.assign(form, emptyForm(), row)
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
function validateTask() {
|
||||||
|
if (!form.code.trim() || !form.titleId.trim() || !form.sectionTitleId.trim())
|
||||||
|
return '请填写编码、印尼语标题和分组标题'
|
||||||
|
if (form.targetValue <= 0 || form.rewardCoin < 0) return '目标值必须大于 0,奖励金币不能小于 0'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
async function saveTask() {
|
||||||
|
const invalid = validateTask()
|
||||||
|
if (invalid) return ElMessage.warning(invalid)
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
...form,
|
||||||
|
code: form.code.trim(),
|
||||||
|
titleId: form.titleId.trim(),
|
||||||
|
sectionTitleId: form.sectionTitleId.trim()
|
||||||
|
}
|
||||||
|
if (editingId.value) await updateRewardTaskApi(editingId.value, payload)
|
||||||
|
else await createRewardTaskApi(payload)
|
||||||
|
ElMessage.success(editingId.value ? '任务已更新' : '任务已创建,默认下线')
|
||||||
|
dialogVisible.value = false
|
||||||
|
await loadTasks()
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function toggleTask(row) {
|
||||||
|
await updateRewardTaskStatusApi(row.id, row.status === 1 ? 0 : 1)
|
||||||
|
await loadTasks()
|
||||||
|
}
|
||||||
|
|
||||||
|
const rules = ref([])
|
||||||
|
const rulesLoading = ref(false)
|
||||||
|
const rulesSaving = ref(false)
|
||||||
|
async function loadRules() {
|
||||||
|
rulesLoading.value = true
|
||||||
|
try {
|
||||||
|
rules.value = (await getRewardRulesApi()).data || []
|
||||||
|
} finally {
|
||||||
|
rulesLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function saveRules() {
|
||||||
|
rulesSaving.value = true
|
||||||
|
try {
|
||||||
|
await saveRewardRulesApi(
|
||||||
|
rules.value.map(({ dayIndex, rewardCoin }) => ({ dayIndex, rewardCoin }))
|
||||||
|
)
|
||||||
|
ElMessage.success('签到规则已保存')
|
||||||
|
} finally {
|
||||||
|
rulesSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const grants = reactive({
|
||||||
|
rows: [],
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
loading: false,
|
||||||
|
userId: '',
|
||||||
|
status: undefined
|
||||||
|
})
|
||||||
|
const grantVisible = ref(false)
|
||||||
|
const grantSaving = ref(false)
|
||||||
|
const grantForm = reactive({ userId: '', taskId: '', amount: 10, idempotentKey: '', reason: '' })
|
||||||
|
async function loadGrants() {
|
||||||
|
grants.loading = true
|
||||||
|
try {
|
||||||
|
const res = await pageRewardGrantsApi({
|
||||||
|
pageIndex: grants.page,
|
||||||
|
pageSize: grants.size,
|
||||||
|
userId: grants.userId || undefined,
|
||||||
|
status: grants.status
|
||||||
|
})
|
||||||
|
grants.rows = res.data?.list || []
|
||||||
|
grants.total = res.data?.total || 0
|
||||||
|
} finally {
|
||||||
|
grants.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function queryGrants() {
|
||||||
|
grants.page = 1
|
||||||
|
loadGrants()
|
||||||
|
}
|
||||||
|
function openGrant() {
|
||||||
|
Object.assign(grantForm, {
|
||||||
|
userId: grants.userId,
|
||||||
|
taskId: '',
|
||||||
|
amount: 10,
|
||||||
|
idempotentKey: '',
|
||||||
|
reason: ''
|
||||||
|
})
|
||||||
|
grantVisible.value = true
|
||||||
|
}
|
||||||
|
async function confirmGrant() {
|
||||||
|
if (!grantForm.userId || grantForm.amount <= 0)
|
||||||
|
return ElMessage.warning('请填写用户 ID 和补发金币')
|
||||||
|
grantSaving.value = true
|
||||||
|
try {
|
||||||
|
await manualRewardGrantApi({
|
||||||
|
...grantForm,
|
||||||
|
taskId: grantForm.taskId || undefined,
|
||||||
|
idempotentKey: grantForm.idempotentKey || undefined
|
||||||
|
})
|
||||||
|
ElMessage.success('奖励已通过钱包流水补发')
|
||||||
|
grantVisible.value = false
|
||||||
|
await loadGrants()
|
||||||
|
} finally {
|
||||||
|
grantSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTab(name) {
|
||||||
|
if (name === 'rules' && !rules.value.length) loadRules()
|
||||||
|
if (name === 'grants' && !grants.rows.length) loadGrants()
|
||||||
|
}
|
||||||
|
function optionLabel(options, value) {
|
||||||
|
return options.find((item) => item[0] === value)?.[1] || value || '-'
|
||||||
|
}
|
||||||
|
onMounted(loadTasks)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageHeading
|
||||||
|
eyebrow="订单与权益"
|
||||||
|
title="奖励任务"
|
||||||
|
description="配置奖励页任务、签到规则和发放记录,所有金币变动均写钱包流水。"
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
v-if="activeTab === 'tasks' && can('reward:update')"
|
||||||
|
type="primary"
|
||||||
|
@click="openCreate"
|
||||||
|
>新建任务</el-button
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
v-if="activeTab === 'grants' && can('reward:grant')"
|
||||||
|
type="primary"
|
||||||
|
@click="openGrant"
|
||||||
|
>人工补发</el-button
|
||||||
|
>
|
||||||
|
</PageHeading>
|
||||||
|
|
||||||
|
<section class="panel reward-panel">
|
||||||
|
<el-tabs v-model="activeTab" @tab-change="handleTab">
|
||||||
|
<el-tab-pane label="任务配置" name="tasks">
|
||||||
|
<div class="reward-toolbar">
|
||||||
|
<el-input
|
||||||
|
v-model="filters.keyword"
|
||||||
|
clearable
|
||||||
|
placeholder="任务编码 / 标题"
|
||||||
|
@keyup.enter="searchTasks"
|
||||||
|
/>
|
||||||
|
<el-select v-model="filters.type" clearable placeholder="全部类型" @change="searchTasks"
|
||||||
|
><el-option
|
||||||
|
v-for="item in typeOptions"
|
||||||
|
:key="item[0]"
|
||||||
|
:label="item[1]"
|
||||||
|
:value="item[0]"
|
||||||
|
/></el-select>
|
||||||
|
<el-select
|
||||||
|
v-model="filters.sectionCode"
|
||||||
|
clearable
|
||||||
|
placeholder="全部分组"
|
||||||
|
@change="searchTasks"
|
||||||
|
><el-option
|
||||||
|
v-for="item in sectionOptions"
|
||||||
|
:key="item[0]"
|
||||||
|
:label="item[1]"
|
||||||
|
:value="item[0]"
|
||||||
|
/></el-select>
|
||||||
|
<el-select v-model="filters.status" clearable placeholder="全部状态" @change="searchTasks"
|
||||||
|
><el-option label="已上线" :value="1" /><el-option label="已下线" :value="0"
|
||||||
|
/></el-select>
|
||||||
|
<el-button @click="searchTasks">查询</el-button
|
||||||
|
><el-button @click="resetFilters">重置</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
v-loading="tasks.loading"
|
||||||
|
:data="tasks.rows"
|
||||||
|
row-key="id"
|
||||||
|
empty-text="暂无奖励任务"
|
||||||
|
>
|
||||||
|
<el-table-column prop="code" label="任务编码" min-width="150" />
|
||||||
|
<el-table-column label="标题" min-width="180"
|
||||||
|
><template #default="{ row }"
|
||||||
|
><strong>{{ row.titleId }}</strong
|
||||||
|
><div class="sub-copy">{{ row.titleZh || row.titleEn }}</div></template
|
||||||
|
></el-table-column
|
||||||
|
>
|
||||||
|
<el-table-column label="类型" width="130"
|
||||||
|
><template #default="{ row }">{{
|
||||||
|
optionLabel(typeOptions, row.type)
|
||||||
|
}}</template></el-table-column
|
||||||
|
>
|
||||||
|
<el-table-column label="分组" width="110"
|
||||||
|
><template #default="{ row }">{{
|
||||||
|
optionLabel(sectionOptions, row.sectionCode)
|
||||||
|
}}</template></el-table-column
|
||||||
|
>
|
||||||
|
<el-table-column label="目标 / 奖励" width="130"
|
||||||
|
><template #default="{ row }"
|
||||||
|
>{{ formatCount(row.targetValue) }} / +{{ formatCount(row.rewardCoin) }}</template
|
||||||
|
></el-table-column
|
||||||
|
>
|
||||||
|
<el-table-column label="周期" width="100"
|
||||||
|
><template #default="{ row }">{{
|
||||||
|
optionLabel(cycleOptions, row.cycleType)
|
||||||
|
}}</template></el-table-column
|
||||||
|
>
|
||||||
|
<el-table-column label="状态" width="90"
|
||||||
|
><template #default="{ row }"
|
||||||
|
><el-tag :type="row.status === 1 ? 'success' : 'info'">{{
|
||||||
|
row.statusText
|
||||||
|
}}</el-tag></template
|
||||||
|
></el-table-column
|
||||||
|
>
|
||||||
|
<el-table-column label="操作" width="170" fixed="right"
|
||||||
|
><template #default="{ row }"
|
||||||
|
><el-button v-if="can('reward:update')" link type="primary" @click="openEdit(row)"
|
||||||
|
>编辑</el-button
|
||||||
|
><el-button
|
||||||
|
v-if="can('reward:update')"
|
||||||
|
link
|
||||||
|
:type="row.status === 1 ? 'danger' : 'success'"
|
||||||
|
@click="toggleTask(row)"
|
||||||
|
>{{ row.status === 1 ? '下线' : '上线' }}</el-button
|
||||||
|
></template
|
||||||
|
></el-table-column
|
||||||
|
>
|
||||||
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="tasks.page"
|
||||||
|
v-model:page-size="tasks.size"
|
||||||
|
:total="tasks.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="loadTasks"
|
||||||
|
@size-change="searchTasks"
|
||||||
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="签到规则" name="rules">
|
||||||
|
<div v-loading="rulesLoading" class="rule-grid"
|
||||||
|
><div v-for="day in rules" :key="day.dayIndex" class="rule-card"
|
||||||
|
><span>第 {{ day.dayIndex }} 天</span
|
||||||
|
><el-input-number v-model="day.rewardCoin" :min="0" /><small>金币</small></div
|
||||||
|
></div
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
v-if="can('reward:checkin')"
|
||||||
|
type="primary"
|
||||||
|
:loading="rulesSaving"
|
||||||
|
@click="saveRules"
|
||||||
|
>保存签到规则</el-button
|
||||||
|
>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="发放记录" name="grants">
|
||||||
|
<div class="reward-toolbar"
|
||||||
|
><el-input
|
||||||
|
v-model="grants.userId"
|
||||||
|
clearable
|
||||||
|
placeholder="用户 ID"
|
||||||
|
@keyup.enter="queryGrants"
|
||||||
|
/><el-select
|
||||||
|
v-model="grants.status"
|
||||||
|
clearable
|
||||||
|
placeholder="全部状态"
|
||||||
|
@change="queryGrants"
|
||||||
|
><el-option label="已发放" value="GRANTED" /><el-option
|
||||||
|
label="待处理"
|
||||||
|
value="PENDING" /><el-option label="失败" value="FAILED" /></el-select
|
||||||
|
><el-button @click="queryGrants">查询</el-button></div
|
||||||
|
>
|
||||||
|
<el-table
|
||||||
|
v-loading="grants.loading"
|
||||||
|
:data="grants.rows"
|
||||||
|
row-key="id"
|
||||||
|
empty-text="暂无发放记录"
|
||||||
|
>
|
||||||
|
<el-table-column prop="userId" label="用户 ID" min-width="160" /><el-table-column
|
||||||
|
prop="taskId"
|
||||||
|
label="任务 ID"
|
||||||
|
min-width="160"
|
||||||
|
/><el-table-column label="金币" width="100"
|
||||||
|
><template #default="{ row }">+{{ formatCount(row.amount) }}</template></el-table-column
|
||||||
|
><el-table-column prop="source" label="来源" width="90" /><el-table-column
|
||||||
|
prop="status"
|
||||||
|
label="状态"
|
||||||
|
width="100"
|
||||||
|
/><el-table-column
|
||||||
|
prop="idempotentKey"
|
||||||
|
label="幂等键"
|
||||||
|
min-width="240"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/><el-table-column label="发放时间" width="180"
|
||||||
|
><template #default="{ row }">{{
|
||||||
|
dateCell(row.grantedAt || row.createdAt)
|
||||||
|
}}</template></el-table-column
|
||||||
|
>
|
||||||
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="grants.page"
|
||||||
|
v-model:page-size="grants.size"
|
||||||
|
:total="grants.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="loadGrants"
|
||||||
|
@size-change="queryGrants"
|
||||||
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="editingId ? '编辑奖励任务' : '新建奖励任务'"
|
||||||
|
width="780px"
|
||||||
|
>
|
||||||
|
<el-form label-position="top" class="task-form"
|
||||||
|
><el-row :gutter="16"
|
||||||
|
><el-col :span="8"
|
||||||
|
><el-form-item label="任务编码"
|
||||||
|
><el-input v-model="form.code" :disabled="Boolean(editingId)" /></el-form-item></el-col
|
||||||
|
><el-col :span="8"
|
||||||
|
><el-form-item label="任务类型"
|
||||||
|
><el-select v-model="form.type"
|
||||||
|
><el-option
|
||||||
|
v-for="item in typeOptions"
|
||||||
|
:key="item[0]"
|
||||||
|
:label="item[1]"
|
||||||
|
:value="item[0]" /></el-select></el-form-item></el-col
|
||||||
|
><el-col :span="8"
|
||||||
|
><el-form-item label="分组"
|
||||||
|
><el-select v-model="form.sectionCode"
|
||||||
|
><el-option
|
||||||
|
v-for="item in sectionOptions"
|
||||||
|
:key="item[0]"
|
||||||
|
:label="item[1]"
|
||||||
|
:value="item[0]" /></el-select></el-form-item></el-col
|
||||||
|
></el-row>
|
||||||
|
<el-row :gutter="16"
|
||||||
|
><el-col :span="6"
|
||||||
|
><el-form-item label="目标值"
|
||||||
|
><el-input-number v-model="form.targetValue" :min="1" /></el-form-item></el-col
|
||||||
|
><el-col :span="6"
|
||||||
|
><el-form-item label="奖励金币"
|
||||||
|
><el-input-number v-model="form.rewardCoin" :min="0" /></el-form-item></el-col
|
||||||
|
><el-col :span="6"
|
||||||
|
><el-form-item label="周期"
|
||||||
|
><el-select v-model="form.cycleType"
|
||||||
|
><el-option
|
||||||
|
v-for="item in cycleOptions"
|
||||||
|
:key="item[0]"
|
||||||
|
:label="item[1]"
|
||||||
|
:value="item[0]" /></el-select></el-form-item></el-col
|
||||||
|
><el-col :span="6"
|
||||||
|
><el-form-item label="排序"
|
||||||
|
><el-input-number v-model="form.sort" :min="0" /></el-form-item></el-col
|
||||||
|
></el-row>
|
||||||
|
<el-divider content-position="left">多语言文案</el-divider
|
||||||
|
><el-row :gutter="16"
|
||||||
|
><el-col :span="8"
|
||||||
|
><el-form-item label="印尼语标题"
|
||||||
|
><el-input v-model="form.titleId" /></el-form-item></el-col
|
||||||
|
><el-col :span="8"
|
||||||
|
><el-form-item label="英文标题"><el-input v-model="form.titleEn" /></el-form-item></el-col
|
||||||
|
><el-col :span="8"
|
||||||
|
><el-form-item label="中文标题"><el-input v-model="form.titleZh" /></el-form-item></el-col
|
||||||
|
></el-row>
|
||||||
|
<el-row :gutter="16"
|
||||||
|
><el-col :span="8"
|
||||||
|
><el-form-item label="印尼语说明"
|
||||||
|
><el-input v-model="form.subtitleId" /></el-form-item></el-col
|
||||||
|
><el-col :span="8"
|
||||||
|
><el-form-item label="英文说明"
|
||||||
|
><el-input v-model="form.subtitleEn" /></el-form-item></el-col
|
||||||
|
><el-col :span="8"
|
||||||
|
><el-form-item label="中文说明"
|
||||||
|
><el-input v-model="form.subtitleZh" /></el-form-item></el-col
|
||||||
|
></el-row>
|
||||||
|
<el-row :gutter="16"
|
||||||
|
><el-col :span="8"
|
||||||
|
><el-form-item label="印尼语分组标题"
|
||||||
|
><el-input v-model="form.sectionTitleId" /></el-form-item></el-col
|
||||||
|
><el-col :span="8"
|
||||||
|
><el-form-item label="英文分组标题"
|
||||||
|
><el-input v-model="form.sectionTitleEn" /></el-form-item></el-col
|
||||||
|
><el-col :span="8"
|
||||||
|
><el-form-item label="中文分组标题"
|
||||||
|
><el-input v-model="form.sectionTitleZh" /></el-form-item></el-col
|
||||||
|
></el-row>
|
||||||
|
<el-divider content-position="left">动作与扩展</el-divider
|
||||||
|
><el-row :gutter="16"
|
||||||
|
><el-col :span="8"
|
||||||
|
><el-form-item label="完成按钮"><el-input v-model="form.ctaId" /></el-form-item></el-col
|
||||||
|
><el-col :span="8"
|
||||||
|
><el-form-item label="动作"
|
||||||
|
><el-select v-model="form.actionType"
|
||||||
|
><el-option
|
||||||
|
v-for="item in actionOptions"
|
||||||
|
:key="item[0]"
|
||||||
|
:label="item[1]"
|
||||||
|
:value="item[0]" /></el-select></el-form-item></el-col
|
||||||
|
><el-col :span="8"
|
||||||
|
><el-form-item label="动作链接"
|
||||||
|
><el-input v-model="form.actionUrl" /></el-form-item></el-col></el-row
|
||||||
|
><el-form-item label="扩展配置 JSON"
|
||||||
|
><el-input v-model="form.configJson" type="textarea" :rows="3"
|
||||||
|
/></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer
|
||||||
|
><el-button @click="dialogVisible = false">取消</el-button
|
||||||
|
><el-button type="primary" :loading="saving" @click="saveTask">保存</el-button></template
|
||||||
|
>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="grantVisible" title="人工补发奖励" width="520px"
|
||||||
|
><el-form label-position="top"
|
||||||
|
><el-form-item label="用户 ID"><el-input v-model="grantForm.userId" /></el-form-item
|
||||||
|
><el-form-item label="任务 ID(可选)"><el-input v-model="grantForm.taskId" /></el-form-item
|
||||||
|
><el-form-item label="补发金币"
|
||||||
|
><el-input-number v-model="grantForm.amount" :min="1" /></el-form-item
|
||||||
|
><el-form-item label="幂等键(重复补发时必须相同)"
|
||||||
|
><el-input v-model="grantForm.idempotentKey" /></el-form-item
|
||||||
|
><el-form-item label="原因"><el-input v-model="grantForm.reason" /></el-form-item></el-form
|
||||||
|
><template #footer
|
||||||
|
><el-button @click="grantVisible = false">取消</el-button
|
||||||
|
><el-button type="primary" :loading="grantSaving" @click="confirmGrant"
|
||||||
|
>确认补发</el-button
|
||||||
|
></template
|
||||||
|
></el-dialog
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.reward-panel {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.reward-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.reward-toolbar .el-input,
|
||||||
|
.reward-toolbar .el-select {
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
.sub-copy {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.rule-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, minmax(100px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin: 8px 0 20px;
|
||||||
|
}
|
||||||
|
.rule-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--el-fill-color-lighter);
|
||||||
|
}
|
||||||
|
.rule-card small {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
.task-form :deep(.el-select),
|
||||||
|
.task-form :deep(.el-input-number) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.el-pagination {
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.rule-grid {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in new issue
Block a user