增加积分调整日志

This commit is contained in:
2026-07-21 18:59:45 +08:00
parent 4fbc4500fd
commit 140a068b88
23 changed files with 673 additions and 18 deletions

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace app\admin\controller\mall;
use app\admin\model\MallPointsLog;
use app\common\controller\Backend;
use app\common\model\MallUserAsset;
use support\Response;
use Webman\Http\Request;
class PointsLog extends Backend
{
protected ?object $model = null;
protected array $withJoinTable = ['userAsset', 'admin'];
protected array|string $preExcludeFields = ['id', 'before', 'after', 'memo', 'admin_id', 'create_time'];
protected array|string $quickSearchField = [
'userAsset.playx_user_id',
'userAsset.username',
'userAsset.phone',
];
protected bool $autoFillAdminId = true;
protected function initController(Request $request): ?Response
{
$this->model = new MallPointsLog();
return null;
}
public function add(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
if ($request->method() === 'POST') {
return $this->_add();
}
$userAssetId = $request->get('userAssetId', $request->post('userAssetId', 0));
$userAsset = MallUserAsset::where('id', $userAssetId)->find();
if (!$userAsset) {
return $this->error(__('User asset not found'));
}
return $this->success('', ['userAsset' => $userAsset]);
}
public function edit(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
return $this->error(__('Points log cannot be modified'));
}
public function del(Request $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
return $this->error(__('Points log cannot be deleted'));
}
}

View File

