ResourceDetail.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. <template>
  2. <div class="page-container" v-loading="loading">
  3. <div class="page-head">
  4. <div>
  5. <div class="crumb">
  6. <a class="link-back" @click="goBack">资源台账</a> <b>/</b> <b>资源详情</b>
  7. </div>
  8. <h1>{{ detail.name || '资源详情' }}</h1>
  9. </div>
  10. <div class="head-tools">
  11. <el-button type="primary" @click="openRenew">续费</el-button>
  12. <el-button @click="goBack">返回</el-button>
  13. </div>
  14. </div>
  15. <div class="page-card" v-if="detail.id">
  16. <div class="card-head"><h2>基础信息</h2></div>
  17. <el-descriptions :column="3" border>
  18. <el-descriptions-item label="资源名称">{{ detail.name }}</el-descriptions-item>
  19. <el-descriptions-item label="类型">{{ detail.typeName || '-' }}</el-descriptions-item>
  20. <el-descriptions-item label="服务商">{{ detail.providerName || '-' }}</el-descriptions-item>
  21. <el-descriptions-item label="账号信息">{{ detail.accountInfo || '-' }}</el-descriptions-item>
  22. <el-descriptions-item label="负责人">{{ detail.ownerName || '-' }}</el-descriptions-item>
  23. <el-descriptions-item label="状态">
  24. <span :class="['status-badge', statusClass(detail.status)]">{{ detail.statusText }}</span>
  25. </el-descriptions-item>
  26. <el-descriptions-item label="开通日期">{{ detail.startDate || '-' }}</el-descriptions-item>
  27. <el-descriptions-item label="到期日期">{{ detail.expireDate || '-' }}</el-descriptions-item>
  28. <el-descriptions-item label="费用">¥{{ fmtMoney(detail.cost) }}</el-descriptions-item>
  29. <el-descriptions-item label="计费周期">{{ costCycleText(detail.costCycle) }}</el-descriptions-item>
  30. <el-descriptions-item label="购买时长">{{ detail.purchaseDurationText || '-' }}</el-descriptions-item>
  31. <el-descriptions-item label="关联项目">
  32. <el-tag v-for="p in detail.projects" :key="p.id" size="small" style="margin-right: 4px">
  33. {{ p.name }} ({{ p.allocationPercent != null ? p.allocationPercent : 100 }}%)
  34. </el-tag>
  35. <span v-if="!detail.projects?.length" class="muted-tip">-</span>
  36. </el-descriptions-item>
  37. <el-descriptions-item label="备注" :span="3">{{ detail.remark || '-' }}</el-descriptions-item>
  38. </el-descriptions>
  39. </div>
  40. <div class="page-card">
  41. <div class="card-head">
  42. <h2>续费历史</h2>
  43. <span class="hint">共 {{ renewals.length }} 次</span>
  44. </div>
  45. <el-table :data="renewals" v-loading="renewLoading">
  46. <el-table-column label="序号" width="60" header-align="right" align="right">
  47. <template #default="{ $index }">{{ $index + 1 }}</template>
  48. </el-table-column>
  49. <el-table-column prop="renewDate" label="续费日期" width="130" />
  50. <el-table-column label="到期日变更" min-width="200">
  51. <template #default="{ row }">
  52. <span class="mono-num">{{ row.oldExpireDate || '-' }}</span>
  53. <span class="arrow"> → </span>
  54. <span class="mono-num renew-target">{{ row.newExpireDate }}</span>
  55. </template>
  56. </el-table-column>
  57. <el-table-column label="续费时长" width="110">
  58. <template #default="{ row }">{{ durationText(row.durationCode) }}</template>
  59. </el-table-column>
  60. <el-table-column label="续费金额" width="120" header-align="right" align="right">
  61. <template #default="{ row }"><span class="mono-num">¥{{ fmtMoney(row.cost) }}</span></template>
  62. </el-table-column>
  63. <el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
  64. <el-table-column label="操作人" width="100">
  65. <template #default="{ row }">{{ operatorName(row.operatorId) }}</template>
  66. </el-table-column>
  67. <el-table-column label="操作" width="80" fixed="right">
  68. <template #default="{ row }">
  69. <el-button type="danger" link size="small" @click="deleteRenewal(row)">删除</el-button>
  70. </template>
  71. </el-table-column>
  72. </el-table>
  73. <el-empty v-if="!renewLoading && renewals.length === 0" description="暂无续费记录" :image-size="80" />
  74. </div>
  75. <el-dialog v-model="renewVisible" title="资源续费" width="520px" @closed="resetRenewForm">
  76. <el-form :model="renewForm" :rules="renewRules" ref="renewRef" label-width="90px">
  77. <el-form-item label="续费日期">
  78. <el-date-picker v-model="renewForm.renewDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
  79. </el-form-item>
  80. <el-form-item label="新到期日期" prop="newExpireDate">
  81. <el-date-picker v-model="renewForm.newExpireDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
  82. </el-form-item>
  83. <el-form-item label="续费时长">
  84. <el-select v-model="renewForm.durationCode" placeholder="请选择" style="width: 100%" clearable>
  85. <el-option label="1个月" :value="1" />
  86. <el-option label="2个月" :value="2" />
  87. <el-option label="3个月" :value="3" />
  88. <el-option label="6个月" :value="4" />
  89. <el-option label="1年" :value="5" />
  90. <el-option label="2年" :value="6" />
  91. <el-option label="3年" :value="7" />
  92. </el-select>
  93. </el-form-item>
  94. <el-form-item label="续费金额">
  95. <el-input-number v-model="renewForm.cost" :min="0" :precision="2" style="width: 100%" />
  96. </el-form-item>
  97. <el-form-item label="备注">
  98. <el-input v-model="renewForm.remark" type="textarea" :rows="2" />
  99. </el-form-item>
  100. </el-form>
  101. <template #footer>
  102. <el-button @click="renewVisible = false">取消</el-button>
  103. <el-button type="primary" :loading="renewing" @click="submitRenew">确定续费</el-button>
  104. </template>
  105. </el-dialog>
  106. </div>
  107. </template>
  108. <script setup>
  109. import { ref, reactive, onMounted } from 'vue'
  110. import { useRoute, useRouter } from 'vue-router'
  111. import { ElMessage } from 'element-plus'
  112. import { resourceDetailApi, resourceRenewApi, resourceRenewalsApi, resourceDeleteRenewalApi } from '@/api/resource'
  113. import { userEnabledListApi } from '@/api/user'
  114. import { resourceStatusClass } from '@/utils/status'
  115. const route = useRoute()
  116. const router = useRouter()
  117. const resourceId = Number(route.params.id)
  118. const detail = reactive({})
  119. const renewals = ref([])
  120. const loading = ref(false)
  121. const renewLoading = ref(false)
  122. const userOptions = ref([])
  123. const renewVisible = ref(false)
  124. const renewing = ref(false)
  125. const renewRef = ref(null)
  126. const renewForm = reactive({
  127. renewDate: todayStr(),
  128. newExpireDate: '',
  129. durationCode: 5,
  130. cost: 0,
  131. remark: ''
  132. })
  133. const renewRules = {
  134. newExpireDate: [{ required: true, message: '请选择新到期日期', trigger: 'change' }]
  135. }
  136. function todayStr() {
  137. const d = new Date()
  138. const p = (n) => String(n).padStart(2, '0')
  139. return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
  140. }
  141. function fmtMoney(v) {
  142. return Number(v || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
  143. }
  144. function costCycleText(c) {
  145. return c === 1 ? '月付' : c === 2 ? '年付' : c === 3 ? '一次性' : '-'
  146. }
  147. function durationText(code) {
  148. const map = { 1: '1个月', 2: '2个月', 3: '3个月', 4: '6个月', 5: '1年', 6: '2年', 7: '3年' }
  149. return code != null ? (map[code] || '-') : '-'
  150. }
  151. function statusClass(s) {
  152. return resourceStatusClass(s)
  153. }
  154. function operatorName(id) {
  155. if (id == null) return '-'
  156. return userOptions.value.find((u) => u.id === id)?.realName || `用户${id}`
  157. }
  158. function goBack() {
  159. router.push('/resource')
  160. }
  161. async function loadDetail() {
  162. loading.value = true
  163. try {
  164. const res = await resourceDetailApi(resourceId)
  165. Object.assign(detail, res.data)
  166. } finally {
  167. loading.value = false
  168. }
  169. }
  170. async function loadRenewals() {
  171. renewLoading.value = true
  172. try {
  173. const res = await resourceRenewalsApi(resourceId)
  174. renewals.value = res.data || []
  175. } finally {
  176. renewLoading.value = false
  177. }
  178. }
  179. function openRenew() {
  180. renewForm.renewDate = todayStr()
  181. // 默认顺延一年作为便利初值, 用户可改
  182. renewForm.newExpireDate = detail.expireDate ? addYears(detail.expireDate, 1) : addYears(todayStr(), 1)
  183. renewForm.durationCode = 5
  184. renewForm.cost = detail.cost || 0
  185. renewForm.remark = ''
  186. renewVisible.value = true
  187. }
  188. function addYears(dateStr, years) {
  189. const d = new Date(dateStr)
  190. d.setFullYear(d.getFullYear() + years)
  191. const p = (n) => String(n).padStart(2, '0')
  192. return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
  193. }
  194. function resetRenewForm() {
  195. renewRef.value?.clearValidate()
  196. }
  197. async function submitRenew() {
  198. const valid = await renewRef.value.validate().catch(() => false)
  199. if (!valid) return
  200. renewing.value = true
  201. try {
  202. await resourceRenewApi(resourceId, { ...renewForm })
  203. ElMessage.success('续费成功')
  204. renewVisible.value = false
  205. await Promise.all([loadDetail(), loadRenewals()])
  206. } finally {
  207. renewing.value = false
  208. }
  209. }
  210. async function deleteRenewal(row) {
  211. try {
  212. await ElMessageBox.confirm(
  213. `确定删除这条续费记录吗?\n续费至 ${row.newExpireDate} ,金额 ¥${fmtMoney(row.cost)}。\n删除后资源的到期日将按剩余续费记录回滚。`,
  214. '删除续费记录',
  215. { type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' }
  216. )
  217. } catch {
  218. return
  219. }
  220. try {
  221. await resourceDeleteRenewalApi(resourceId, row.id)
  222. ElMessage.success('已删除续费记录')
  223. await Promise.all([loadDetail(), loadRenewals()])
  224. } catch (e) {
  225. ElMessage.error(e?.response?.data?.message || '删除失败')
  226. }
  227. }
  228. onMounted(async () => {
  229. await Promise.all([loadDetail(), loadRenewals()])
  230. const res = await userEnabledListApi().catch(() => ({ data: [] }))
  231. userOptions.value = res.data || []
  232. })
  233. </script>
  234. <style scoped>
  235. .link-back {
  236. color: var(--color-primary);
  237. cursor: pointer;
  238. }
  239. .link-back:hover {
  240. text-decoration: underline;
  241. }
  242. .muted-tip {
  243. color: var(--color-text-placeholder);
  244. font-size: 12px;
  245. }
  246. .mono-num {
  247. font-variant-numeric: tabular-nums;
  248. }
  249. .arrow {
  250. color: var(--color-text-placeholder);
  251. }
  252. .renew-target {
  253. color: var(--color-primary);
  254. font-weight: 600;
  255. }
  256. </style>