index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. <template>
  2. <div class="p-2">
  3. <el-row :gutter="20">
  4. <!-- 左侧操作区 -->
  5. <el-col :span="6">
  6. <el-card shadow="hover" class="mb-[10px]">
  7. <template #header>
  8. <span class="font-bold">道具核销操作</span>
  9. </template>
  10. <el-form label-width="80px" size="default">
  11. <el-form-item label="选择商品">
  12. <el-radio-group v-model="data2.leftForm.selectedItem">
  13. <el-radio label="1001">三湘杯资格卡</el-radio>
  14. </el-radio-group>
  15. </el-form-item>
  16. <el-form-item label="核销数量">
  17. <el-input-number v-model="data2.leftForm.num" :min="1" placeholder="请输入数量" class="w-full" />
  18. </el-form-item>
  19. <el-form-item label="搜索用户">
  20. <el-input v-model="data2.leftForm.searchUserKeyword" placeholder="用户名/手机号" clearable>
  21. <template #append>
  22. <el-button icon="Search" @click="handleSearchUser()" />
  23. </template>
  24. </el-input>
  25. </el-form-item>
  26. <el-table :data="data2.userItemsList" border fit highlight-current-row style="width: 100%; margin-top: 10px">
  27. <el-table-column prop="itemName" label="道具名称" align="center" />
  28. <el-table-column prop="quantity" label="持有数量" align="center" />
  29. <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
  30. <template #default="scope">
  31. <el-tooltip content="核销" placement="top">
  32. <el-button link type="danger" icon="Edit" @click="dealDedution(scope.row)" v-hasPermi="['business:checkRecord:add']"
  33. >核销</el-button
  34. >
  35. </el-tooltip>
  36. </template>
  37. </el-table-column>
  38. </el-table>
  39. </el-form>
  40. </el-card>
  41. </el-col>
  42. <!-- 右侧数据展示区 -->
  43. <el-col :span="18">
  44. <!-- 原来的搜索 + 表格区域 -->
  45. <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
  46. <div v-show="showSearch" class="mb-[10px]">
  47. <el-card shadow="hover">
  48. <el-form ref="queryFormRef" :model="data.queryParams" :inline="true">
  49. <el-form-item label="" prop="userId">
  50. <el-input v-model="data.queryParams.userIds" placeholder="请输入用户名/手机号" clearable @keyup.enter="handleQuery" />
  51. </el-form-item>
  52. <el-form-item label="核销时间">
  53. <el-date-picker
  54. v-model="data.queryParams.loginTimeRange"
  55. type="datetimerange"
  56. range-separator="至"
  57. start-placeholder="开始时间"
  58. end-placeholder="结束时间"
  59. value-format="YYYY-MM-DD HH:mm:ss"
  60. format="YYYY-MM-DD HH:mm:ss"
  61. />
  62. </el-form-item>
  63. <el-form-item>
  64. <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
  65. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  66. </el-form-item>
  67. </el-form>
  68. </el-card>
  69. </div>
  70. </transition>
  71. <el-card shadow="never">
  72. <el-table v-loading="loading" border :data="checkRecordList" @selection-change="handleSelectionChange">
  73. <el-table-column label="序号" width="60" align="center">
  74. <template #default="{ $index }">
  75. {{ $index + 1 + (data.queryParams.pageNum - 1) * data.queryParams.pageSize }}
  76. </template>
  77. </el-table-column>
  78. <el-table-column label="用户名" align="center" prop="nickName" />
  79. <el-table-column label="用户手机号" align="center" prop="phone" />
  80. <el-table-column label="道具名" align="center" prop="itemsName" />
  81. <el-table-column label="核销数量" align="center" prop="num" />
  82. <el-table-column label="核销时间" align="center" prop="createdAt" width="180">
  83. <template #default="scope">
  84. <span>{{ parseTime(scope.row.createdAt, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
  85. </template>
  86. </el-table-column>
  87. </el-table>
  88. <pagination
  89. v-show="total > 0"
  90. :total="total"
  91. v-model:page="data.queryParams.pageNum"
  92. v-model:limit="data.queryParams.pageSize"
  93. @pagination="getList"
  94. />
  95. </el-card>
  96. </el-col>
  97. </el-row>
  98. <!-- 弹窗等其他内容保持不变 -->
  99. <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
  100. <!-- 表单内容 -->
  101. </el-dialog>
  102. </div>
  103. </template>
  104. <script setup name="CheckRecord" lang="ts">
  105. import {
  106. listCheckRecord,
  107. getCheckRecord,
  108. delCheckRecord,
  109. addCheckRecord,
  110. updateCheckRecord,
  111. selectPlayerItemsListByUser
  112. } from '@/api/system/business/checkRecord';
  113. import { CheckRecordVO, CheckRecordQuery, CheckRecordForm } from '@/api/system/business/checkRecord/types';
  114. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  115. const checkRecordList = ref<CheckRecordVO[]>([]);
  116. const buttonLoading = ref(false);
  117. const loading = ref(true);
  118. const showSearch = ref(true);
  119. const ids = ref<Array<string | number>>([]);
  120. const single = ref(true);
  121. const multiple = ref(true);
  122. const total = ref(0);
  123. const queryFormRef = ref<ElFormInstance>();
  124. const checkRecordFormRef = ref<ElFormInstance>();
  125. const dialog = reactive<DialogOption>({
  126. visible: false,
  127. title: ''
  128. });
  129. const initFormData: CheckRecordForm = {
  130. id: undefined,
  131. userId: undefined,
  132. num: undefined,
  133. itemId: undefined,
  134. createdAt: undefined,
  135. updatedAt: undefined,
  136. createUserId: undefined,
  137. createUserName: undefined
  138. };
  139. const data = reactive<PageData<CheckRecordForm, CheckRecordQuery>>({
  140. form: { ...initFormData },
  141. queryParams: {
  142. pageNum: 1,
  143. pageSize: 10,
  144. userId: undefined,
  145. num: undefined,
  146. itemId: undefined,
  147. createdAt: undefined,
  148. updatedAt: undefined,
  149. createUserId: undefined,
  150. createUserName: undefined,
  151. params: {}
  152. },
  153. rules: {
  154. id: [{ required: true, message: '不能为空', trigger: 'blur' }]
  155. }
  156. });
  157. const { queryParams, form, rules } = toRefs(data);
  158. /** 查询用户核销记录列表 */
  159. const getList = async () => {
  160. loading.value = true;
  161. const res = await listCheckRecord(queryParams.value);
  162. checkRecordList.value = res.rows;
  163. total.value = res.total;
  164. loading.value = false;
  165. };
  166. /** 取消按钮 */
  167. const cancel = () => {
  168. reset();
  169. dialog.visible = false;
  170. };
  171. /** 表单重置 */
  172. const reset = () => {
  173. form.value = { ...initFormData };
  174. checkRecordFormRef.value?.resetFields();
  175. };
  176. /** 搜索按钮操作 */
  177. const handleQuery = () => {
  178. queryParams.value.pageNum = 1;
  179. const timeRange = data.queryParams.loginTimeRange;
  180. if (timeRange && timeRange.length === 2) {
  181. data.queryParams.beginTime = timeRange[0];
  182. data.queryParams.endTime = timeRange[1];
  183. } else {
  184. data.queryParams.beginTime = null;
  185. data.queryParams.endTime = null;
  186. }
  187. getList();
  188. };
  189. /** 重置按钮操作 */
  190. const resetQuery = () => {
  191. queryFormRef.value?.resetFields();
  192. handleQuery();
  193. };
  194. /** 多选框选中数据 */
  195. const handleSelectionChange = (selection: CheckRecordVO[]) => {
  196. ids.value = selection.map((item) => item.id);
  197. single.value = selection.length != 1;
  198. multiple.value = !selection.length;
  199. };
  200. /** 新增按钮操作 */
  201. const handleAdd = () => {
  202. reset();
  203. dialog.visible = true;
  204. dialog.title = '添加用户核销记录';
  205. };
  206. /** 修改按钮操作 */
  207. const handleUpdate = async (row?: CheckRecordVO) => {
  208. reset();
  209. const _id = row?.id || ids.value[0];
  210. const res = await getCheckRecord(_id);
  211. Object.assign(form.value, res.data);
  212. dialog.visible = true;
  213. dialog.title = '修改用户核销记录';
  214. };
  215. /** 提交按钮 */
  216. const submitForm = () => {
  217. checkRecordFormRef.value?.validate(async (valid: boolean) => {
  218. if (valid) {
  219. buttonLoading.value = true;
  220. if (form.value.id) {
  221. await updateCheckRecord(form.value).finally(() => (buttonLoading.value = false));
  222. } else {
  223. await addCheckRecord(form.value).finally(() => (buttonLoading.value = false));
  224. }
  225. proxy?.$modal.msgSuccess('操作成功');
  226. dialog.visible = false;
  227. await getList();
  228. }
  229. });
  230. };
  231. /** 删除按钮操作 */
  232. const handleDelete = async (row?: CheckRecordVO) => {
  233. const _ids = row?.id || ids.value;
  234. await proxy?.$modal.confirm('是否确认删除用户核销记录编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
  235. await delCheckRecord(_ids);
  236. proxy?.$modal.msgSuccess('删除成功');
  237. await getList();
  238. };
  239. /** 导出按钮操作 */
  240. const handleExport = () => {
  241. proxy?.download(
  242. 'business/checkRecord/export',
  243. {
  244. ...queryParams.value
  245. },
  246. `checkRecord_${new Date().getTime()}.xlsx`
  247. );
  248. };
  249. onMounted(() => {
  250. getList();
  251. });
  252. // 类型定义(建议补充)
  253. interface LeftSideForm {
  254. selectedItem: '1001'; // 👈 设置默认选中 '1001'(即“三湘杯资格卡”)
  255. num: number | null; // 核销数量
  256. searchUserKeyword: string; // 搜索用户关键词
  257. }
  258. interface UserItemVO {
  259. itemId: number;
  260. itemName: string;
  261. quantity: number;
  262. phone: string;
  263. nickName: string;
  264. userId: number;
  265. }
  266. const initLeftFormData: LeftSideForm = {
  267. selectedItem: '1001', // ✅ 直接赋值
  268. num: null,
  269. searchUserKeyword: ''
  270. };
  271. // 扩展 data 类型
  272. const data2 = reactive<
  273. PageData<CheckRecordForm, CheckRecordQuery> & {
  274. leftForm: LeftSideForm;
  275. userItemsList: UserItemVO[];
  276. }
  277. >({
  278. form: { ...initFormData },
  279. queryParams: {
  280. pageNum: 1,
  281. pageSize: 10,
  282. userId: undefined,
  283. num: undefined,
  284. itemId: undefined,
  285. createdAt: undefined,
  286. updatedAt: undefined,
  287. createUserId: undefined,
  288. createUserName: undefined,
  289. params: {}
  290. },
  291. rules: {
  292. id: [{ required: true, message: 'ID不能为空', trigger: 'blur' }]
  293. },
  294. // 左侧新增字段
  295. leftForm: { ...initLeftFormData },
  296. userItemsList: [] // 用户拥有的道具列表
  297. });
  298. // 搜索用户并加载其道具列表
  299. const handleSearchUser = async () => {
  300. data2.userItemsList = []; // 清空列表
  301. const keyword = data2.leftForm.searchUserKeyword;
  302. if (!keyword.trim()) {
  303. ElMessage.warning('请输入用户名或手机号');
  304. return;
  305. }
  306. try {
  307. const res = await selectPlayerItemsListByUser(keyword);
  308. data2.userItemsList = res.data; // ✅ 注意:AxiosPromise<R<T>> 返回的是 { data: T },res.data 就是 PlayerItemsVo[]
  309. if (res.data.length <= 0) {
  310. ElMessage.error('暂无数据');
  311. }
  312. } catch (error) {
  313. ElMessage.error('查询失败');
  314. data2.userItemsList = []; // 清空列表
  315. }
  316. };
  317. const dealDedution = async (row?: UserItemVO) => {
  318. if (!row) {
  319. ElMessage.warning('请选择一条记录');
  320. return;
  321. }
  322. // ✅ 校验核销数量
  323. const num = data2.leftForm.num;
  324. if (!num) {
  325. ElMessage.warning('请先输入核销数量,才能核销');
  326. return;
  327. }
  328. if (!Number.isInteger(Number(num)) || Number(num) <= 0) {
  329. ElMessage.warning('核销数量必须是正整数');
  330. return;
  331. }
  332. const { phone, nickName, itemName, userId, itemId } = row;
  333. // 构建提示信息
  334. const message = `
  335. <div style="text-align: center;">
  336. <p>是否扣除用户:<strong>${nickName} (${phone})</strong></p>
  337. <p>[${itemName}] X ${data2.leftForm.num ?? 0}</p>
  338. <p style="color: #999; margin-top: 10px;">本次操作不可逆!请确认后继续操作!</p>
  339. </div>
  340. `;
  341. try {
  342. // 弹出确认框
  343. await ElMessageBox.confirm(message, '确认核销', {
  344. confirmButtonText: '确认核销',
  345. cancelButtonText: '取消',
  346. type: 'warning',
  347. dangerouslyUseHTMLString: true,
  348. center: true,
  349. customClass: 'custom-message-box' // 自定义类名,用于样式调整
  350. });
  351. // 构造核销记录数据
  352. const recordData: CheckRecordForm = {
  353. userId,
  354. itemId,
  355. num: data2.leftForm.num
  356. };
  357. // 填入 form.value 并提交
  358. Object.assign(form.value, recordData);
  359. buttonLoading.value = true;
  360. try {
  361. await addCheckRecord(form.value);
  362. ElMessage.success('核销成功');
  363. dialog.visible = false;
  364. // 刷新核销记录列表
  365. await getList();
  366. // 可选:刷新左侧用户道具列表
  367. handleSearchUser();
  368. } catch (error) {
  369. //ElMessage.error('核销失败,请重试');
  370. } finally {
  371. buttonLoading.value = false;
  372. }
  373. } catch (cancel) {
  374. // 用户点击取消,不做任何事
  375. ElMessage.info('已取消核销');
  376. }
  377. };
  378. </script>
  379. <style>
  380. /* 在全局样式文件或当前组件的 <style> 中添加 */
  381. .custom-message-box {
  382. .el-message-box__header {
  383. border-bottom: 1px solid #ebeef5;
  384. }
  385. .el-message-box__title {
  386. font-size: 16px;
  387. color: #303133;
  388. }
  389. .el-message-box__content {
  390. padding: 20px;
  391. }
  392. .el-message-box__btns {
  393. text-align: center;
  394. }
  395. .el-button--primary {
  396. background-color: #409eff;
  397. border-color: #409eff;
  398. color: #fff;
  399. }
  400. .el-button--default {
  401. background-color: #fff;
  402. border-color: #dcdfe6;
  403. color: #606266;
  404. }
  405. }
  406. </style>