index.vue 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874
  1. <template>
  2. <div class="p-2">
  3. <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
  4. <div v-show="showSearch" class="mb-[10px]">
  5. <el-card shadow="hover">
  6. <el-form ref="queryFormRef" :model="queryParams" :inline="true">
  7. <el-form-item label="等级" prop="levelNumber">
  8. <el-input v-model="queryParams.levelNumber" placeholder="请输入等级" clearable @keyup.enter="handleQuery" />
  9. </el-form-item>
  10. <el-form-item label="小盲" prop="smallBlind">
  11. <el-input v-model="queryParams.smallBlind" placeholder="请输入小盲" clearable @keyup.enter="handleQuery" />
  12. </el-form-item>
  13. <el-form-item label="大盲" prop="bigBlind">
  14. <el-input v-model="queryParams.bigBlind" placeholder="请输入大盲" clearable @keyup.enter="handleQuery" />
  15. </el-form-item>
  16. <el-form-item label="前注" prop="ante">
  17. <el-input v-model="queryParams.ante" placeholder="请输入前注" clearable @keyup.enter="handleQuery" />
  18. </el-form-item>
  19. <el-form-item label="盲注分钟" prop="durationMinutes">
  20. <el-input v-model="queryParams.durationMinutes" placeholder="请输入盲注分钟" clearable @keyup.enter="handleQuery" />
  21. </el-form-item>
  22. <el-form-item>
  23. <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
  24. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  25. </el-form-item>
  26. </el-form>
  27. </el-card>
  28. </div>
  29. </transition>
  30. <div class="blind-levels-container">
  31. <el-card shadow="never" class="blind-levels-card">
  32. <template #header>
  33. <el-row :gutter="10" class="mb8">
  34. <el-col :span="1.5">
  35. <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['physical:blindLevels:add']">新增</el-button>
  36. </el-col>
  37. <el-col :span="1.5">
  38. <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['physical:blindLevels:edit']">修改</el-button>
  39. </el-col>
  40. <el-col :span="1.5">
  41. <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['physical:blindLevels:remove']">删除</el-button>
  42. </el-col>
  43. <el-col :span="1.5">
  44. <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['physical:blindLevels:export']">导出</el-button>
  45. </el-col>
  46. <el-col :span="1.5">
  47. <el-button type="primary" plain icon="Upload" @click="handleImport" v-hasPermi="['physical:blindLevels:import']">导入</el-button>
  48. </el-col>
  49. <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
  50. </el-row>
  51. </template>
  52. <el-table v-loading="loading" border :data="blindLevelsList" @selection-change="handleSelectionChange" row-key="id" :row-class-name="tableRowClassName">
  53. <el-table-column type="selection" width="55" align="center" :reserve-selection="true" />
  54. <el-table-column label="显示级别" align="center" width="120">
  55. <template #default="scope">
  56. <span v-if="scope.row.isBreak === 1" style="color: #e6a23c; font-weight: bold">{{ getDisplayLevel(scope.row) }}</span>
  57. <span v-else>{{ getDisplayLevel(scope.row) }}</span>
  58. </template>
  59. </el-table-column>
  60. <el-table-column label="小盲" align="center">
  61. <template #default="scope">
  62. <div class="stepper-cell">
  63. <el-button size="small" text @click="stepValue(scope.row, 'smallBlind', -1)">−</el-button>
  64. <span class="stepper-value">{{ scope.row.smallBlind ?? 0 }}</span>
  65. <el-button size="small" text @click="stepValue(scope.row, 'smallBlind', 1)">+</el-button>
  66. </div>
  67. </template>
  68. </el-table-column>
  69. <el-table-column label="大盲" align="center">
  70. <template #default="scope">
  71. <div class="stepper-cell">
  72. <el-button size="small" text @click="stepValue(scope.row, 'bigBlind', -1)">−</el-button>
  73. <span class="stepper-value">{{ scope.row.bigBlind ?? 0 }}</span>
  74. <el-button size="small" text @click="stepValue(scope.row, 'bigBlind', 1)">+</el-button>
  75. </div>
  76. </template>
  77. </el-table-column>
  78. <el-table-column label="前注" align="center">
  79. <template #default="scope">
  80. <div class="stepper-cell">
  81. <el-button size="small" text @click="stepValue(scope.row, 'ante', -1)">−</el-button>
  82. <span class="stepper-value">{{ scope.row.ante ?? 0 }}</span>
  83. <el-button size="small" text @click="stepValue(scope.row, 'ante', 1)">+</el-button>
  84. </div>
  85. </template>
  86. </el-table-column>
  87. <el-table-column label="盲注分钟" align="center" width="140">
  88. <template #default="scope">
  89. <div class="stepper-cell">
  90. <el-button size="small" text @click="stepValue(scope.row, 'durationMinutes', -1)">−</el-button>
  91. <span class="stepper-value">{{ scope.row.durationMinutes ?? 0 }}</span>
  92. <el-button size="small" text @click="stepValue(scope.row, 'durationMinutes', 1)">+</el-button>
  93. </div>
  94. </template>
  95. </el-table-column>
  96. <el-table-column label="休息级别" align="center" width="120">
  97. <template #default="scope">
  98. <el-select v-model="scope.row.isBreak" size="small" style="width: 100px">
  99. <el-option label="否" :value="0" />
  100. <el-option label="是" :value="1" />
  101. </el-select>
  102. </template>
  103. </el-table-column>
  104. <el-table-column label="休息分钟" align="center" width="120">
  105. <template #default="scope">
  106. <div class="stepper-cell">
  107. <el-button size="small" text :disabled="scope.row.isBreak !== 1" @click="stepValue(scope.row, 'breakDurationMinutes', -1)">−</el-button>
  108. <span class="stepper-value" :style="{ opacity: scope.row.isBreak !== 1 ? 0.4 : 1 }">{{ scope.row.breakDurationMinutes ?? 0 }}</span>
  109. <el-button size="small" text :disabled="scope.row.isBreak !== 1" @click="stepValue(scope.row, 'breakDurationMinutes', 1)">+</el-button>
  110. </div>
  111. </template>
  112. </el-table-column>
  113. <el-table-column label="名次" align="center" width="120">
  114. <template #default="scope">
  115. <div class="stepper-cell">
  116. <el-button size="small" text @click="stepValue(scope.row, 'displayOrder', -1)">−</el-button>
  117. <span class="stepper-value">{{ scope.row.displayOrder ?? 0 }}</span>
  118. <el-button size="small" text @click="stepValue(scope.row, 'displayOrder', 1)">+</el-button>
  119. </div>
  120. </template>
  121. </el-table-column>
  122. <el-table-column label="操作" align="center" width="140">
  123. <template #default="scope">
  124. <el-button link type="primary" size="small" :loading="insertLoading" @click="handleInsertAfter(scope.row)">插入</el-button>
  125. <el-button link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
  126. </template>
  127. </el-table-column>
  128. </el-table>
  129. <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
  130. <div class="blind-levels-footer">
  131. <el-button type="primary" :loading="saveLoading" @click="handleBatchSave">保存</el-button>
  132. <el-button @click="handleClose">关闭</el-button>
  133. </div>
  134. </el-card>
  135. </div>
  136. <!-- 添加或修改线下盲注等级对话框 -->
  137. <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
  138. <el-form ref="blindLevelsFormRef" :model="form" :rules="rules" label-width="80px">
  139. <el-form-item label="所属盲注" prop="name">
  140. <el-input v-model="form.name" disabled />
  141. </el-form-item>
  142. <el-form-item label="等级" prop="levelNumber">
  143. <el-input v-model="form.levelNumber" placeholder="请输入等级" />
  144. </el-form-item>
  145. <el-form-item label="小盲" prop="smallBlind">
  146. <el-input v-model="form.smallBlind" placeholder="请输入小盲" />
  147. </el-form-item>
  148. <el-form-item label="大盲" prop="bigBlind">
  149. <el-input v-model="form.bigBlind" placeholder="请输入大盲" />
  150. </el-form-item>
  151. <el-form-item label="前注" prop="ante">
  152. <el-input v-model="form.ante" placeholder="请输入前注" />
  153. </el-form-item>
  154. <el-form-item label="盲注分钟" prop="durationMinutes">
  155. <el-input v-model="form.durationMinutes" placeholder="请输入盲注分钟" />
  156. </el-form-item>
  157. <el-form-item label="名次" prop="displayOrder">
  158. <el-input v-model="form.displayOrder" placeholder="请输入名次" />
  159. </el-form-item>
  160. <el-form-item label="是否休息" prop="isBreak">
  161. <el-radio-group v-model="form.isBreak">
  162. <el-radio :label="1">是</el-radio>
  163. <el-radio :label="0">否</el-radio>
  164. </el-radio-group>
  165. </el-form-item>
  166. <el-form-item label="休息时长" prop="breakDurationMinutes">
  167. <el-input v-model="form.breakDurationMinutes" placeholder="请输入休息时长" />
  168. </el-form-item>
  169. <el-form-item label="备注" prop="remark">
  170. <el-input v-model="form.remark" placeholder="请输入备注" />
  171. </el-form-item>
  172. </el-form>
  173. <template #footer>
  174. <div class="dialog-footer">
  175. <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
  176. <el-button @click="cancel">取 消</el-button>
  177. </div>
  178. </template>
  179. </el-dialog>
  180. <!-- 导入对话框 -->
  181. <el-dialog :title="importDialog.title" v-model="importDialog.visible" width="500px" append-to-body>
  182. <el-upload
  183. ref="uploadRef"
  184. :limit="1"
  185. accept=".xlsx, .xls"
  186. :headers="importDialog.headers"
  187. :action="importDialog.url"
  188. :disabled="importDialog.isUploading"
  189. :on-change="handleFileChange"
  190. :on-progress="handleFileUploadProgress"
  191. :on-success="handleFileSuccess"
  192. :auto-upload="false"
  193. drag
  194. >
  195. <el-icon class="el-icon--upload"><upload-filled /></el-icon>
  196. <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
  197. <template #tip>
  198. <div class="el-upload__tip text-center">
  199. <div class="el-upload__tip">
  200. <el-checkbox v-model="importDialog.updateSupport" />是否更新已经存在的数据
  201. </div>
  202. <span>仅允许导入xls、xlsx格式文件。</span>
  203. <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="handleDownloadTemplate">下载模板</el-link>
  204. </div>
  205. </template>
  206. </el-upload>
  207. <template #footer>
  208. <div class="dialog-footer">
  209. <el-button type="primary" @click="submitFileForm">确 定</el-button>
  210. <el-button @click="importDialog.visible = false; uploadFile.value = null; uploadRef.value?.clearFiles()">取 消</el-button>
  211. </div>
  212. </template>
  213. </el-dialog>
  214. </div>
  215. </template>
  216. <script setup name="BlindLevels" lang="ts">
  217. import {
  218. listBlindLevels,
  219. getBlindLevels,
  220. delBlindLevels,
  221. addBlindLevels,
  222. updateBlindLevels,
  223. downloadImportTemplate,
  224. importData,
  225. insertAfter,
  226. batchSave
  227. } from '@/api/system/physical/blindLevels';
  228. import { BlindLevelsVO, BlindLevelsQuery, BlindLevelsForm } from '@/api/system/physical/blindLevels/types';
  229. import { parseTime } from '@/utils/dateUtils';
  230. import { useBlindStructureInfo } from '@/api/system/business/levels/useBlindStructureId';
  231. import { getToken } from '@/utils/auth';
  232. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  233. const blindLevelsList = ref<BlindLevelsVO[]>([]);
  234. const buttonLoading = ref(false);
  235. const saveLoading = ref(false);
  236. const insertLoading = ref(false);
  237. const loading = ref(true);
  238. const showSearch = ref(true);
  239. const ids = ref<Array<string | number>>([]);
  240. const single = ref(true);
  241. const multiple = ref(true);
  242. const total = ref(0);
  243. const queryFormRef = ref<ElFormInstance>();
  244. const blindLevelsFormRef = ref<ElFormInstance>();
  245. const uploadRef = ref<ElUploadInstance>();
  246. const uploadFile = ref<File | null>(null);
  247. const rowErrors = ref<Map<string | number, string[]>>(new Map());
  248. const props = defineProps({
  249. blindStructureId: {
  250. type: [Number, String],
  251. required: false,
  252. default: null
  253. },
  254. name: {
  255. type: String,
  256. required: false,
  257. default: ''
  258. }
  259. });
  260. const { info } = useBlindStructureInfo(props);
  261. const currentId = ref<number | string | null>(null);
  262. const currentName = ref('');
  263. const dialog = reactive<DialogOption>({
  264. visible: false,
  265. title: ''
  266. });
  267. const importDialog = reactive({
  268. visible: false,
  269. title: '导入盲注等级数据',
  270. isUploading: false,
  271. updateSupport: false,
  272. headers: { Authorization: 'Bearer ' + getToken() },
  273. url: import.meta.env.VITE_APP_BASE_API + '/physical/blindLevels/importData'
  274. });
  275. const initFormData: BlindLevelsForm = {
  276. id: undefined,
  277. name: undefined,
  278. blindStructureId: undefined,
  279. levelNumber: undefined,
  280. smallBlind: undefined,
  281. bigBlind: undefined,
  282. ante: undefined,
  283. durationMinutes: undefined,
  284. createdAt: undefined,
  285. updatedAt: undefined,
  286. createUserId: undefined,
  287. updateUserId: undefined,
  288. isBreak: 0,
  289. breakDurationMinutes: undefined,
  290. remark: undefined,
  291. displayOrder: undefined
  292. };
  293. const data = reactive<PageData<BlindLevelsForm, BlindLevelsQuery>>({
  294. form: { ...initFormData },
  295. queryParams: {
  296. pageNum: 1,
  297. pageSize: 10,
  298. blindStructureId: undefined,
  299. levelNumber: undefined,
  300. smallBlind: undefined,
  301. bigBlind: undefined,
  302. ante: undefined,
  303. durationMinutes: undefined,
  304. createdAt: undefined,
  305. updatedAt: undefined,
  306. createUserId: undefined,
  307. updateUserId: undefined,
  308. isBreak: undefined,
  309. breakDurationMinutes: undefined,
  310. displayOrder: undefined,
  311. params: {},
  312. // ✅ 核心修复:强制按levelNumber升序排序,解决插入后位置错乱问题
  313. orderByColumn: 'levelNumber',
  314. isAsc: 'asc'
  315. },
  316. rules: {
  317. id: [{ required: true, message: '不能为空', trigger: 'blur' }],
  318. levelNumber: [{ required: true, message: '级别编号不能为空', trigger: 'blur' }],
  319. smallBlind: [{ required: true, message: '小盲金额不能为空', trigger: 'blur' }],
  320. bigBlind: [{ required: true, message: '大盲金额不能为空', trigger: 'blur' }],
  321. durationMinutes: [{ required: true, message: '持续时间不能为空', trigger: 'blur' }]
  322. }
  323. });
  324. const { queryParams, form, rules } = toRefs(data);
  325. /** 查询线下盲注等级列表 */
  326. const getList = async () => {
  327. loading.value = true;
  328. const res = await listBlindLevels(queryParams.value);
  329. // 确保 isBreak 为数字类型,避免 select 显示 false/true,并按 levelNumber 升序排序
  330. // 统一排序逻辑:与线下盲注表保持一致,按级别编号排序而非ID排序
  331. blindLevelsList.value = (res.rows || [])
  332. .map((item: any) => ({
  333. ...item,
  334. isBreak: item.isBreak === true || item.isBreak === 1 ? 1 : 0
  335. }))
  336. .sort((a, b) => {
  337. const levelA = a.levelNumber || 0;
  338. const levelB = b.levelNumber || 0;
  339. return levelA - levelB;
  340. });
  341. total.value = res.total;
  342. loading.value = false;
  343. };
  344. const sortListByLevelNumber = () => {
  345. blindLevelsList.value.sort((a, b) => {
  346. const levelA = a.levelNumber || 0;
  347. const levelB = b.levelNumber || 0;
  348. return levelA - levelB;
  349. });
  350. };
  351. /** 计算显示级别:休息行不占级别,非休息行连续计算(如:1、2、休息(第2级后)、3、4...) */
  352. const getDisplayLevel = (row: BlindLevelsVO): string | number => {
  353. const idx = blindLevelsList.value.findIndex((r) => r.id === row.id);
  354. if (idx === -1) return row.levelNumber;
  355. const nonBreakBefore = blindLevelsList.value.slice(0, idx).filter((r) => r.isBreak !== 1).length;
  356. if (row.isBreak === 1) {
  357. return `休息(第${nonBreakBefore}级后)`;
  358. }
  359. return nonBreakBefore + 1;
  360. };
  361. /** 取消按钮 */
  362. const cancel = () => {
  363. reset();
  364. dialog.visible = false;
  365. };
  366. /** 表单重置 */
  367. const reset = () => {
  368. form.value = { ...initFormData };
  369. blindLevelsFormRef.value?.resetFields();
  370. };
  371. /** 搜索按钮操作 */
  372. const handleQuery = () => {
  373. queryParams.value.pageNum = 1;
  374. getList();
  375. };
  376. /** 重置按钮操作 */
  377. const resetQuery = () => {
  378. queryFormRef.value?.resetFields();
  379. handleQuery();
  380. };
  381. /** 多选框选中数据 */
  382. const handleSelectionChange = (selection: BlindLevelsVO[]) => {
  383. ids.value = selection.map((item) => item.id);
  384. single.value = selection.length != 1;
  385. multiple.value = !selection.length;
  386. };
  387. /** 新增按钮操作 */
  388. const handleAdd = () => {
  389. reset();
  390. dialog.visible = true;
  391. dialog.title = '添加';
  392. };
  393. /** 修改按钮操作 */
  394. const handleUpdate = async (row?: BlindLevelsVO) => {
  395. reset();
  396. const _id = row?.id || ids.value[0];
  397. const res = await getBlindLevels(_id);
  398. Object.assign(form.value, res.data);
  399. dialog.visible = true;
  400. dialog.title = '修改';
  401. };
  402. /** 提交按钮 */
  403. const submitForm = () => {
  404. blindLevelsFormRef.value?.validate(async (valid: boolean) => {
  405. if (valid) {
  406. buttonLoading.value = true;
  407. if (form.value.id) {
  408. await updateBlindLevels(form.value).finally(() => (buttonLoading.value = false));
  409. } else {
  410. await addBlindLevels(form.value).finally(() => (buttonLoading.value = false));
  411. }
  412. proxy?.$modal.msgSuccess('操作成功');
  413. dialog.visible = false;
  414. await getList();
  415. }
  416. });
  417. };
  418. /** 删除按钮操作 */
  419. const handleDelete = async (row?: BlindLevelsVO) => {
  420. // 行内删除:未保存的临时数据,直接移除不弹框
  421. if (row && (row as any)._isNew) {
  422. const idx = blindLevelsList.value.findIndex((r) => r.id === row.id);
  423. if (idx !== -1) {
  424. blindLevelsList.value.splice(idx, 1);
  425. }
  426. proxy?.$modal.msgSuccess('删除成功');
  427. return;
  428. }
  429. // 已保存数据或批量删除:弹确认后调API
  430. const _ids = row?.id || ids.value;
  431. await proxy?.$modal.confirm('是否确认删除编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
  432. await delBlindLevels(_ids);
  433. proxy?.$modal.msgSuccess('删除成功');
  434. await getList();
  435. };
  436. /** 导出按钮操作 */
  437. const handleExport = () => {
  438. proxy?.download(
  439. 'physical/blindLevels/export',
  440. {
  441. ...queryParams.value
  442. },
  443. `盲注等级详情${parseTime(new Date(), '{y}{m}{d}{h}{i}{s}')}.xlsx`
  444. );
  445. };
  446. /** 导入按钮操作 */
  447. const handleImport = () => {
  448. importDialog.visible = true;
  449. };
  450. /** 下载模板 */
  451. const handleDownloadTemplate = async () => {
  452. try {
  453. const response = await downloadImportTemplate();
  454. const blobData = new Blob([response.data || response], {
  455. type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  456. });
  457. const urlBlob = window.URL.createObjectURL(blobData);
  458. const link = document.createElement('a');
  459. link.href = urlBlob;
  460. link.setAttribute('download', '盲注等级模板.xlsx');
  461. document.body.appendChild(link);
  462. link.click();
  463. setTimeout(() => {
  464. link.remove();
  465. window.URL.revokeObjectURL(urlBlob);
  466. }, 0);
  467. } catch (error) {
  468. console.error('Download error:', error);
  469. ElMessage.error('下载失败,请重试');
  470. }
  471. };
  472. /** 文件上传中处理 */
  473. const handleFileUploadProgress = () => {
  474. importDialog.isUploading = true;
  475. };
  476. /** 文件上传成功处理 */
  477. const handleFileSuccess = async (response: any, file: any) => {
  478. importDialog.isUploading = false;
  479. uploadRef.value?.clearFiles();
  480. importDialog.visible = false;
  481. if (response.code === 200) {
  482. proxy?.$modal.msgSuccess('导入成功');
  483. await getList();
  484. } else {
  485. proxy?.$modal.msgError(response.msg || '导入失败');
  486. }
  487. };
  488. /** 文件选择变化 */
  489. const handleFileChange = (file: any) => {
  490. uploadFile.value = file.raw;
  491. };
  492. /** 提交上传文件 */
  493. const submitFileForm = () => {
  494. if (!uploadFile.value) {
  495. proxy?.$modal.msgError('请先选择文件');
  496. return;
  497. }
  498. if (!currentId.value) {
  499. proxy?.$modal.msgError('盲注结构ID不能为空');
  500. return;
  501. }
  502. importDialog.isUploading = true;
  503. importData(uploadFile.value, currentId.value)
  504. .then((response: any) => {
  505. importDialog.isUploading = false;
  506. uploadFile.value = null;
  507. uploadRef.value?.clearFiles();
  508. importDialog.visible = false;
  509. if (response.code === 200) {
  510. proxy?.$modal.msgSuccess('导入成功');
  511. getList();
  512. } else {
  513. proxy?.$modal.msgError(response.msg || '导入失败');
  514. }
  515. })
  516. .catch((error) => {
  517. importDialog.isUploading = false;
  518. proxy?.$modal.msgError('导入失败,请重试');
  519. console.error('Import error:', error);
  520. });
  521. };
  522. /** 行内步进器增减(步长=1) */
  523. const stepValue = (row: BlindLevelsVO, field: 'smallBlind' | 'bigBlind' | 'ante' | 'durationMinutes' | 'breakDurationMinutes' | 'displayOrder', delta: number) => {
  524. const val = (row[field] as number) || 0;
  525. (row as any)[field] = Math.max(0, val + delta);
  526. };
  527. /** 表格行样式(有错误的行标红) */
  528. const tableRowClassName = ({ row }: { row: BlindLevelsVO }) => {
  529. if (rowErrors.value.has(row.id)) {
  530. return 'row-error';
  531. }
  532. return '';
  533. };
  534. /** 校验所有行数据 */
  535. const validateRows = (): string[] => {
  536. const allErrors: string[] = [];
  537. rowErrors.value.clear();
  538. blindLevelsList.value.forEach((row) => {
  539. const errors: string[] = [];
  540. if (!row.levelNumber || row.levelNumber <= 0) {
  541. errors.push(`级别${row.levelNumber || '?'}:级别编号不能为空`);
  542. }
  543. if (row.smallBlind === null || row.smallBlind === undefined || row.smallBlind < 0) {
  544. errors.push(`级别${row.levelNumber || '?'}:小盲金额不能为空`);
  545. }
  546. if (row.bigBlind === null || row.bigBlind === undefined || row.bigBlind < 0) {
  547. errors.push(`级别${row.levelNumber || '?'}:大盲金额不能为空`);
  548. }
  549. if (row.bigBlind !== null && row.bigBlind !== undefined && row.smallBlind !== null && row.smallBlind !== undefined && row.bigBlind < row.smallBlind) {
  550. errors.push(`级别${row.levelNumber || '?'}:大盲金额不能小于小盲金额`);
  551. }
  552. if (row.durationMinutes === null || row.durationMinutes === undefined || row.durationMinutes < 0) {
  553. errors.push(`级别${row.levelNumber || '?'}:本级别持续时间(分钟)不能为空`);
  554. }
  555. if (row.isBreak === 1 && (!row.breakDurationMinutes || row.breakDurationMinutes <= 0)) {
  556. errors.push(`级别${row.levelNumber || '?'}:休息级别为“是”时,休息分钟不能为0`);
  557. }
  558. if (errors.length > 0) {
  559. rowErrors.value.set(row.id, errors);
  560. allErrors.push(...errors);
  561. }
  562. });
  563. return allErrors;
  564. };
  565. /** 在指定级别后插入(默认全0,本地插入不刷新列表) */
  566. const handleInsertAfter = async (row: BlindLevelsVO) => {
  567. if (insertLoading.value) return; // 防止重复点击
  568. insertLoading.value = true;
  569. const newLevelNumber = (row.levelNumber || 0) + 1;
  570. const newLevel: BlindLevelsForm = {
  571. id: undefined,
  572. blindStructureId: currentId.value as string | number,
  573. levelNumber: newLevelNumber,
  574. smallBlind: 0,
  575. bigBlind: 0,
  576. ante: 0,
  577. durationMinutes: 0,
  578. isBreak: 0,
  579. breakDurationMinutes: 0,
  580. displayOrder: 0
  581. };
  582. try {
  583. await insertAfter(newLevel, row.levelNumber);
  584. // 关键修复:插入新级别后,将后续所有记录的levelNumber+1,避免编号重复
  585. blindLevelsList.value.forEach((item) => {
  586. if (item.levelNumber >= newLevelNumber && item.id !== row.id) {
  587. item.levelNumber += 1;
  588. }
  589. });
  590. // 本地插入新行到正确位置
  591. const idx = blindLevelsList.value.findIndex((r) => r.id === row.id);
  592. if (idx !== -1) {
  593. blindLevelsList.value.splice(idx + 1, 0, { ...newLevel, id: Date.now(), _isNew: true } as any);
  594. // 按levelNumber排序确保顺序正确
  595. sortListByLevelNumber();
  596. }
  597. proxy?.$modal.msgSuccess(`成功在第${row.levelNumber}级后插入第${newLevelNumber}级`);
  598. } catch (error) {
  599. proxy?.$modal.msgError('插入失败,请重试');
  600. console.error('Insert after error:', error);
  601. } finally {
  602. insertLoading.value = false;
  603. }
  604. };
  605. /** 批量保存(先拉取全部数据,合并当前页编辑后一起提交) */
  606. const handleBatchSave = async () => {
  607. if (!blindLevelsList.value || blindLevelsList.value.length === 0) {
  608. proxy?.$modal.msgError('没有数据可保存');
  609. return;
  610. }
  611. // 校验当前页
  612. const errors = validateRows();
  613. if (errors.length > 0) {
  614. proxy?.$modal.msgError(errors[0]);
  615. return;
  616. }
  617. saveLoading.value = true;
  618. try {
  619. // ✅ 关键修复:只收集已存在记录的编辑内容(不包括新插入的临时记录)
  620. // 原因:新插入的记录已经通过 insertAfter() API 在后端创建了,
  621. // 如果再提交会导致重复数据!
  622. const localEdits = new Map<string | number, BlindLevelsVO>();
  623. blindLevelsList.value.forEach((row) => {
  624. if (row.id && !(row as any)._isNew) {
  625. // 只收集已存在的记录,忽略 _isNew 标记的临时记录
  626. localEdits.set(row.id, row);
  627. }
  628. });
  629. // 拉取全部数据(不分页,强制按levelNumber排序)
  630. const allRes = await listBlindLevels({
  631. ...queryParams.value,
  632. pageNum: 1,
  633. pageSize: 9999999,
  634. orderByColumn: 'levelNumber',
  635. isAsc: 'asc'
  636. });
  637. let allRows: BlindLevelsVO[] = (allRes.rows || []).map((item: any) => ({
  638. ...item,
  639. isBreak: item.isBreak === true || item.isBreak === 1 ? 1 : 0
  640. }));
  641. // 用本地编辑覆盖全量数据中对应的行(包括对新插入记录的编辑)
  642. allRows.forEach((row) => {
  643. if (row.id && localEdits.has(row.id)) {
  644. Object.assign(row, localEdits.get(row.id));
  645. }
  646. });
  647. // ✅ 特殊处理:将用户对新插入记录的编辑合并到后端返回的对应记录上
  648. blindLevelsList.value.forEach((localRow) => {
  649. if ((localRow as any)._isNew && localRow.levelNumber) {
  650. // 找到后端数据中相同 levelNumber 的记录(就是刚才 insertAfter 创建的)
  651. const backendRow = allRows.find(r => r.levelNumber === localRow.levelNumber);
  652. if (backendRow) {
  653. // 将用户的编辑内容合并到后端记录上
  654. Object.assign(backendRow, {
  655. smallBlind: localRow.smallBlind,
  656. bigBlind: localRow.bigBlind,
  657. ante: localRow.ante,
  658. durationMinutes: localRow.durationMinutes,
  659. isBreak: localRow.isBreak,
  660. breakDurationMinutes: localRow.breakDurationMinutes
  661. });
  662. }
  663. }
  664. });
  665. // ✅ 保存前自动修正levelNumber,确保连续且不重复
  666. allRows.sort((a, b) => (a.levelNumber || 0) - (b.levelNumber || 0));
  667. let expectedLevel = 1;
  668. allRows.forEach((row) => {
  669. row.levelNumber = expectedLevel++;
  670. });
  671. console.log('💾 准备保存的数据:', allRows); // 调试日志
  672. await batchSave(allRows as BlindLevelsForm[]);
  673. proxy?.$modal.msgSuccess('保存成功');
  674. await getList();
  675. } catch (error) {
  676. proxy?.$modal.msgError('保存失败,请重试');
  677. console.error('Batch save error:', error);
  678. } finally {
  679. saveLoading.value = false;
  680. }
  681. };
  682. /** 关闭 */
  683. const handleClose = () => {
  684. // 如果在弹窗中使用,关闭弹窗
  685. const parentDialog = (proxy?.$parent as any)?.$parent;
  686. if (parentDialog && parentDialog.$options?.name !== 'BlindLevels') {
  687. // 尝试找到父级 dialog 并关闭
  688. let el: any = proxy?.$el;
  689. while (el) {
  690. if (el.classList && el.classList.contains('el-dialog')) {
  691. const closeBtn = el.querySelector('.el-dialog__headerbtn');
  692. if (closeBtn) closeBtn.click();
  693. break;
  694. }
  695. el = el.parentElement;
  696. }
  697. }
  698. };
  699. onMounted(() => {
  700. const id = currentId;
  701. if (id !== undefined) {
  702. queryParams.value.blindStructureId = currentId.value;
  703. initFormData.name = currentName.value;
  704. }
  705. getList();
  706. // 将保存/关闭按钮移到父级 dialog 的 footer 区域,使其固定不随滚动
  707. nextTick(() => {
  708. const footerEl = (proxy?.$el as HTMLElement)?.querySelector('.blind-levels-footer') as HTMLElement;
  709. if (!footerEl) return;
  710. let dialogEl: HTMLElement | null = footerEl.closest('.el-dialog');
  711. if (!dialogEl) {
  712. let el: HTMLElement | null = footerEl.parentElement;
  713. while (el) {
  714. if (el.classList?.contains('el-dialog')) {
  715. dialogEl = el;
  716. break;
  717. }
  718. el = el.parentElement;
  719. }
  720. }
  721. if (dialogEl) {
  722. let dialogFooter = dialogEl.querySelector('.el-dialog__footer') as HTMLElement;
  723. if (!dialogFooter) {
  724. dialogFooter = document.createElement('div');
  725. dialogFooter.className = 'el-dialog__footer';
  726. dialogEl.appendChild(dialogFooter);
  727. }
  728. // 清空旧的 footer 内容,避免重复
  729. dialogFooter.innerHTML = '';
  730. dialogFooter.appendChild(footerEl);
  731. footerEl.style.position = '';
  732. footerEl.style.bottom = '';
  733. footerEl.style.background = '';
  734. footerEl.style.borderRadius = '';
  735. footerEl.style.boxShadow = '';
  736. footerEl.style.zIndex = '';
  737. footerEl.style.padding = '10px 0 0';
  738. }
  739. });
  740. });
  741. watch(
  742. () => info(),
  743. (newInfo) => {
  744. if (newInfo.id !== currentId.value || newInfo.name !== currentName.value) {
  745. currentId.value = newInfo.id;
  746. currentName.value = newInfo.name;
  747. initFormData.blindStructureId = newInfo.id;
  748. initFormData.name = newInfo.name;
  749. queryParams.value.blindStructureId = newInfo.id;
  750. if (newInfo.id !== null) {
  751. getList();
  752. }
  753. }
  754. },
  755. { immediate: true }
  756. );
  757. // ✅ 暴露刷新方法给父组件调用
  758. defineExpose({
  759. refreshList: getList
  760. });
  761. </script>
  762. <style scoped>
  763. .blind-levels-container {
  764. height: 100%;
  765. }
  766. .blind-levels-card {
  767. height: 100%;
  768. overflow: auto;
  769. }
  770. .blind-levels-footer {
  771. display: flex;
  772. justify-content: flex-end;
  773. gap: 10px;
  774. padding: 10px 0 0;
  775. }
  776. .stepper-cell {
  777. display: flex;
  778. align-items: center;
  779. justify-content: center;
  780. gap: 4px;
  781. }
  782. .stepper-cell .el-button {
  783. padding: 2px 8px;
  784. font-size: 14px;
  785. min-width: 28px;
  786. color: #606266;
  787. }
  788. .stepper-cell .el-button:hover:not(:disabled) {
  789. color: #409eff;
  790. }
  791. .stepper-cell .el-button:disabled {
  792. color: #c0c4cc;
  793. cursor: not-allowed;
  794. }
  795. .stepper-value {
  796. display: inline-block;
  797. min-width: 60px;
  798. text-align: center;
  799. padding: 4px 12px;
  800. background: #f5f7fa;
  801. border-radius: 4px;
  802. font-size: 14px;
  803. color: #606266;
  804. }
  805. :deep(.row-error) {
  806. background-color: #fef0f0 !important;
  807. }
  808. :deep(.row-error td) {
  809. color: #f56c6c;
  810. }
  811. </style>