index-copy-1.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  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.close()
  175. messageRef.value.open()
  176. messageText.value = '势力必须为数字'
  177. return
  178. }
  179. batchRef.value.close()
  180. }
  181. // 生成数组的所有组合(辅助函数,用于生成指定个数元素的组合情况)
  182. function getCombinations(arr, num) {
  183. const result = [];
  184. if (num === 0) { return [[]]; }
  185. for (let i = 0; i < arr.length; i++) {
  186. const element = arr[i];
  187. const remainingCombos = getCombinations(arr.slice(i + 1), num - 1);
  188. for (const combo of remainingCombos) {
  189. result.push([element].concat(combo));
  190. }
  191. }
  192. return result;
  193. }
  194. function calculateBestMatch() {
  195. let startTime = new Date().getTime()
  196. let endTime = null
  197. outputData.value = []
  198. const targetMale = inputData.value[0].power.split(',').filter(item => item !== '').map(Number)
  199. const targetFemale = inputData.value[1].power.split(',').filter(item => item !== '').map(Number)
  200. const backupMale = inputData.value[2].power.split(',').filter(item => item !== '').map(Number)
  201. const backupFemale = inputData.value[3].power.split(',').filter(item => item !== '').map(Number)
  202. if (targetFemale.length === 0 && targetFemale.length === 0) {
  203. messageRef.value.open()
  204. messageText.value = '目标公数据与母数据不能同时为空'
  205. return
  206. }
  207. if(targetFemale.length + targetFemale.length > 20){
  208. messageRef.value.open()
  209. messageText.value = '目标公数据与母数据的和不能超过20条'
  210. return
  211. }
  212. if(backupMale.length > 20){
  213. messageRef.value.open()
  214. messageText.value = '备选的公数据不能超过20条'
  215. return
  216. }
  217. if(backupFemale.length > 20){
  218. messageRef.value.open()
  219. messageText.value = '备选的母数据不能超过20条'
  220. return
  221. }
  222. if (targetFemale.length > backupMale.length && backupMale.length !== 0){
  223. messageRef.value.open()
  224. messageText.value = '备选的公数据长度必须大于或等于目标的母数据'
  225. return
  226. }
  227. if (targetMale.length > backupFemale.length && backupFemale.length !== 0) {
  228. messageRef.value.open()
  229. messageText.value = '备选的母数据长度必须或等于大于目标的公数据'
  230. return
  231. }
  232. const targetMaleLen = targetMale.length;
  233. const targetFemaleLen = targetFemale.length;
  234. let targetWeight = 0
  235. if (backupMale.length === 0){
  236. targetWeight = targetMale.reduce((acc, val) => acc + val, 0);
  237. } else if(backupFemale.length === 0){
  238. targetWeight = targetFemale.reduce((acc, val) => acc + val, 0)
  239. } else if(backupMale.length !== 0 && backupFemale.length !== 0){
  240. targetWeight = targetMale.reduce((acc, val) => acc + val, 0) + targetFemale.reduce((acc, val) => acc + val, 0);
  241. }
  242. let minDifference = Infinity;
  243. let bestfemaleCombination = null;
  244. let bestMaleCombination = null;
  245. const femaleCombinations = getCombinations(backupFemale, targetMaleLen);
  246. const maleCombinations = getCombinations(backupMale, targetFemaleLen);
  247. if (femaleCombinations.length !== 0 && maleCombinations.length !== 0){
  248. for (const femaleComb of femaleCombinations) {
  249. for (const maleComb of maleCombinations) {
  250. const totalWeight = femaleComb.reduce((acc, val) => acc + val, 0) + maleComb.reduce((acc, val) => acc + val, 0);
  251. const difference = Math.abs(totalWeight - targetWeight);
  252. if (difference < minDifference) {
  253. minDifference = difference;
  254. bestfemaleCombination = femaleComb;
  255. bestMaleCombination = maleComb;
  256. if (minDifference == 0) {
  257. break;
  258. }
  259. }
  260. }
  261. }
  262. }
  263. // 备选数据的母为空
  264. if (femaleCombinations.length === 0 && maleCombinations.length !== 0) {
  265. for (const maleComb of maleCombinations) {
  266. const totalWeight = maleComb.reduce((acc, val) => acc + val , 0);
  267. const difference = Math.abs(totalWeight - targetWeight);
  268. if (difference < minDifference) {
  269. minDifference = difference;
  270. bestMaleCombination = maleComb;
  271. if (minDifference == 0) {
  272. break;
  273. }
  274. }
  275. }
  276. }
  277. // 备选数据的公为空
  278. if (femaleCombinations.length !== 0 && maleCombinations.length === 0) {
  279. for (const femaleComb of femaleCombinations) {
  280. const totalWeight = femaleComb.reduce((acc, val) => acc + val, 0)
  281. const difference = Math.abs(totalWeight - targetWeight);
  282. if (difference < minDifference) {
  283. minDifference = difference;
  284. bestfemaleCombination = femaleComb;
  285. // bestMaleCombination = maleComb;
  286. if (minDifference == 0) {
  287. break;
  288. }
  289. }
  290. }
  291. }
  292. bestMaleCombination && bestMaleCombination.sort((a, b) => b - a)
  293. bestfemaleCombination && bestfemaleCombination.sort((a, b) => b - a)
  294. targetMale.sort((a, b) => b - a)
  295. targetFemale.sort((a, b) => b - a)
  296. if (!bestMaleCombination && !bestfemaleCombination){ return }
  297. bestfemaleCombination && targetMale.forEach((_, index) => {
  298. outputData.value.push({
  299. targetMale: targetMale[index],
  300. targetFemale: null,
  301. backupMale: null,
  302. backupFemale: bestfemaleCombination[index]
  303. })
  304. })
  305. const fl = targetFemale.length
  306. for(let index = 0; index < fl; index ++){
  307. if(outputData.value[index]){
  308. outputData.value[index].targetFemale = targetFemale[index]
  309. outputData.value[index].backupMale = bestMaleCombination[index]
  310. } else {
  311. outputData.value.push({
  312. targetMale: null,
  313. targetFemale: targetFemale[index],
  314. backupMale: bestMaleCombination[index],
  315. backupFemale: null
  316. })
  317. }
  318. }
  319. totalTarget.value = outputData.value.reduce((prev, current) => prev + current.targetMale, 0) + outputData.value.reduce((prev, current) => prev + current.targetFemale, 0)
  320. totalBackup.value = outputData.value.reduce((prev, current) => prev + current.backupMale, 0) + outputData.value.reduce((prev, current) => prev + current.backupFemale, 0)
  321. powerDifference.value = totalTarget.value - totalBackup.value
  322. endTime = new Date().getTime()
  323. time.value = endTime - startTime
  324. }
  325. </script>
  326. <style>
  327. .text-power {
  328. width: 120px;
  329. max-width: 120px;
  330. overflow: hidden;
  331. text-overflow: ellipsis;
  332. text-wrap: nowrap;
  333. /* white-space: pre-wrap; */
  334. }
  335. .edit-button {
  336. color: #409eff !important;
  337. cursor: pointer;
  338. }
  339. textarea {
  340. font-size: 13px;
  341. padding: 8px;
  342. }
  343. </style>
  344. <style lang="scss" scoped>
  345. button {
  346. height: 30px;
  347. display: flex;
  348. align-items: center;
  349. justify-content: center;
  350. font-size: 14px;
  351. }
  352. .dialog-title {
  353. line-height: 30px;
  354. padding-left: 10px;
  355. font-size: 14px;
  356. }
  357. .table-wrapper {
  358. font-size: 14px !important;
  359. }
  360. .custom-picker {
  361. border: 1px solid #e3e3e3;
  362. padding: 3px 5px;
  363. text-align: center;
  364. width: 100%;
  365. div {
  366. display: flex;
  367. justify-content: space-between;
  368. text {
  369. overflow: hidden;
  370. white-space: nowrap;
  371. text-overflow: ellipsis;
  372. }
  373. }
  374. }
  375. .container {
  376. padding: 10px 20px;
  377. }
  378. .section {
  379. margin-bottom: 20px;
  380. }
  381. .section-title {
  382. font-size: 18px;
  383. font-weight: bold;
  384. margin-bottom: 10px;
  385. }
  386. .input {
  387. width: 100%;
  388. text-align: center;
  389. border: 1px solid #ddd;
  390. padding: 5px;
  391. }
  392. button {
  393. margin: 5px;
  394. }
  395. .add-btn,
  396. .calculate-btn {
  397. background-color: #007bff;
  398. color: white;
  399. padding: 10px;
  400. font-size: 16px;
  401. text-align: center;
  402. border-radius: 5px;
  403. }
  404. .delete-btn {
  405. background-color: #ff4d4f;
  406. color: white;
  407. padding: 5px;
  408. font-size: 14px;
  409. border-radius: 5px;
  410. }
  411. .summary {
  412. margin-top: 20px;
  413. font-size: 14px;
  414. text-align: right;
  415. font-weight: bold;
  416. }
  417. </style>