argeement-sign-list.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. <template>
  2. <div class="mb-4 p-4 bg-gray-50 rounded border border-gray-200 flex flex-row flex-wrap">
  3. <div class="w-full lg:w-1/2 flex flex-row flex-wrap">
  4. <div v-for="s in agreementProgressStats" :key="s.key" class="flex-1 flex flex-col p-2 items-center" :style="{width: agreementProgressStatsSpan + '%'}">
  5. <span class="text-2xl bold">{{ s.value }}</span>
  6. <span class="text-xs text-secondary text-align-center max-h-12 overflow-hidden text-ellipsis">{{ s.title }}</span>
  7. </div>
  8. <div class="p-2" :style="{width: agreementProgressStatsSpan + '%'}">
  9. <v-chart :option="agreementPieChartOption" autoresize />
  10. </div>
  11. </div>
  12. <div class="w-full lg:w-1/2 flex flex-row flex-wrap border-l border-gray-200">
  13. <div v-for="s in agreementRejectStats" :key="s.key" class="flex-1 flex flex-col p-2 items-center" :style="{width: agreementRejectStatsSpan + '%'}">
  14. <span class="text-2xl bold">{{ s.value }}</span>
  15. <span class="text-xs text-secondary text-align-center max-h-12 overflow-hidden text-ellipsi">{{ s.title }}</span>
  16. </div>
  17. <div class="p-2" :style="{width: agreementProgressStatsSpan + '%'}">
  18. <v-chart :option="agreementBarChartOption" autoresize />
  19. </div>
  20. </div>
  21. </div>
  22. <CommonListBlock
  23. ref="listRef"
  24. :show-total="true"
  25. :row-count="1"
  26. :row-type="5"
  27. :page-size="10"
  28. :drop-down-names="[
  29. {
  30. options: agreementProgressOptions,
  31. label: '状态',
  32. defaultSelectedValue: lastAgreementProgress,
  33. },
  34. {
  35. options: agreementLevelOptions,
  36. label: '等级',
  37. defaultSelectedValue: lastAgreementLevel,
  38. },
  39. ]"
  40. :load="(page: number, pageSize: number, _tag: number, searchText: string, drop: number[]) => loadAgreementSignAdminList(page, pageSize, searchText, drop)"
  41. :show-detail="handleShowDetail"
  42. >
  43. <template #headLeft>
  44. <div class="flex flex-row items-center gap-2">
  45. <span>{{ progressHint }}</span>
  46. </div>
  47. </template>
  48. <template #itemRight="{ item }">
  49. <div class="flex flex-col md:flex-row md:items-center shrink-0 mt-3 md:mt-2">
  50. <!-- 自查评估表 状态与审核 -->
  51. <div class="relative flex flex-row items-center flex-wrap">
  52. <span class="text-xs text-gray-300 absolute -top-2 left-0">自查评估表状态</span>
  53. <span
  54. class="text-sm text-gray-600 truncate max-w-35"
  55. :title="checkFormProgressLabel(item.checkFormProgress)"
  56. >
  57. {{ checkFormProgressLabel(item.checkFormProgress) }}
  58. </span>
  59. <a-button type="link" @click.stop="handleEditCheckForm(item)">编辑</a-button>
  60. <div v-if="item.checkFormId && canReviewButPrevNotCompleted(item.checkFormProgress)" class="flex flex-row items-center text-sm">
  61. <ExclamationCircleOutlined /> 上一阶段未完成
  62. </div>
  63. <a-button
  64. type="primary"
  65. v-else-if="item.checkFormId && canReview(item.checkFormProgress)"
  66. @click.stop="handleReviewCheckForm(item)"
  67. >
  68. 审核
  69. </a-button>
  70. <a-button
  71. type="primary"
  72. disabled
  73. v-else-if="item.checkFormId && isCurrentLevelReviewed(item.checkFormProgress)"
  74. >
  75. 已审核
  76. </a-button>
  77. </div>
  78. <!-- 分隔线(移动端横向 / 桌面端竖向) -->
  79. <div class="my-3 border-t border-gray-300 md:my-0 md:mx-3 md:border-t-0 md:border-l md:h-6"></div>
  80. <!-- 传承协议 状态与审核 -->
  81. <div class="relative flex flex-row items-center flex-wrap">
  82. <span class="text-xs text-gray-300 absolute -top-2 left-0">传承协议状态</span>
  83. <a-popover v-if="item.rejectType && item.rejectType > 0" title="退回原因" trigger="hover">
  84. <template #content>
  85. <div style="max-width: 300px">{{ item.rejectReason || '无' }}</div>
  86. </template>
  87. <span class="text-sm" style="color: #f5222d; cursor: pointer;">
  88. <ExclamationCircleOutlined /> 已退回
  89. </span>
  90. </a-popover>
  91. <span v-else
  92. class="text-sm text-gray-600 truncate max-w-35"
  93. :title="agreementProgressLabel(item.progress)"
  94. >
  95. {{ agreementProgressLabel(item.progress) }}
  96. </span>
  97. <a-button type="link" @click.stop="handleEdit(item)">编辑</a-button>
  98. <div v-if="item.agreementId && canReviewButPrevNotCompleted(item.progress)" class="flex flex-row items-center text-sm">
  99. <ExclamationCircleOutlined /> 上一阶段未完成
  100. </div>
  101. <a-button
  102. type="primary"
  103. v-else-if="item.agreementId && canReview(item.progress)"
  104. @click.stop="handleReview(item)"
  105. >
  106. 审核
  107. </a-button>
  108. <a-button
  109. type="primary"
  110. disabled
  111. v-else-if="item.agreementId && isCurrentLevelReviewed(item.progress)"
  112. >
  113. 已审核
  114. </a-button>
  115. </div>
  116. </div>
  117. </template>
  118. </CommonListBlock>
  119. </template>
  120. <script setup lang="ts">
  121. import { computed, onMounted, ref, watch } from 'vue';
  122. import { useRoute, useRouter } from 'vue-router';
  123. import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
  124. import { useMemorizeVar } from '@/composeables/useMemorizeVar';
  125. import { useAuthStore } from '@/stores/auth';
  126. import { GROUP_TO_REVIEW_PROGRESS } from './composeables/GroupData';
  127. import { useReview } from './composeables/Review';
  128. import { use } from "echarts/core";
  129. import { CanvasRenderer } from "echarts/renderers";
  130. import { PieChart, BarChart, LineChart } from "echarts/charts";
  131. import {
  132. TitleComponent,
  133. TooltipComponent,
  134. LegendComponent,
  135. GridComponent,
  136. } from "echarts/components";
  137. use([CanvasRenderer, PieChart, BarChart, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent]);
  138. import VChart from "vue-echarts";
  139. import CommonListBlock, { type DropdownCommonItem } from '@/components/content/CommonListBlock.vue';
  140. import AssessmentContentApi, { type UserAgreementListRow } from '@/api/collect/AssessmentContent';
  141. import { LEVEL_NATIONAL, LEVEL_PROVINCE, LEVEL_DISTRICT } from '@/api/collect/AssessmentConsts';
  142. const dataStats = ref<Awaited<ReturnType<typeof AssessmentContentApi.getDataCount>>|null>(null);
  143. const agreementProgressLabels: Record<string, string> = {
  144. '-1': '未提交',
  145. '0': '草稿',
  146. '1': '已提交待审核',
  147. '2': '单位审核完成',
  148. '3': '县级审核完成',
  149. '4': '市级审核完成',
  150. '5': '省级审核完成',
  151. };
  152. const agreementRejectLabels: Record<string, string> = {
  153. '1': '自评阶段退回',
  154. '2': '项目保护单位退回',
  155. '3': '县(区)退回',
  156. '4': '设区市/省非遗中心退回',
  157. '5': '省文旅厅退回',
  158. };
  159. const agreementProgressStats = computed(() => dataStats.value?.agreement || []);
  160. const agreementProgressStatsSpan = computed(() => 100 / (Math.max(agreementProgressStats.value.length, 1) - 1));
  161. const agreementRejectStats = computed(() => dataStats.value?.agreementReject || []);
  162. const agreementRejectStatsSpan = computed(() => 100 / (Math.max(agreementRejectStats.value.length, 1) - 1));
  163. const agreementPieChartOption = computed(() => {
  164. const data = (dataStats.value?.agreement || []).map((s: any) => ({ name: s.title, value: s.value }));
  165. return {
  166. tooltip: { trigger: 'item' },
  167. series: [{
  168. type: 'pie',
  169. radius: ['40%', '70%'],
  170. center: ['50%', '50%'],
  171. data,
  172. label: { show: false },
  173. emphasis: { label: { show: true } },
  174. }],
  175. };
  176. });
  177. const agreementBarChartOption = computed(() => {
  178. const data = (dataStats.value?.agreementReject || []).map((s: any) => s.value);
  179. const titles = (dataStats.value?.agreementReject || []).map((s: any) => s.title);
  180. const max = Math.max(...data, 1);
  181. return {
  182. tooltip: { trigger: 'axis' },
  183. grid: { left: 0, right: 0, top: 8, bottom: 0, containLabel: true },
  184. xAxis: { type: 'category', data: titles, axisLabel: { show: false }, axisTick: { show: false } },
  185. yAxis: { type: 'value', axisLabel: { show: false }, splitLine: { show: false }, max },
  186. series: [
  187. { type: 'bar', data: data.map(v => max - v), barWidth: 12, itemStyle: { color: '#f0f0f0', borderRadius: 2 }, stack: 'total', silent: true, z: 1 },
  188. { type: 'bar', data, barWidth: 12, itemStyle: { color: '#1890ff', borderRadius: 2 }, stack: 'total', z: 2 },
  189. ],
  190. };
  191. });
  192. onMounted(async () => {
  193. dataStats.value = await AssessmentContentApi.getDataCount();
  194. });
  195. const router = useRouter();
  196. const route = useRoute();
  197. const authStore = useAuthStore();
  198. const listRef = ref<any>(null);
  199. const { variable: lastAgreementProgress } = useMemorizeVar('adminUserAgreementProgress', -100);
  200. const { variable: lastAgreementLevel } = useMemorizeVar('adminUserAgreementLevel', 0);
  201. const agreementLevelOptions: DropdownCommonItem[] = [
  202. { id: 0, name: '全部等级' },
  203. { id: LEVEL_NATIONAL, name: '国家级' },
  204. { id: LEVEL_PROVINCE, name: '省级' },
  205. { id: LEVEL_DISTRICT, name: '市级' },
  206. ];
  207. const agreementProgressOptions: DropdownCommonItem[] = [
  208. { id: -100, name: '全部状态' },
  209. { id: 0, name: '未提交' },
  210. { id: 1, name: '已提交待审核' },
  211. { id: 2, name: '项目保护单位审核完成' },
  212. { id: 3, name: '县(区)文旅部门审核完成' },
  213. { id: 4, name: '设区市文旅部门、省非遗中心审核完成' },
  214. { id: 5, name: '省文化和旅游厅审核完成' },
  215. ];
  216. const { canReview, canReviewButPrevNotCompleted, isCurrentLevelReviewed, progressHint } = useReview();
  217. function agreementProgressLabel(progress: number | null | undefined) {
  218. if (progress === null || progress === undefined)
  219. return '未填写';
  220. const hit = agreementProgressOptions.find((o) => o.id === progress);
  221. return hit?.name ?? `进度 ${progress}`;
  222. }
  223. const checkFormProgressLabels: Record<number, string> = {
  224. [-1]: '未提交',
  225. [0]: '草稿',
  226. [1]: '已提交审核',
  227. [2]: '项目保护单位审核完成',
  228. [3]: '县(区)文旅部门审核完成',
  229. [4]: '设区市文旅部门/省非遗中心审核完成',
  230. [5]: '省文化和旅游厅审核完成',
  231. };
  232. function checkFormProgressLabel(progress: number | null | undefined) {
  233. if (progress === null || progress === undefined)
  234. return '未填写';
  235. return checkFormProgressLabels[progress] ?? `进度 ${progress}`;
  236. }
  237. function handleEditCheckForm(item: any) {
  238. router.push({
  239. name: 'CollectEvaluationForm',
  240. query: { id: item.checkFormId ?? 0, userId: item.userId ?? 0 },
  241. });
  242. }
  243. function handleReviewCheckForm(item: any) {
  244. if (!item.checkFormId || !item.userId) return;
  245. router.push({
  246. name: 'CollectEvaluationFormReview',
  247. query: {
  248. id: String(item.checkFormId),
  249. userId: String(item.userId),
  250. ...(item.checkFormProgress != null ? { progress: String(item.checkFormProgress) } : {}),
  251. },
  252. });
  253. }
  254. async function loadAgreementSignAdminList(page: number, pageSize: number, searchText: string, dropDownValues: number[]) {
  255. const pv = dropDownValues?.[0];
  256. const lv = dropDownValues?.[1];
  257. lastAgreementProgress.value = pv ?? -100;
  258. lastAgreementLevel.value = lv ?? 0;
  259. const progress = pv != null && pv > -50 ? pv : undefined;
  260. const level = lv != null && lv > 0 ? lv : undefined;
  261. const list = await AssessmentContentApi.getUserAgreementList({
  262. year: new Date().getFullYear(),
  263. page,
  264. pageSize,
  265. keywords: searchText?.trim() || undefined,
  266. progress,
  267. level,
  268. });
  269. return {
  270. page,
  271. total: list.total,
  272. data: list.data.map((row) => ({
  273. ...row,
  274. id: row.id,
  275. agreementId: row.agreementId,
  276. userId: row.userId,
  277. title: row.title,
  278. desc: [row.mobile, row.basicUnit, row.ichTitle].filter(Boolean).join(' · ') || '—',
  279. })),
  280. };
  281. }
  282. function handleShowDetail(item: any) {
  283. router.push({
  284. name: 'CollectAgreementSign',
  285. query: { id: item.agreementId && item.agreementId > 0 ? item.agreementId : 0, userId: item.userId ?? 0 },
  286. });
  287. }
  288. function handleEdit(item: any) {
  289. router.push({
  290. name: 'CollectAgreementSign',
  291. query: { id: item.agreementId && item.agreementId > 0 ? item.agreementId : 0, userId: item.userId ?? 0 },
  292. });
  293. }
  294. function handleReview(item: any) {
  295. if (!item.agreementId || !item.userId) return;
  296. router.push({
  297. name: 'CollectAgreementSignReview',
  298. query: {
  299. id: String(item.agreementId),
  300. userId: String(item.userId),
  301. ...(item.progress != null ? { progress: String(item.progress) } : {}),
  302. },
  303. });
  304. }
  305. watch(route, () => {
  306. listRef.value?.reload();
  307. })
  308. defineExpose({
  309. reload: () => listRef.value?.reload(),
  310. });
  311. </script>