選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 
 

193 行
7.7 KiB

  1. {% extends "../layout.html" %}
  2. {% block title %}短信记录{% endblock %}
  3. {% block content %}
  4. <div id="smsApp" v-cloak>
  5. <el-card shadow="never">
  6. <template #header>
  7. <div class="flex items-center justify-between">
  8. <span class="font-medium">短信发送记录</span>
  9. </div>
  10. </template>
  11. <!-- 筛选栏 -->
  12. <div class="flex items-center gap-4 mb-4 flex-wrap">
  13. <el-input v-model="query.keyword" placeholder="搜索手机号..." style="width:180px;" clearable @keyup.enter="loadList"></el-input>
  14. <el-select v-model="query.bizType" placeholder="业务类型" style="width:140px;" clearable>
  15. <el-option label="实名认证" value="real_name_auth"></el-option>
  16. <el-option label="登录" value="login"></el-option>
  17. <el-option label="修改手机号" value="change_phone"></el-option>
  18. <el-option label="审核通过" value="audit_approved"></el-option>
  19. <el-option label="审核驳回" value="audit_rejected"></el-option>
  20. </el-select>
  21. <el-select v-model="query.status" placeholder="发送状态" style="width:120px;" clearable>
  22. <el-option label="待发送" :value="0"></el-option>
  23. <el-option label="已发送" :value="1"></el-option>
  24. <el-option label="发送失败" :value="2"></el-option>
  25. </el-select>
  26. <el-date-picker v-model="dateRange" type="daterange" range-separator="至"
  27. start-placeholder="开始日期" end-placeholder="结束日期"
  28. value-format="YYYY-MM-DD" style="width:240px;" :teleported="false"></el-date-picker>
  29. <el-button type="primary" @click="loadList">搜索</el-button>
  30. <el-button @click="resetQuery">重置</el-button>
  31. <div style="flex:1"></div>
  32. <el-button type="warning" plain @click="showClearDialog = true">清除短信限制</el-button>
  33. </div>
  34. <!-- 信息栏 -->
  35. <div class="flex items-center gap-3 mb-3 text-sm text-gray-500">
  36. <span>共 <b class="text-orange-500">${ total }</b> 条记录</span>
  37. </div>
  38. <!-- 表格 -->
  39. <el-table :data="list" v-loading="loading" border>
  40. <el-table-column prop="id" label="ID" width="70"></el-table-column>
  41. <el-table-column prop="mobile" label="手机号" width="130"></el-table-column>
  42. <el-table-column prop="code" label="验证码" width="90"></el-table-column>
  43. <el-table-column prop="biz_type" label="业务类型" width="120">
  44. <template #default="{ row }">
  45. <el-tag size="small" :type="bizTypeMap[row.biz_type]?.type || 'info'">${ bizTypeMap[row.biz_type]?.text || row.biz_type }</el-tag>
  46. </template>
  47. </el-table-column>
  48. <el-table-column prop="status" label="状态" width="100">
  49. <template #default="{ row }">
  50. <el-tag size="small" :type="statusMap[row.status]?.type || 'info'">${ statusMap[row.status]?.text || '未知' }</el-tag>
  51. </template>
  52. </el-table-column>
  53. <el-table-column prop="fail_reason" label="失败原因" min-width="180">
  54. <template #default="{ row }">
  55. <span class="text-gray-500">${ row.fail_reason || '-' }</span>
  56. </template>
  57. </el-table-column>
  58. <el-table-column prop="ip" label="IP地址" width="130"></el-table-column>
  59. <el-table-column prop="create_time" label="发送时间" width="170">
  60. <template #default="{ row }">${ row.create_time?.slice(0,19) }</template>
  61. </el-table-column>
  62. </el-table>
  63. <!-- 分页 -->
  64. <div class="flex justify-end mt-4">
  65. <el-pagination background layout="total, sizes, prev, pager, next" :total="total" v-model:page-size="pageSize" :page-sizes="[20, 50, 100]" v-model:current-page="page" @current-change="loadList" @size-change="onSizeChange"></el-pagination>
  66. </div>
  67. </el-card>
  68. <!-- 清除短信限制弹窗 -->
  69. <el-dialog v-model="showClearDialog" title="清除短信限制" width="420px" :close-on-click-modal="false">
  70. <el-form label-width="80px">
  71. <el-form-item label="类型">
  72. <el-radio-group v-model="clearForm.type">
  73. <el-radio value="phone">手机号</el-radio>
  74. <el-radio value="ip">IP地址</el-radio>
  75. </el-radio-group>
  76. </el-form-item>
  77. <el-form-item :label="clearForm.type === 'phone' ? '手机号' : 'IP地址'">
  78. <el-input v-model="clearForm.value" :placeholder="clearForm.type === 'phone' ? '请输入手机号' : '请输入IP地址'" clearable></el-input>
  79. </el-form-item>
  80. </el-form>
  81. <template #footer>
  82. <el-button @click="showClearDialog = false">取消</el-button>
  83. <el-button type="primary" :loading="clearLoading" @click="handleClearLimit">确定</el-button>
  84. </template>
  85. </el-dialog>
  86. </div>
  87. {% endblock %}
  88. {% block js %}
  89. <script>
  90. const { createApp, ref, reactive, onMounted } = Vue;
  91. const app = createApp({
  92. delimiters: ['${', '}'],
  93. setup() {
  94. const loading = ref(false);
  95. const list = ref([]);
  96. const total = ref(0);
  97. const page = ref(1);
  98. const pageSize = ref(20);
  99. const query = reactive({ keyword: '', bizType: '', status: '' });
  100. const dateRange = ref(null);
  101. const bizTypeMap = {
  102. real_name_auth: { type: 'primary', text: '实名认证' },
  103. login: { type: '', text: '登录' },
  104. change_phone: { type: 'warning', text: '修改手机号' },
  105. audit_approved: { type: 'success', text: '审核通过' },
  106. audit_rejected: { type: 'danger', text: '审核驳回' }
  107. };
  108. const statusMap = {
  109. 0: { type: 'info', text: '待发送' },
  110. 1: { type: 'success', text: '已发送' },
  111. 2: { type: 'danger', text: '发送失败' }
  112. };
  113. async function loadList() {
  114. loading.value = true;
  115. try {
  116. const params = new URLSearchParams({
  117. page: page.value, pageSize: pageSize.value,
  118. keyword: query.keyword, bizType: query.bizType,
  119. status: query.status !== '' && query.status !== null ? query.status : '',
  120. startDate: dateRange.value ? dateRange.value[0] : '',
  121. endDate: dateRange.value ? dateRange.value[1] : ''
  122. });
  123. const res = await fetch('/admin/system/sms/list?' + params).then(r => r.json());
  124. if (res.code === 0) {
  125. list.value = res.data.data || [];
  126. total.value = res.data.count || 0;
  127. }
  128. } finally { loading.value = false; }
  129. }
  130. function resetQuery() {
  131. query.keyword = '';
  132. query.bizType = '';
  133. query.status = '';
  134. dateRange.value = null;
  135. page.value = 1;
  136. loadList();
  137. }
  138. function onSizeChange() {
  139. page.value = 1;
  140. loadList();
  141. }
  142. onMounted(() => loadList());
  143. // 清除短信限制
  144. const showClearDialog = ref(false);
  145. const clearLoading = ref(false);
  146. const clearForm = reactive({ type: 'phone', value: '' });
  147. async function handleClearLimit() {
  148. if (!clearForm.value.trim()) {
  149. return ElementPlus.ElMessage.warning('请输入' + (clearForm.type === 'phone' ? '手机号' : 'IP地址'));
  150. }
  151. clearLoading.value = true;
  152. try {
  153. const res = await fetch('/admin/system/sms/clearLimit', {
  154. method: 'POST',
  155. headers: { 'Content-Type': 'application/json' },
  156. body: JSON.stringify({ type: clearForm.type, value: clearForm.value.trim() })
  157. }).then(r => r.json());
  158. if (res.code === 0) {
  159. ElementPlus.ElMessage.success('已清除');
  160. showClearDialog.value = false;
  161. clearForm.value = '';
  162. } else {
  163. ElementPlus.ElMessage.error(res.msg || '操作失败');
  164. }
  165. } finally { clearLoading.value = false; }
  166. }
  167. return { loading, list, total, page, pageSize, query, dateRange, bizTypeMap, statusMap, loadList, resetQuery, onSizeChange, showClearDialog, clearLoading, clearForm, handleClearLimit };
  168. }
  169. });
  170. app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
  171. app.mount('#smsApp');
  172. </script>
  173. {% endblock %}