paymentEntry.vue 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. <template>
  2. <div class="payment-entry-page">
  3. <div class="page-toolbar">
  4. <div>
  5. <h2>支付入口</h2>
  6. <p>定义支付从哪里发起,并为每个入口配置可用的支付方式</p>
  7. </div>
  8. <el-button type="primary" icon="Plus" @click="openCreate">新增支付入口</el-button>
  9. </div>
  10. <el-table v-loading="loading" :data="entries" border>
  11. <el-table-column label="支付入口" min-width="190">
  12. <template #default="{ row }">
  13. <div class="entry-name">
  14. <span>{{ row.name }}</span>
  15. <el-tooltip v-if="row.is_system" content="系统内置入口,只能调整配置,不能删除" placement="top">
  16. <el-icon class="locked"><Lock /></el-icon>
  17. </el-tooltip>
  18. </div>
  19. <span class="entry-code">{{ row.code }}</span>
  20. </template>
  21. </el-table-column>
  22. <el-table-column label="可用支付方式" min-width="280">
  23. <template #default="{ row }">
  24. <el-tag v-for="code in row.method_codes" :key="code" class="method-tag" effect="plain">
  25. {{ methodLabels[code] || code }}
  26. </el-tag>
  27. <span v-if="!row.method_codes?.length" class="muted">未配置</span>
  28. </template>
  29. </el-table-column>
  30. <el-table-column label="排序" prop="sort" width="80" align="center" />
  31. <el-table-column label="状态" width="110" align="center">
  32. <template #default="{ row }">
  33. <el-switch
  34. v-model="row.enabled"
  35. inline-prompt
  36. active-text="启用"
  37. inactive-text="停用"
  38. @change="changeStatus(row)"
  39. />
  40. </template>
  41. </el-table-column>
  42. <el-table-column label="说明" prop="description" min-width="240" show-overflow-tooltip />
  43. <el-table-column label="操作" width="150" fixed="right" align="center">
  44. <template #default="{ row }">
  45. <el-button text type="primary" icon="Edit" @click="openEdit(row)">编辑</el-button>
  46. <el-button text type="danger" icon="Delete" :disabled="row.is_system" @click="removeEntry(row)">删除</el-button>
  47. </template>
  48. </el-table-column>
  49. <template #empty>
  50. <el-empty description="暂无支付入口" />
  51. </template>
  52. </el-table>
  53. <el-dialog v-model="dialogVisible" :title="editingID ? '编辑支付入口' : '新增支付入口'" width="620px" destroy-on-close>
  54. <el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
  55. <el-form-item label="名称" prop="name">
  56. <el-input v-model="form.name" maxlength="50" placeholder="例如:收费岗亭、停车小票" />
  57. </el-form-item>
  58. <el-form-item label="入口编码" prop="code">
  59. <el-input v-model="form.code" :disabled="Boolean(editingID)" maxlength="32" placeholder="例如:counter" />
  60. <div class="form-hint">保存后编码不可修改,业务记录会使用该编码关联入口</div>
  61. </el-form-item>
  62. <el-form-item label="支付方式" prop="method_codes">
  63. <el-checkbox-group v-model="form.method_codes">
  64. <el-checkbox v-for="method in methods" :key="method.code" :label="method.code" :disabled="method.code === 'cash'">
  65. {{ method.name }}
  66. </el-checkbox>
  67. </el-checkbox-group>
  68. <div class="form-hint">现金支付是基础方式,始终保留在入口中;其它方式可按入口单独启停。</div>
  69. </el-form-item>
  70. <el-form-item label="排序">
  71. <el-input-number v-model="form.sort" :min="0" :max="999" controls-position="right" />
  72. </el-form-item>
  73. <el-form-item label="启用">
  74. <el-switch v-model="form.enabled" />
  75. </el-form-item>
  76. <el-form-item label="说明">
  77. <el-input v-model="form.description" type="textarea" :rows="3" maxlength="200" show-word-limit />
  78. </el-form-item>
  79. </el-form>
  80. <template #footer>
  81. <el-button @click="dialogVisible = false">取消</el-button>
  82. <el-button type="primary" :loading="saving" @click="saveEntry">保存</el-button>
  83. </template>
  84. </el-dialog>
  85. </div>
  86. </template>
  87. <script setup>
  88. import { onMounted, reactive, ref } from 'vue'
  89. import { ElMessage, ElMessageBox } from 'element-plus'
  90. import {
  91. createPaymentEntry,
  92. deletePaymentEntry,
  93. getPaymentEntries,
  94. getPaymentMethods,
  95. setPaymentEntryStatus,
  96. updatePaymentEntry
  97. } from '@/api/payment'
  98. const codePattern = /^[a-z][a-z0-9_]{1,31}$/
  99. const entries = ref([])
  100. const methods = ref([])
  101. const methodLabels = reactive({})
  102. const loading = ref(false)
  103. const saving = ref(false)
  104. const dialogVisible = ref(false)
  105. const editingID = ref(0)
  106. const formRef = ref()
  107. const form = reactive(defaultForm())
  108. const rules = {
  109. name: [{ required: true, message: '请输入支付入口名称', trigger: 'blur' }],
  110. code: [
  111. { required: true, message: '请输入入口编码', trigger: 'blur' },
  112. { pattern: codePattern, message: '以小写字母开头,只能包含小写字母、数字和下划线', trigger: 'blur' }
  113. ],
  114. method_codes: [{ type: 'array', required: true, min: 1, message: '请至少选择一种支付方式', trigger: 'change' }]
  115. }
  116. function defaultForm() {
  117. return { code: '', name: '', method_codes: ['cash'], enabled: true, sort: 50, description: '' }
  118. }
  119. async function loadData() {
  120. loading.value = true
  121. try {
  122. const [entryRes, methodRes] = await Promise.all([getPaymentEntries(), getPaymentMethods()])
  123. if (entryRes.code === 0) entries.value = entryRes.data || []
  124. if (methodRes.code === 0) {
  125. methods.value = (methodRes.data || []).filter(item => !item.is_legacy)
  126. Object.keys(methodLabels).forEach(key => delete methodLabels[key])
  127. methods.value.forEach(item => { methodLabels[item.code] = item.name })
  128. }
  129. } finally {
  130. loading.value = false
  131. }
  132. }
  133. function openCreate() {
  134. editingID.value = 0
  135. Object.assign(form, defaultForm())
  136. dialogVisible.value = true
  137. }
  138. function openEdit(row) {
  139. editingID.value = row.ID
  140. Object.assign(form, {
  141. code: row.code,
  142. name: row.name,
  143. method_codes: [...(row.method_codes || ['cash'])],
  144. enabled: row.enabled,
  145. sort: row.sort,
  146. description: row.description || ''
  147. })
  148. if (!form.method_codes.includes('cash')) form.method_codes.unshift('cash')
  149. dialogVisible.value = true
  150. }
  151. async function saveEntry() {
  152. await formRef.value.validate()
  153. saving.value = true
  154. try {
  155. const payload = { ...form, method_codes: [...new Set(['cash', ...form.method_codes])] }
  156. const res = editingID.value
  157. ? await updatePaymentEntry(editingID.value, payload)
  158. : await createPaymentEntry(payload)
  159. if (res.code === 0) {
  160. ElMessage.success(editingID.value ? '支付入口已更新' : '支付入口已新增')
  161. dialogVisible.value = false
  162. await loadData()
  163. }
  164. } finally {
  165. saving.value = false
  166. }
  167. }
  168. async function changeStatus(row) {
  169. const target = row.enabled
  170. try {
  171. const res = await setPaymentEntryStatus(row.ID, target)
  172. if (res.code === 0) ElMessage.success(target ? '支付入口已启用' : '支付入口已停用')
  173. else row.enabled = !target
  174. } catch (e) {
  175. row.enabled = !target
  176. }
  177. }
  178. async function removeEntry(row) {
  179. await ElMessageBox.confirm(`确定删除“${row.name}”吗?历史支付记录不会被删除。`, '删除支付入口', {
  180. type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消'
  181. })
  182. const res = await deletePaymentEntry(row.ID)
  183. if (res.code === 0) {
  184. ElMessage.success('支付入口已删除')
  185. await loadData()
  186. }
  187. }
  188. onMounted(loadData)
  189. </script>
  190. <style scoped>
  191. .page-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }
  192. .page-toolbar h2 { margin: 0; font-size: 18px; font-weight: 600; color: #303133; }
  193. .page-toolbar p { margin: 5px 0 0; font-size: 13px; color: #909399; }
  194. .entry-name { display: flex; align-items: center; gap: 6px; font-weight: 600; color: #303133; }
  195. .entry-code { display: block; margin-top: 3px; color: #909399; font-family: Consolas, monospace; font-size: 12px; }
  196. .locked { color: #e6a23c; }
  197. .method-tag { margin-right: 6px; }
  198. .muted { color: #909399; font-size: 12px; }
  199. .form-hint { margin-top: 4px; color: #909399; font-size: 12px; line-height: 1.5; }
  200. </style>