index-copy-1.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. <template>
  2. <view class="container">
  3. <div style="display: flex;flex-direction: column;">
  4. <div style="display: flex;">
  5. <button type="primary" style="width:100%;margin-bottom: 10px;" @click="switchRoles">
  6. 交换
  7. </button>
  8. <button type="primary" style="width:100%;margin-bottom: 10px;" @click="calculateBestMatch">
  9. 计算最佳配对
  10. </button>
  11. </div>
  12. <uni-table border stripe emptyText="暂无更多数据">
  13. <uni-tr>
  14. <uni-th width="55px">角色</uni-th>
  15. <uni-th align="left" width="55px">数量</uni-th>
  16. <uni-th align="left" width="55px">性别</uni-th>
  17. <uni-th align="left" width="120px">势力</uni-th>
  18. <uni-th align="left" width="37px">操作</uni-th>
  19. </uni-tr>
  20. <uni-tr v-for="(item, index) in inputData" :key="index">
  21. <uni-td rowspan="1">{{ item.role }}</uni-td>
  22. <uni-td>{{ item.num }}</uni-td>
  23. <uni-td>{{ item.gender }}</uni-td>
  24. <uni-td class="text-power">
  25. <view>{{ item.power }}</view>
  26. </uni-td>
  27. <uni-td>
  28. <uni-icons class="edit-button" type="compose" size="16"
  29. @click="editRow(item.gender, item.role, index)" />
  30. </uni-td>
  31. </uni-tr>
  32. </uni-table>
  33. </div>
  34. <!-- 输出表格 -->
  35. <view class="section" v-if="outputData.length > 0">
  36. <div style="display: flex;justify-content: space-between;margin-top:10px">
  37. <text class="section-title">输出, 计算时间 {{ time }} ms </text>
  38. <span>差额: {{ powerDifference }}</span>
  39. </div>
  40. <uni-table border stripe emptyText="暂无更多数据">
  41. <uni-tr>
  42. <uni-th width="60px" rowspan="2">顺序</uni-th>
  43. <uni-th align="left" colspan="2" width="120px">目标</uni-th>
  44. <uni-th align="left" colspan="2" width="120px">备选</uni-th>
  45. </uni-tr>
  46. <uni-tr>
  47. <uni-th align="left" width="60px">目标</uni-th>
  48. <uni-th align="left" width="60px">备选</uni-th>
  49. <uni-th align="left" width="60px">目标</uni-th>
  50. <uni-th align="left" width="60px">备选</uni-th>
  51. </uni-tr>
  52. <uni-tr v-for="(item, index) in outputData" :key="index">
  53. <uni-td>{{ index + 1 }}</uni-td>
  54. <uni-td>{{ item.targetMale }}</uni-td>
  55. <uni-td>{{ item.targetFemale }}</uni-td>
  56. <uni-td>{{ item.backupMale }}</uni-td>
  57. <uni-td>{{ item.backupFemale }}</uni-td>
  58. </uni-tr>
  59. <uni-tr>
  60. <uni-td>合计</uni-td>
  61. <uni-td colspan="2">{{ totalTarget }}</uni-td>
  62. <uni-td colspan="2">{{ totalBackup }}</uni-td>
  63. </uni-tr>
  64. </uni-table>
  65. </view>
  66. <uni-popup ref="batchRef" :is-mask-click="false">
  67. <div
  68. style="display: flex;flex-direction: column;justify-content: center;padding: 10px; width: 95%;background: #fff;border-radius: 10px;padding: 10px;">
  69. <div style="display: flex;justify-content: space-between;align-items: center;margin-bottom: 10px;">
  70. <span></span>
  71. <h4 style="margin-bottom: 10px"> 编辑{{ role }}</h4>
  72. <uni-icons type="closeempty" size="18" @click="batchRef.close()"></uni-icons>
  73. </div>
  74. <div style="display: flex;justify-content: space-between; align-items: center;" v-if="gender === '公'">
  75. <span class="dialog-title">猫咪势力(公)</span>
  76. <uni-icons type="close" size="18" @click="malePowers = ''"></uni-icons>
  77. </div>
  78. <textarea v-model="powers" placeholder="用逗号或顿号隔开,如:200,10,100"
  79. style="height: 70px;border: 1px solid #dfe2e5" v-if="gender === '公'" />
  80. <div style="display: flex;justify-content: space-between; align-items: center;" v-if="gender === '母'">
  81. <span class="dialog-title">猫咪势力(母)</span>
  82. <uni-icons type="close" size="18" @click="feMalePowers = ''"></uni-icons>
  83. </div>
  84. <textarea v-model="powers" placeholder="用逗号或顿号隔开,如:200,10,100"
  85. style="height: 70px;border: 1px solid #dfe2e5" v-if="gender === '母'" />
  86. <button size='mini' type="primary" @click="handleConfirm">保存</button>
  87. </div>
  88. </uni-popup>
  89. <uni-popup ref="messageRef" type="message">
  90. <uni-popup-message type="error" :message="messageText" :duration="5000"></uni-popup-message>
  91. </uni-popup>
  92. </view>
  93. </template>
  94. <script setup>
  95. import { ref } from 'vue';
  96. const messageText = ref('')
  97. const messageRef = ref(null)
  98. const total = ref(0)
  99. const index = ref(-1)
  100. const time = ref("")
  101. const role = ref("")
  102. const type = ref('')
  103. const gender = ref("")
  104. const powers = ref("")
  105. const malePowers = ref('')
  106. const feMalePowers = ref('')
  107. const totalTarget = ref(0)
  108. const totalBackup = ref(0)
  109. const mockData = [
  110. {
  111. role: '目标',
  112. num: 0,
  113. gender: '公',
  114. power: ''
  115. },
  116. {
  117. role: '目标',
  118. num: 0,
  119. gender: '母',
  120. power: ''
  121. },
  122. {
  123. role: '备选',
  124. num: 0,
  125. gender: '公',
  126. power: ''
  127. },
  128. {
  129. role: '备选',
  130. num: 0,
  131. gender: '母',
  132. power: ''
  133. }
  134. ]
  135. const inputData = ref(mockData);
  136. const outputData = ref([]);
  137. const batchRef = ref(null)
  138. function editRow(_gender, _role, _index) {
  139. type.value = 'edit'
  140. gender.value = _gender
  141. role.value = _role
  142. index.value = _index
  143. const { power } = inputData.value[_index]
  144. powers.value = power
  145. batchRef.value.open()
  146. }
  147. function switchRoles(){
  148. outputData.value = []
  149. const [t1, t2, b1, b2] = inputData.value
  150. inputData.value[0] = { ...b1 }
  151. inputData.value[1] = { ...b2 }
  152. inputData.value[2] = { ...t1 }
  153. inputData.value[3] = { ...t2 }
  154. }
  155. const powerDifference = ref(0)
  156. function isPositiveInteger(str) {
  157. return /^[1-9]\d*$/.test(str);
  158. }
  159. function generatePowers(_powers){
  160. _powers = _powers.replace(/,|、|,/g, ',')
  161. const powerSet = _powers.split(',').map(item => item.trim()).filter(item => item !== '')
  162. const isOk = powerSet.every(num => isPositiveInteger(num))
  163. if (!isOk && powerSet.length !== 0) return false
  164. inputData.value[index.value].num = powerSet.length;
  165. inputData.value[index.value].power = powerSet.join(',')
  166. const [t1, t2] = inputData.value
  167. const t1ps = t1.power.split(',')
  168. const t2ps = t2.power.split(',')
  169. }
  170. function handleConfirm(){
  171. messageRef.value.close()
  172. const isOk = generatePowers(powers.value, '公')
  173. if (typeof isOk === 'boolean' && !isOk){
  174. messageRef.value.open()
  175. messageText.value = '势力必须为数字'
  176. return
  177. }
  178. batchRef.value.close()
  179. }
  180. // 生成数组的所有组合(辅助函数,用于生成指定个数元素的组合情况)
  181. function getCombinations(arr, num) {
  182. const result = [];
  183. if (num === 0) { return [[]]; }
  184. for (let i = 0; i < arr.length; i++) {
  185. const element = arr[i];
  186. const remainingCombos = getCombinations(arr.slice(i + 1), num - 1);
  187. for (const combo of remainingCombos) {
  188. result.push([element].concat(combo));
  189. }
  190. }
  191. return result;
  192. }
  193. function calculateBestMatch() {
  194. let startTime = new Date().getTime()
  195. let endTime = null
  196. outputData.value = []
  197. const targetMale = inputData.value[0].power.split(',').filter(item => item !== '').map(Number)
  198. const targetFemale = inputData.value[1].power.split(',').filter(item => item !== '').map(Number)
  199. const backupMale = inputData.value[2].power.split(',').filter(item => item !== '').map(Number)
  200. const backupFemale = inputData.value[3].power.split(',').filter(item => item !== '').map(Number)
  201. if (targetFemale.length === 0 && targetFemale.length === 0) {
  202. messageRef.value.open()
  203. messageText.value = '目标公数据与母数据不能同时为空'
  204. return
  205. }
  206. if(targetFemale.length + targetFemale.length > 20){
  207. messageRef.value.open()
  208. messageText.value = '目标公数据与母数据的和不能超过20条'
  209. return
  210. }
  211. if(backupMale.length > 20){
  212. messageRef.value.open()
  213. messageText.value = '备选的公数据不能超过20条'
  214. return
  215. }
  216. if(backupFemale.length > 20){
  217. messageRef.value.open()
  218. messageText.value = '备选的母数据不能超过20条'
  219. return
  220. }
  221. if (targetFemale.length > backupMale.length && backupMale.length !== 0){
  222. messageRef.value.open()
  223. messageText.value = '备选的公数据长度必须大于或等于目标的母数据'
  224. return
  225. }
  226. if (targetMale.length > backupFemale.length && backupFemale.length !== 0) {
  227. messageRef.value.open()
  228. messageText.value = '备选的母数据长度必须或等于大于目标的公数据'
  229. return
  230. }
  231. const targetMaleLen = targetMale.length;
  232. const targetFemaleLen = targetFemale.length;
  233. let targetWeight = 0
  234. if (backupMale.length === 0){
  235. targetWeight = targetMale.reduce((acc, val) => acc + val, 0);
  236. } else if(backupFemale.length === 0){
  237. targetWeight = targetFemale.reduce((acc, val) => acc + val, 0)
  238. } else if(backupMale.length !== 0 && backupFemale.length !== 0){
  239. targetWeight = targetMale.reduce((acc, val) => acc + val, 0) + targetFemale.reduce((acc, val) => acc + val, 0);
  240. }
  241. let minDifference = Infinity;
  242. let bestfemaleCombination = null;
  243. let bestMaleCombination = null;
  244. const femaleCombinations = getCombinations(backupFemale, targetMaleLen);
  245. const maleCombinations = getCombinations(backupMale, targetFemaleLen);
  246. if (femaleCombinations.length !== 0 && maleCombinations.length !== 0){
  247. for (const femaleComb of femaleCombinations) {
  248. for (const maleComb of maleCombinations) {
  249. const totalWeight = femaleComb.reduce((acc, val) => acc + val, 0) + maleComb.reduce((acc, val) => acc + val, 0);
  250. const difference = Math.abs(totalWeight - targetWeight);
  251. if (difference < minDifference) {
  252. minDifference = difference;
  253. bestfemaleCombination = femaleComb;
  254. bestMaleCombination = maleComb;
  255. if (minDifference == 0) {
  256. break;
  257. }
  258. }
  259. }
  260. }
  261. }
  262. // 备选数据的母为空
  263. if (femaleCombinations.length === 0 && maleCombinations.length !== 0) {
  264. for (const maleComb of maleCombinations) {
  265. const totalWeight = maleComb.reduce((acc, val) => acc + val , 0);
  266. const difference = Math.abs(totalWeight - targetWeight);
  267. if (difference < minDifference) {
  268. minDifference = difference;
  269. bestMaleCombination = maleComb;
  270. if (minDifference == 0) {
  271. break;
  272. }
  273. }
  274. }
  275. }
  276. // 备选数据的公为空
  277. if (femaleCombinations.length !== 0 && maleCombinations.length === 0) {
  278. for (const femaleComb of femaleCombinations) {
  279. const totalWeight = femaleComb.reduce((acc, val) => acc + val, 0)
  280. const difference = Math.abs(totalWeight - targetWeight);
  281. if (difference < minDifference) {
  282. minDifference = difference;
  283. bestfemaleCombination = femaleComb;
  284. // bestMaleCombination = maleComb;
  285. if (minDifference == 0) {
  286. break;
  287. }
  288. }
  289. }
  290. }
  291. bestMaleCombination && bestMaleCombination.sort((a, b) => b - a)
  292. bestfemaleCombination && bestfemaleCombination.sort((a, b) => b - a)
  293. targetMale.sort((a, b) => b - a)
  294. targetFemale.sort((a, b) => b - a)
  295. if (!bestMaleCombination && !bestfemaleCombination){ return }
  296. bestfemaleCombination && targetMale.forEach((_, index) => {
  297. outputData.value.push({
  298. targetMale: targetMale[index],
  299. targetFemale: null,
  300. backupMale: null,
  301. backupFemale: bestfemaleCombination[index]
  302. })
  303. })
  304. const fl = targetFemale.length
  305. for(let index = 0; index < fl; index ++){
  306. if(outputData.value[index]){
  307. outputData.value[index].targetFemale = targetFemale[index]
  308. outputData.value[index].backupMale = bestMaleCombination[index]
  309. } else {
  310. outputData.value.push({
  311. targetMale: null,
  312. targetFemale: targetFemale[index],
  313. backupMale: bestMaleCombination[index],
  314. backupFemale: null
  315. })
  316. }
  317. }
  318. totalTarget.value = outputData.value.reduce((prev, current) => prev + current.targetMale, 0) + outputData.value.reduce((prev, current) => prev + current.targetFemale, 0)
  319. totalBackup.value = outputData.value.reduce((prev, current) => prev + current.backupMale, 0) + outputData.value.reduce((prev, current) => prev + current.backupFemale, 0)
  320. powerDifference.value = totalTarget.value - totalBackup.value
  321. endTime = new Date().getTime()
  322. time.value = endTime - startTime
  323. }
  324. </script>
  325. <style>
  326. .text-power {
  327. width: 120px;
  328. max-width: 120px;
  329. overflow: hidden;
  330. text-overflow: ellipsis;
  331. text-wrap: nowrap;
  332. /* white-space: pre-wrap; */
  333. }
  334. .edit-button {
  335. color: #409eff !important;
  336. cursor: pointer;
  337. }
  338. textarea {
  339. font-size: 13px;
  340. padding: 8px;
  341. }
  342. </style>
  343. <style lang="scss" scoped>
  344. button {
  345. height: 30px;
  346. display: flex;
  347. align-items: center;
  348. justify-content: center;
  349. font-size: 14px;
  350. }
  351. .dialog-title {
  352. line-height: 30px;
  353. padding-left: 10px;
  354. font-size: 14px;
  355. }
  356. .table-wrapper {
  357. font-size: 14px !important;
  358. }
  359. .custom-picker {
  360. border: 1px solid #e3e3e3;
  361. padding: 3px 5px;
  362. text-align: center;
  363. width: 100%;
  364. div {
  365. display: flex;
  366. justify-content: space-between;
  367. text {
  368. overflow: hidden;
  369. white-space: nowrap;
  370. text-overflow: ellipsis;
  371. }
  372. }
  373. }
  374. .container {
  375. padding: 10px 20px;
  376. }
  377. .section {
  378. margin-bottom: 20px;
  379. }
  380. .section-title {
  381. font-size: 18px;
  382. font-weight: bold;
  383. margin-bottom: 10px;
  384. }
  385. .input {
  386. width: 100%;
  387. text-align: center;
  388. border: 1px solid #ddd;
  389. padding: 5px;
  390. }
  391. button {
  392. margin: 5px;
  393. }
  394. .add-btn,
  395. .calculate-btn {
  396. background-color: #007bff;
  397. color: white;
  398. padding: 10px;
  399. font-size: 16px;
  400. text-align: center;
  401. border-radius: 5px;
  402. }
  403. .delete-btn {
  404. background-color: #ff4d4f;
  405. color: white;
  406. padding: 5px;
  407. font-size: 14px;
  408. border-radius: 5px;
  409. }
  410. .summary {
  411. margin-top: 20px;
  412. font-size: 14px;
  413. text-align: right;
  414. font-weight: bold;
  415. }
  416. </style>