@@ -17,7 +17,14 @@ class UserAsset extends Backend
*/ */
protected ?object $model = null; protected ?object $model = null;
protected array|string $preExcludeFields = ['id', 'create_time', 'update_time']; protected array|string $preExcludeFields = [
'id',
'locked_points',
'available_points',
'today_claimed',
'create_time',
'update_time',
];
protected string|array $quickSearchField = [ protected string|array $quickSearchField = [
'playx_user_id', 'playx_user_id',

View File

@@ -0,0 +1,13 @@
<?php
return [
'User asset' => 'User asset',
'Points adjustment' => 'Points adjustment',
'User asset not found' => 'User asset not found',
'Points adjustment must be a non-zero integer' => 'Points adjustment must be a non-zero integer',
'Insufficient available points' => 'Insufficient available points',
'Administrator added %s points' => 'Administrator added %s points',
'Administrator deducted %s points' => 'Administrator deducted %s points',
'Points log cannot be modified' => 'Points log cannot be modified',
'Points log cannot be deleted' => 'Points log cannot be deleted',
];

View File

@@ -0,0 +1,13 @@
<?php
return [
'User asset' => '用户资产',
'Points adjustment' => '调整积分',
'User asset not found' => '用户资产不存在',
'Points adjustment must be a non-zero integer' => '调整积分必须为非零整数',
'Insufficient available points' => '可用积分不足',
'Administrator added %s points' => '管理员增加积分 %s',
'Administrator deducted %s points' => '管理员扣除积分 %s',
'Points log cannot be modified' => '积分流水不允许修改',
'Points log cannot be deleted' => '积分流水不允许删除',
];

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace app\admin\model;
use app\common\model\MallUserAsset;
use think\model\relation\BelongsTo;
class MallPointsLog extends \app\common\model\MallPointsLog
{
public static function onBeforeInsert($model): void
{
$userAssetId = filter_var($model->user_asset_id, FILTER_VALIDATE_INT);
if ($userAssetId === false || $userAssetId <= 0) {
throw new \InvalidArgumentException(__('User asset not found'));
}
$scoreValue = filter_var($model->score_value, FILTER_VALIDATE_INT);
if ($scoreValue === false || $scoreValue === 0) {
throw new \InvalidArgumentException(__('Points adjustment must be a non-zero integer'));
}
$userAsset = MallUserAsset::where('id', $userAssetId)->lock(true)->find();
if (!$userAsset) {
throw new \RuntimeException(__('User asset not found'));
}
$before = $userAsset->available_points;
$after = $before + $scoreValue;
if ($after < 0) {
throw new \RuntimeException(__('Insufficient available points'));
}
$model->user_asset_id = $userAssetId;
$model->score_value = $scoreValue;
$model->before = $before;
$model->after = $after;
$model->memo = $scoreValue > 0
? __('Administrator added %s points', [abs($scoreValue)])
: __('Administrator deducted %s points', [abs($scoreValue)]);
$userAsset->available_points = $after;
$userAsset->save();
}
public static function onBeforeDelete(): bool
{
return false;
}
public function userAsset(): BelongsTo
{
return $this->belongsTo(MallUserAsset::class, 'user_asset_id');
}
public function admin(): BelongsTo
{
return $this->belongsTo(Admin::class, 'admin_id');
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace app\admin\validate;
use think\Validate;
class MallPointsLog extends Validate
{
protected $failException = true;
protected $rule = [
'user_asset_id' => 'require|integer|gt:0',
'score_value' => 'require|integer|notIn:0',
];
protected $scene = [
'add' => ['user_asset_id', 'score_value'],
];
public function __construct()
{
$this->field = [
'user_asset_id' => __('User asset'),
'score_value' => __('Points adjustment'),
];
parent::__construct();
}
}

View File

@@ -847,7 +847,7 @@ class Playx extends Api
} }
/** /**
* 积分流水(领取/兑换/退回) * 积分流水(领取/兑换/退回/管理员调整
* GET /api/v1/mall/pointsLogs * GET /api/v1/mall/pointsLogs
* *
* 鉴权token / session_id / user_id同 assets/orders * 鉴权token / session_id / user_id同 assets/orders
@@ -981,6 +981,28 @@ FROM (
FROM mall_order o FROM mall_order o
LEFT JOIN mall_item i ON i.id = o.mall_item_id LEFT JOIN mall_item i ON i.id = o.mall_item_id
WHERE o.user_id = :user_id_refund AND o.status = 'REJECTED' AND o.points_cost > 0 AND o.update_time IS NOT NULL WHERE o.user_id = :user_id_refund AND o.status = 'REJECTED' AND o.points_cost > 0 AND o.update_time IS NOT NULL
UNION ALL
SELECT
'ADMIN_ADJUST' AS biz_type,
CASE WHEN pl.score_value > 0 THEN 'IN' ELSE 'OUT' END AS direction,
ABS(pl.score_value) AS points,
pl.create_time AS ts,
pl.id AS ref_id,
'' AS order_no,
'' AS order_status,
0 AS item_id,
'' AS item_title,
0 AS item_type,
0 AS item_score,
0 AS item_amount,
0 AS item_multiplier,
'' AS item_category,
'' AS item_category_title,
CONCAT(LPAD(pl.create_time, 10, '0'), '_4_', LPAD(pl.id, 10, '0')) AS sort_key
FROM mall_points_log pl
WHERE pl.user_asset_id = :user_asset_id_adjust
) t ) t
WHERE 1=1 WHERE 1=1
SQL; SQL;
@@ -989,6 +1011,7 @@ SQL;
'user_id_claim' => $playxUserId, 'user_id_claim' => $playxUserId,
'user_id_redeem' => $playxUserId, 'user_id_redeem' => $playxUserId,
'user_id_refund' => $playxUserId, 'user_id_refund' => $playxUserId,
'user_asset_id_adjust' => $assetId,
]; ];
if ($cursor !== '') { if ($cursor !== '') {
@@ -1016,10 +1039,18 @@ SQL;
if (!is_array($row)) { if (!is_array($row)) {
continue; continue;
} }
$memo = '';
if (($row['biz_type'] ?? '') === 'ADMIN_ADJUST') {
$memoKey = ($row['direction'] ?? '') === 'OUT'
? 'Administrator deducted %s points'
: 'Administrator added %s points';
$memo = __($memoKey, [$row['points'] ?? 0]);
}
$list[] = [ $list[] = [
'biz_type' => $row['biz_type'] ?? '', 'biz_type' => $row['biz_type'] ?? '',
'direction' => $row['direction'] ?? '', 'direction' => $row['direction'] ?? '',
'points' => $row['points'] ?? 0, 'points' => $row['points'] ?? 0,
'memo' => $memo,
'ts' => $row['ts'] ?? null, 'ts' => $row['ts'] ?? null,
'ref_id' => $row['ref_id'] ?? '', 'ref_id' => $row['ref_id'] ?? '',
'order_no' => $row['order_no'] ?? '', 'order_no' => $row['order_no'] ?? '',

View File

@@ -78,6 +78,8 @@ return [
'item_id and user_id/session_id required' => 'item_id and user_id/session_id/token are required', 'item_id and user_id/session_id required' => 'item_id and user_id/session_id/token are required',
'Item not found or not available' => 'Item not found or not available', 'Item not found or not available' => 'Item not found or not available',
'Insufficient points' => 'Insufficient points', 'Insufficient points' => 'Insufficient points',
'Administrator added %s points' => 'Administrator added %s points',
'Administrator deducted %s points' => 'Administrator deducted %s points',
'Redeem submitted, please wait about 10 minutes' => 'Redeem submitted, please wait about 10 minutes', 'Redeem submitted, please wait about 10 minutes' => 'Redeem submitted, please wait about 10 minutes',
'Missing required fields' => 'Missing required fields', 'Missing required fields' => 'Missing required fields',
'Out of stock' => 'Out of stock', 'Out of stock' => 'Out of stock',

View File

@@ -79,6 +79,8 @@ return [
'item_id and user_id/session_id required' => 'item_id dan user_id/session_id/token diperlukan', 'item_id and user_id/session_id required' => 'item_id dan user_id/session_id/token diperlukan',
'Item not found or not available' => 'Item tidak dijumpai atau tidak tersedia', 'Item not found or not available' => 'Item tidak dijumpai atau tidak tersedia',
'Insufficient points' => 'Mata tidak mencukupi', 'Insufficient points' => 'Mata tidak mencukupi',
'Administrator added %s points' => 'Pentadbir menambah %s mata',
'Administrator deducted %s points' => 'Pentadbir menolak %s mata',
'Redeem submitted, please wait about 10 minutes' => 'Penebusan dihantar, sila tunggu kira-kira 10 minit', 'Redeem submitted, please wait about 10 minutes' => 'Penebusan dihantar, sila tunggu kira-kira 10 minit',
'Missing required fields' => 'Medan wajib tiada', 'Missing required fields' => 'Medan wajib tiada',
'Out of stock' => 'Stok tidak mencukupi', 'Out of stock' => 'Stok tidak mencukupi',

View File

@@ -80,6 +80,8 @@ return [
'item_id and user_id/session_id required' => '缺少 item_id或未提供有效的 user_id/session_id/token', 'item_id and user_id/session_id required' => '缺少 item_id或未提供有效的 user_id/session_id/token',
'Item not found or not available' => '商品不存在或已下架', 'Item not found or not available' => '商品不存在或已下架',
'Insufficient points' => '积分不足', 'Insufficient points' => '积分不足',
'Administrator added %s points' => '管理员增加积分 %s',
'Administrator deducted %s points' => '管理员扣除积分 %s',
'Redeem submitted, please wait about 10 minutes' => '兑换已提交,请等待约 10 分钟', 'Redeem submitted, please wait about 10 minutes' => '兑换已提交,请等待约 10 分钟',
'Missing required fields' => '缺少必填字段', 'Missing required fields' => '缺少必填字段',
'Out of stock' => '库存不足', 'Out of stock' => '库存不足',

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace app\common\model;
use app\common\model\traits\TimestampInteger;
use support\think\Model;
class MallPointsLog extends Model
{
use TimestampInteger;
protected string $table = 'mall_points_log';
protected string $pk = 'id';
protected bool $autoWriteTimestamp = true;
protected bool $updateTime = false;
}

View File

@@ -0,0 +1,13 @@
import createAxios from '/@/utils/axios'
export const url = '/admin/mall.PointsLog/'
export function getUserAsset(userAssetId: string | number) {
return createAxios({
url: url + 'add',
method: 'get',
params: {
userAssetId,
},
})
}

View File

@@ -19,6 +19,10 @@ export default {
title: 'About this page', title: 'About this page',
desc: 'History of users moving points from locked/pending to available, filterable by time and user.', desc: 'History of users moving points from locked/pending to available, filterable by time and user.',
}, },
pointsLog: {
title: 'About this page',
desc: 'Records administrator additions and deductions to available points. Entries can only be added and viewed.',
},
item: { item: {
title: 'About this page', title: 'About this page',
desc: 'Manage catalog items, stock, and multilingual display text for the points mall.', desc: 'Manage catalog items, stock, and multilingual display text for the points mall.',

View File

@@ -0,0 +1,20 @@
export default {
id: 'ID',
user_asset: 'User',
user_asset_id: 'User asset ID',
playx_user_id: 'PlayX ID',
username: 'Username',
available_points: 'Available points',
current_points: 'Current points',
score_value: 'Points adjustment',
before: 'Before',
after: 'After',
memo: 'Remark',
admin: 'Administrator',
create_time: 'Created at',
quick_search_fields: 'PlayX ID, username, phone',
non_zero_integer_required: 'Please enter a non-zero integer',
insufficient_available_points: 'Insufficient available points',
admin_add_memo: 'Administrator added {value} points',
admin_deduct_memo: 'Administrator deducted {value} points',
}

View File

@@ -5,6 +5,7 @@ export default {
playx_user_id: 'playX_user_id', playx_user_id: 'playX_user_id',
locked_points: 'locked_points', locked_points: 'locked_points',
available_points: 'available_points', available_points: 'available_points',
adjust_available_points: 'Adjust points',
today_limit: 'today_limit', today_limit: 'today_limit',
today_claimed: 'today_claimed', today_claimed: 'today_claimed',
today_limit_date: 'today_limit_date', today_limit_date: 'today_limit_date',
@@ -12,4 +13,3 @@ export default {
update_time: 'update_time', update_time: 'update_time',
'quick Search Fields': 'id, playX_user_id, username, phone', 'quick Search Fields': 'id, playX_user_id, username, phone',
} }

View File

@@ -107,6 +107,9 @@ export default {
mall_order_approve: 'Approve', mall_order_approve: 'Approve',
mall_dailyPush: 'Daily push', mall_dailyPush: 'Daily push',
mall_claimLog: 'Claim log', mall_claimLog: 'Claim log',
mall_pointsLog: 'Points management',
mall_pointsLog_index: 'Browse',
mall_pointsLog_add: 'Add',
mall_item: 'Products', mall_item: 'Products',
mall_playxOrder: 'playX orders', mall_playxOrder: 'playX orders',
mall_playxCenter: 'playX center', mall_playxCenter: 'playX center',

View File

@@ -19,6 +19,10 @@ export default {
title: '页面说明', title: '页面说明',
desc: '用户从待领取积分划转至可用积分的操作流水,可按时间与用户追溯。', desc: '用户从待领取积分划转至可用积分的操作流水,可按时间与用户追溯。',
}, },
pointsLog: {
title: '页面说明',
desc: '记录管理员对用户可用积分的人工增加与扣除;流水仅允许新增和查看。',
},
item: { item: {
title: '页面说明', title: '页面说明',
desc: '维护积分商城上架商品、库存与多语言展示信息。', desc: '维护积分商城上架商品、库存与多语言展示信息。',

View File

@@ -0,0 +1,20 @@
export default {
id: 'ID',
user_asset: '用户',
user_asset_id: '用户资产ID',
playx_user_id: 'playX-ID',
username: '用户名',
available_points: '可用积分',
current_points: '当前积分',
score_value: '变更积分',
before: '调整前积分',
after: '调整后积分',
memo: '备注',
admin: '操作管理员',
create_time: '创建时间',
quick_search_fields: 'playX用户ID、用户名、手机号',
non_zero_integer_required: '请输入非零整数',
insufficient_available_points: '可用积分不足',
admin_add_memo: '管理员增加积分 {value}',
admin_deduct_memo: '管理员扣除积分 {value}',
}

View File

@@ -5,6 +5,7 @@ export default {
playx_user_id: 'playX-ID', playx_user_id: 'playX-ID',
locked_points: '待领取积分', locked_points: '待领取积分',
available_points: '可用积分', available_points: '可用积分',
adjust_available_points: '调整积分',
today_limit: '今日可领取上限', today_limit: '今日可领取上限',
today_claimed: '今日已领取', today_claimed: '今日已领取',
today_limit_date: '今日上限日期', today_limit_date: '今日上限日期',
@@ -12,4 +13,3 @@ export default {
update_time: '修改时间', update_time: '修改时间',
'quick Search Fields': 'ID、playX用户ID、用户名、手机号', 'quick Search Fields': 'ID、playX用户ID、用户名、手机号',
} }

View File

@@ -108,6 +108,9 @@ export default {
mall_order_approve: '审核通过', mall_order_approve: '审核通过',
mall_dailyPush: '每日推送', mall_dailyPush: '每日推送',
mall_claimLog: '领取记录', mall_claimLog: '领取记录',
mall_pointsLog: '积分管理',
mall_pointsLog_index: '查看',
mall_pointsLog_add: '新增',
mall_item: '商品管理', mall_item: '商品管理',
mall_playxOrder: 'playX订单', mall_playxOrder: 'playX订单',
mall_playxCenter: 'playX中心', mall_playxCenter: 'playX中心',

View File

@@ -0,0 +1,150 @@
<template>
<div class="default-main ba-table-box">
<MallPageIntro page-key="pointsLog" />
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<TableHeader
:buttons="['refresh', 'add', 'comSearch', 'quickSearch', 'columnDisplay']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('mall.pointsLog.quick_search_fields') })"
>
<el-button v-if="state.userAsset.id" v-blur class="table-header-operate">
<span class="table-header-operate-text">
{{
state.userAsset.username +
' (ID:' +
state.userAsset.id +
') ' +
t('mall.pointsLog.available_points') +
': ' +
state.userAsset.available_points
}}
</span>
</el-button>
</TableHeader>
<Table />
<PopupForm />
</div>
</template>
<script setup lang="ts">
import { debounce } from 'lodash-es'
import { provide, reactive, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import { getUserAsset, url } from '/@/api/backend/mall/pointsLog'
import { baTableApi } from '/@/api/common'
import MallPageIntro from '/@/components/mall/MallPageIntro.vue'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import baTableClass from '/@/utils/baTable'
import PopupForm from './popupForm.vue'
defineOptions({
name: 'mall/pointsLog',
})
interface UserAssetInfo {
id?: number
username?: string
phone?: string
playx_user_id?: string
available_points?: number
}
const { t } = useI18n()
const route = useRoute()
const routeUserAssetId = route.query.user_asset_id
const defaultUserAssetId = typeof routeUserAssetId === 'string' ? routeUserAssetId : ''
const state = reactive<{ userAsset: UserAssetInfo }>({
userAsset: {},
})
const baTable = new baTableClass(
new baTableApi(url),
{
column: [
{ label: t('mall.pointsLog.id'), prop: 'id', align: 'center', operator: '=', width: 70 },
{ label: t('mall.pointsLog.user_asset_id'), prop: 'user_asset_id', align: 'center', operator: '=', width: 100 },
{
label: t('mall.pointsLog.playx_user_id'),
prop: 'userAsset.playx_user_id',
align: 'center',
operator: 'LIKE',
operatorPlaceholder: t('Fuzzy query'),
},
{
label: t('mall.pointsLog.username'),
prop: 'userAsset.username',
align: 'center',
operator: 'LIKE',
operatorPlaceholder: t('Fuzzy query'),
},
{ label: t('mall.pointsLog.score_value'), prop: 'score_value', align: 'center', operator: 'RANGE', sortable: 'custom' },
{ label: t('mall.pointsLog.before'), prop: 'before', align: 'center', operator: 'RANGE', sortable: 'custom' },
{ label: t('mall.pointsLog.after'), prop: 'after', align: 'center', operator: 'RANGE', sortable: 'custom' },
{
label: t('mall.pointsLog.memo'),
prop: 'memo',
align: 'center',
operator: 'LIKE',
operatorPlaceholder: t('Fuzzy query'),
showOverflowTooltip: true,
},
{ label: t('mall.pointsLog.admin'), prop: 'admin.username', align: 'center', operator: 'LIKE' },
{
label: t('mall.pointsLog.create_time'),
prop: 'create_time',
align: 'center',
render: 'datetime',
sortable: 'custom',
operator: 'RANGE',
width: 160,
},
],
dblClickNotEditColumn: ['all'],
},
{
defaultItems: {
user_asset_id: defaultUserAssetId,
score_value: 0,
},
}
)
const getUserAssetInfo = debounce((userAssetId: unknown) => {
if ((typeof userAssetId !== 'string' && typeof userAssetId !== 'number') || userAssetId === '') {
state.userAsset = {}
return
}
getUserAsset(userAssetId).then((response) => {
state.userAsset = response.data.userAsset
})
}, 300)
baTable.after.onSubmit = () => {
getUserAssetInfo(baTable.comSearch.form.user_asset_id)
}
baTable.after.onTableHeaderAction = ({ event }) => {
if (event == 'refresh') {
getUserAssetInfo(baTable.comSearch.form.user_asset_id)
}
}
baTable.mount()
baTable.getData()
provide('baTable', baTable)
getUserAssetInfo(baTable.comSearch.form.user_asset_id)
watch(
() => baTable.comSearch.form.user_asset_id,
(newValue) => {
baTable.form.defaultItems!.user_asset_id = newValue
getUserAssetInfo(newValue)
}
)
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,173 @@
<template>
<el-dialog class="ba-operate-dialog" :close-on-click-modal="false" :model-value="baTable.form.operate == 'Add'" @close="baTable.toggleForm">
<template #header>
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
{{ baTable.form.operate ? t(baTable.form.operate) : '' }}
</div>
</template>
<el-scrollbar class="ba-table-form-scrollbar">
<div
class="ba-operate-form"
:class="'ba-' + baTable.form.operate + '-form'"
:style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
>
<el-form
v-if="!baTable.form.loading"
ref="formRef"
:model="baTable.form.items"
:label-position="config.layout.shrink ? 'top' : 'right'"
:label-width="baTable.form.labelWidth + 'px'"
:rules="rules"
@keyup.enter="baTable.onSubmit(formRef)"
>
<FormItem
type="remoteSelect"
prop="user_asset_id"
:label="t('mall.pointsLog.user_asset')"
v-model="baTable.form.items!.user_asset_id"
:placeholder="t('Click select')"
:input-attr="{
pk: 'id',
field: 'username',
remoteUrl: '/admin/mall.UserAsset/select',
onChange: getUserAssetInfo,
}"
/>
<el-form-item :label="t('mall.pointsLog.playx_user_id')">
<el-input v-model="state.userAsset.playx_user_id" disabled />
</el-form-item>
<el-form-item :label="t('mall.pointsLog.username')">
<el-input v-model="state.userAsset.username" disabled />
</el-form-item>
<el-form-item :label="t('mall.pointsLog.current_points')">
<el-input v-model="state.userAsset.available_points" disabled />
</el-form-item>
<el-form-item prop="score_value" :label="t('mall.pointsLog.score_value')">
<el-input-number
v-model="baTable.form.items!.score_value"
:precision="0"
:step="1"
controls-position="right"
style="width: 100%"
@change="changeScore"
/>
</el-form-item>
<el-form-item :label="t('mall.pointsLog.after')">
<el-input v-model="state.after" disabled />
</el-form-item>
<el-form-item :label="t('mall.pointsLog.memo')">
<el-input v-model="state.memo" type="textarea" disabled />
</el-form-item>
</el-form>
</div>
</el-scrollbar>
<template #footer>
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
<el-button @click="baTable.toggleForm('')">{{ t('Cancel') }}</el-button>
<el-button v-blur :loading="baTable.form.submitLoading" type="primary" @click="baTable.onSubmit(formRef)">
{{ t('Save') }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import type { FormItemRule } from 'element-plus'
import { inject, reactive, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { getUserAsset } from '/@/api/backend/mall/pointsLog'
import FormItem from '/@/components/formItem/index.vue'
import { useConfig } from '/@/stores/config'
import type baTableClass from '/@/utils/baTable'
import { buildValidatorData } from '/@/utils/validate'
interface UserAssetInfo {
id?: number
username?: string
playx_user_id?: string
available_points?: number
}
const config = useConfig()
const { t } = useI18n()
const injectedBaTable = inject<baTableClass>('baTable')
if (!injectedBaTable) {
throw new Error('baTable is not provided')
}
const baTable = injectedBaTable
const formRef = useTemplateRef('formRef')
const state = reactive<{
userAsset: UserAssetInfo
after: number
memo: string
}>({
userAsset: {},
after: 0,
memo: '',
})
const validateScoreValue: FormItemRule['validator'] = (_rule, value, callback) => {
if (typeof value !== 'number' || !Number.isInteger(value) || value === 0) {
callback(new Error(t('mall.pointsLog.non_zero_integer_required')))
return
}
if ((state.userAsset.available_points ?? 0) + value < 0) {
callback(new Error(t('mall.pointsLog.insufficient_available_points')))
return
}
callback()
}
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
user_asset_id: [buildValidatorData({ name: 'required', title: t('mall.pointsLog.user_asset') })],
score_value: [
{
validator: validateScoreValue,
trigger: 'blur',
},
],
})
const changeScore = (value: number | undefined) => {
const scoreValue = value ?? 0
const availablePoints = state.userAsset.available_points ?? 0
state.after = availablePoints + scoreValue
if (scoreValue > 0) {
state.memo = t('mall.pointsLog.admin_add_memo', { value: Math.abs(scoreValue) })
} else if (scoreValue < 0) {
state.memo = t('mall.pointsLog.admin_deduct_memo', { value: Math.abs(scoreValue) })
} else {
state.memo = ''
}
}
const getUserAssetInfo = () => {
const userAssetId = baTable.form.items!.user_asset_id
if ((typeof userAssetId !== 'string' && typeof userAssetId !== 'number') || userAssetId === '') {
state.userAsset = {}
state.after = 0
state.memo = ''
return
}
getUserAsset(userAssetId).then((response) => {
state.userAsset = response.data.userAsset
changeScore(baTable.form.items!.score_value)
})
}
watch(
() => baTable.form.operate,
(operate) => {
if (operate == 'Add') {
getUserAssetInfo()
}
}
)
</script>
<style scoped lang="scss"></style>

View File

@@ -27,7 +27,13 @@
:label-width="baTable.form.labelWidth + 'px'" :label-width="baTable.form.labelWidth + 'px'"
:rules="rules" :rules="rules"
> >
<FormItem :label="t('mall.userAsset.id')" type="number" v-model="baTable.form.items!.id" prop="id" :input-attr="{ disabled: true }" /> <FormItem
:label="t('mall.userAsset.id')"
type="number"
v-model="baTable.form.items!.id"
prop="id"
:input-attr="{ disabled: true }"
/>
<FormItem <FormItem
:label="t('mall.userAsset.playx_user_id')" :label="t('mall.userAsset.playx_user_id')"
type="string" type="string"
@@ -54,17 +60,15 @@
type="number" type="number"
v-model="baTable.form.items!.locked_points" v-model="baTable.form.items!.locked_points"
prop="locked_points" prop="locked_points"
:input-attr="{ step: 1, min: 0 }" :input-attr="{ disabled: true }"
:placeholder="t('Please input field', { field: t('mall.userAsset.locked_points') })"
/>
<FormItem
:label="t('mall.userAsset.available_points')"
type="number"
v-model="baTable.form.items!.available_points"
prop="available_points"
:input-attr="{ step: 1, min: 0 }"
:placeholder="t('Please input field', { field: t('mall.userAsset.available_points') })"
/> />
<el-form-item :label="t('mall.userAsset.available_points')">
<el-input v-model="baTable.form.items!.available_points" readonly>
<template #append>
<el-button @click="changeAvailablePoints">{{ t('mall.userAsset.adjust_available_points') }}</el-button>
</template>
</el-input>
</el-form-item>
<FormItem <FormItem
:label="t('mall.userAsset.today_limit')" :label="t('mall.userAsset.today_limit')"
type="number" type="number"
@@ -78,8 +82,7 @@
type="number" type="number"
v-model="baTable.form.items!.today_claimed" v-model="baTable.form.items!.today_claimed"
prop="today_claimed" prop="today_claimed"
:input-attr="{ step: 1, min: 0 }" :input-attr="{ disabled: true }"
:placeholder="t('Please input field', { field: t('mall.userAsset.today_claimed') })"
/> />
</el-form> </el-form>
</div> </div>
@@ -104,8 +107,10 @@ import baTableClass from '/@/utils/baTable'
import FormItem from '/@/components/formItem/index.vue' import FormItem from '/@/components/formItem/index.vue'
import type { FormItemRule } from 'element-plus' import type { FormItemRule } from 'element-plus'
import { buildValidatorData } from '/@/utils/validate' import { buildValidatorData } from '/@/utils/validate'
import { useRouter } from 'vue-router'
const config = useConfig() const config = useConfig()
const router = useRouter()
const formRef = useTemplateRef('formRef') const formRef = useTemplateRef('formRef')
const baTable = inject('baTable') as baTableClass const baTable = inject('baTable') as baTableClass
const { t } = useI18n() const { t } = useI18n()
@@ -118,7 +123,17 @@ const rules: Partial<Record<string, FormItemRule[]>> = reactive({
today_limit: [buildValidatorData({ name: 'required', title: t('mall.userAsset.today_limit') })], today_limit: [buildValidatorData({ name: 'required', title: t('mall.userAsset.today_limit') })],
today_claimed: [buildValidatorData({ name: 'required', title: t('mall.userAsset.today_claimed') })], today_claimed: [buildValidatorData({ name: 'required', title: t('mall.userAsset.today_claimed') })],
}) })
const changeAvailablePoints = () => {
const userAssetId = baTable.form.items!.id
baTable.toggleForm()
router.push({
name: 'mall/pointsLog',
query: {
user_asset_id: userAssetId,
},
})
}
</script> </script>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>