fix_crm_add_wx_page_utf8.mjs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. import { writeFileSync } from 'node:fs';
  2. import { fileURLToPath } from 'node:url';
  3. import path from 'node:path';
  4. const vuePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../src/views/crm/addWx/index.vue');
  5. const sqlPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../java/sql/add_crm_add_wx_menu_tenant.sql');
  6. const migrationPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../java/fs-service/src/main/resources/db/tenant-add-wx-client-fail-time-migration.sql');
  7. const vue = `<template>
  8. <div class="app-container">
  9. <el-row class="baseInfo list-stats-row mb8">
  10. <el-col>
  11. <el-card shadow="hover" class="stat-card">
  12. <div slot="header" class="card-header">
  13. <span>${'\u52A0\u5FAE\u4EFB\u52A1\u603B\u6570'}</span>
  14. </div>
  15. <div class="content">
  16. <span class="card-number total">
  17. <count-to :start-val="0" :end-val="count.totalNum || 0" :duration="2000" />
  18. </span>
  19. </div>
  20. </el-card>
  21. </el-col>
  22. <el-col>
  23. <el-card shadow="hover" class="stat-card">
  24. <div slot="header" class="card-header">
  25. <span>${'\u52A0\u5FAE\u4E2D'}</span>
  26. </div>
  27. <div class="content">
  28. <span class="card-number adding">
  29. <count-to :start-val="0" :end-val="count.addingNum || 0" :duration="2000" />
  30. </span>
  31. </div>
  32. </el-card>
  33. </el-col>
  34. <el-col>
  35. <el-card shadow="hover" class="stat-card">
  36. <div slot="header" class="card-header">
  37. <span>${'\u4ECA\u65E5\u52A0\u5FAE\u6210\u529F'}</span>
  38. </div>
  39. <div class="content">
  40. <span class="card-number success">
  41. <count-to :start-val="0" :end-val="count.toDayAddNum || 0" :duration="2000" />
  42. </span>
  43. </div>
  44. </el-card>
  45. </el-col>
  46. <el-col>
  47. <el-card shadow="hover" class="stat-card">
  48. <div slot="header" class="card-header">
  49. <span>${'\u4ECA\u65E5\u52A0\u5FAE\u5931\u8D25'}</span>
  50. </div>
  51. <div class="content">
  52. <span class="card-number fail">
  53. <count-to :start-val="0" :end-val="count.toDayFailNum || 0" :duration="2000" />
  54. </span>
  55. </div>
  56. </el-card>
  57. </el-col>
  58. </el-row>
  59. <el-form
  60. v-show="showSearch"
  61. ref="queryForm"
  62. :model="queryParams"
  63. :inline="true"
  64. label-width="88px"
  65. class="list-search-form"
  66. >
  67. <el-form-item label="${'\u5BA2\u6237\u540D\u79F0'}" prop="customerName">
  68. <el-input
  69. v-model="queryParams.customerName"
  70. placeholder="${'\u8BF7\u8F93\u5165\u5BA2\u6237\u540D\u79F0'}"
  71. clearable
  72. size="small"
  73. @keyup.enter.native="handleQuery"
  74. />
  75. </el-form-item>
  76. <el-form-item label="${'\u624B\u673A\u53F7'}" prop="phone">
  77. <el-input
  78. v-model="queryParams.phone"
  79. placeholder="${'\u8BF7\u8F93\u5165\u624B\u673A\u53F7'}"
  80. clearable
  81. size="small"
  82. @keyup.enter.native="handleQuery"
  83. />
  84. </el-form-item>
  85. <el-form-item label="${'\u52A0\u5FAE\u9500\u552E'}" prop="accountId">
  86. <el-select v-model="queryParams.accountId" filterable clearable placeholder="${'\u5168\u90E8'}" size="small">
  87. <el-option
  88. v-for="item in accountList"
  89. :key="item.id"
  90. :label="formatAccountLabel(item)"
  91. :value="item.id"
  92. />
  93. </el-select>
  94. </el-form-item>
  95. <el-form-item label="${'\u5916\u547C\u4EFB\u52A1'}" prop="roboticId">
  96. <el-select v-model="queryParams.roboticId" filterable clearable placeholder="${'\u5168\u90E8'}" size="small">
  97. <el-option v-for="item in roboticList" :key="item.id" :label="item.name" :value="item.id" />
  98. </el-select>
  99. </el-form-item>
  100. <el-form-item label="${'\u52A0\u5FAE\u7C7B\u578B'}" prop="isWeCom">
  101. <el-select v-model="queryParams.isWeCom" clearable placeholder="${'\u5168\u90E8'}" size="small">
  102. <el-option label="${'\u4E2A\u5FAE'}" :value="1" />
  103. <el-option label="${'\u4F01\u5FAE'}" :value="2" />
  104. </el-select>
  105. </el-form-item>
  106. <el-form-item label="${'\u52A0\u5FAE\u72B6\u6001'}" prop="isAdd">
  107. <el-select v-model="queryParams.isAdd" clearable placeholder="${'\u5168\u90E8'}" size="small">
  108. <el-option label="${'\u672A\u6DFB\u52A0'}" :value="0" />
  109. <el-option label="${'\u52A0\u5FAE\u4E2D'}" :value="2" />
  110. <el-option label="${'\u52A0\u5FAE\u6210\u529F'}" :value="1" />
  111. <el-option label="${'\u52A0\u5FAE\u5931\u8D25'}" :value="3" />
  112. </el-select>
  113. </el-form-item>
  114. <el-form-item label="${'\u53D1\u8D77\u65F6\u95F4'}" prop="time">
  115. <el-date-picker
  116. v-model="queryParams.time"
  117. type="daterange"
  118. range-separator="${'\u81F3'}"
  119. value-format="yyyy-MM-dd"
  120. start-placeholder="${'\u5F00\u59CB\u65E5\u671F'}"
  121. end-placeholder="${'\u7ED3\u675F\u65E5\u671F'}"
  122. size="small"
  123. />
  124. </el-form-item>
  125. <el-form-item class="list-search-actions">
  126. <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">${'\u641C\u7D22'}</el-button>
  127. <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">${'\u91CD\u7F6E'}</el-button>
  128. <el-button
  129. v-hasPermi="['company:companyClient:export']"
  130. type="warning"
  131. icon="el-icon-download"
  132. size="mini"
  133. @click="handleExport"
  134. >${'\u5BFC\u51FA'}</el-button>
  135. <right-toolbar :show-search.sync="showSearch" @queryTable="getList" />
  136. </el-form-item>
  137. </el-form>
  138. <el-table v-loading="loading" :data="list" border>
  139. <el-table-column label="${'\u5BA2\u6237\u540D\u79F0'}" align="center" min-width="120" show-overflow-tooltip>
  140. <template slot-scope="scope">
  141. <el-link
  142. v-if="scope.row.customerId"
  143. type="primary"
  144. :underline="false"
  145. @click="openCustomerDetail(scope.row)"
  146. >{{ scope.row.customerName || scope.row.nickName || '-' }}</el-link>
  147. <span v-else>{{ scope.row.customerName || scope.row.nickName || '-' }}</span>
  148. </template>
  149. </el-table-column>
  150. <el-table-column label="${'\u624B\u673A\u53F7'}" align="center" prop="phone" width="130" />
  151. <el-table-column label="${'\u5916\u547C\u4EFB\u52A1'}" align="center" prop="roboticName" min-width="140" show-overflow-tooltip />
  152. <el-table-column label="${'\u52A0\u5FAE\u9500\u552E'}" align="center" min-width="120" show-overflow-tooltip>
  153. <template slot-scope="scope">
  154. {{ scope.row.companyUserName || '-' }}
  155. </template>
  156. </el-table-column>
  157. <el-table-column label="${'\u52A0\u5FAE\u5FAE\u4FE1\u53F7'}" align="center" prop="wxNickName" min-width="120" show-overflow-tooltip />
  158. <el-table-column label="${'\u52A0\u5FAE\u7C7B\u578B'}" align="center" width="90">
  159. <template slot-scope="scope">
  160. <el-tag v-if="scope.row.isWeCom == 1" type="success" size="mini">${'\u4E2A\u5FAE'}</el-tag>
  161. <el-tag v-else-if="scope.row.isWeCom == 2" type="primary" size="mini">${'\u4F01\u5FAE'}</el-tag>
  162. <span v-else>-</span>
  163. </template>
  164. </el-table-column>
  165. <el-table-column label="${'\u52A0\u5FAE\u72B6\u6001'}" align="center" width="100">
  166. <template slot-scope="scope">
  167. <el-tag :type="getIsAddType(scope.row.isAdd)" size="mini">{{ getIsAddLabel(scope.row.isAdd) }}</el-tag>
  168. </template>
  169. </el-table-column>
  170. <el-table-column label="${'\u5BA2\u6237\u610F\u5411'}" align="center" width="100">
  171. <template slot-scope="scope">
  172. <span v-for="item in levelList" :key="item.dictValue">
  173. <el-tag v-if="scope.row.intention == item.dictValue" size="mini">{{ item.dictLabel }}</el-tag>
  174. </span>
  175. </template>
  176. </el-table-column>
  177. <el-table-column label="${'\u8BDD\u672F'}" align="center" prop="dialogName" min-width="120" show-overflow-tooltip />
  178. <el-table-column label="${'\u53D1\u8D77\u65F6\u95F4'}" align="center" width="160">
  179. <template slot-scope="scope">
  180. <span>{{ parseTime(scope.row.addTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
  181. </template>
  182. </el-table-column>
  183. <el-table-column label="${'\u6210\u529F\u65F6\u95F4'}" align="center" width="160">
  184. <template slot-scope="scope">
  185. <span>{{ parseTime(scope.row.successAddTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
  186. </template>
  187. </el-table-column>
  188. <el-table-column label="${'\u5931\u8D25\u65F6\u95F4'}" align="center" width="160">
  189. <template slot-scope="scope">
  190. <span>{{ parseTime(scope.row.failAddTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
  191. </template>
  192. </el-table-column>
  193. </el-table>
  194. <pagination
  195. v-show="total > 0"
  196. :total="total"
  197. :page.sync="queryParams.pageNum"
  198. :limit.sync="queryParams.pageSize"
  199. @pagination="getList"
  200. />
  201. <el-drawer size="75%" :title="customerDrawer.title" :visible.sync="customerDrawer.open">
  202. <customer-details ref="customerDetails" />
  203. </el-drawer>
  204. </div>
  205. </template>
  206. <script>
  207. import { listCompanyClient, addWxStatistics, exportCompanyClient } from '@/api/company/companyClient'
  208. import { listAll as roboticListAll } from '@/api/company/companyVoiceRobotic'
  209. import { listAll as accountListAll } from '@/api/company/companyAccount'
  210. import { getDicts } from '@/api/system/dict/data'
  211. import CountTo from 'vue-count-to'
  212. import customerDetails from '../components/customerDetails.vue'
  213. export default {
  214. name: 'CrmAddWxProgress',
  215. components: { CountTo, customerDetails },
  216. data() {
  217. return {
  218. loading: false,
  219. showSearch: true,
  220. total: 0,
  221. list: [],
  222. roboticList: [],
  223. accountList: [],
  224. levelList: [],
  225. count: {},
  226. customerDrawer: {
  227. title: '${'\u5BA2\u6237\u8BE6\u60C5'}',
  228. open: false
  229. },
  230. queryParams: {
  231. pageNum: 1,
  232. pageSize: 10,
  233. customerName: null,
  234. phone: null,
  235. accountId: null,
  236. roboticId: null,
  237. isWeCom: null,
  238. isAdd: null,
  239. time: []
  240. }
  241. }
  242. },
  243. created() {
  244. this.initOptions()
  245. this.handleQuery()
  246. },
  247. methods: {
  248. initOptions() {
  249. roboticListAll().then(res => {
  250. this.roboticList = res.data || []
  251. })
  252. accountListAll().then(res => {
  253. this.accountList = res.data || []
  254. })
  255. getDicts('customer_intention_level').then(res => {
  256. this.levelList = res.data || []
  257. })
  258. },
  259. formatAccountLabel(item) {
  260. const userName = item.companyUserName || item.nickName || ''
  261. const wxName = item.wxNickName || ''
  262. if (userName && wxName) {
  263. return userName + '-' + wxName
  264. }
  265. return userName || wxName || '-'
  266. },
  267. getQueryData() {
  268. const params = JSON.parse(JSON.stringify(this.queryParams))
  269. if (params.time && params.time.length === 2) {
  270. params.beginTime = params.time[0]
  271. params.endTime = params.time[1]
  272. }
  273. delete params.time
  274. return params
  275. },
  276. getList() {
  277. this.loading = true
  278. listCompanyClient(this.getQueryData())
  279. .then(response => {
  280. this.list = response.rows || []
  281. this.total = response.total || 0
  282. })
  283. .finally(() => {
  284. this.loading = false
  285. })
  286. },
  287. loadStatistics() {
  288. addWxStatistics(this.getQueryData()).then(res => {
  289. this.count = res || {}
  290. })
  291. },
  292. getIsAddLabel(value) {
  293. if (value === 0) return '${'\u672A\u6DFB\u52A0'}'
  294. if (value === 1) return '${'\u52A0\u5FAE\u6210\u529F'}'
  295. if (value === 2) return '${'\u52A0\u5FAE\u4E2D'}'
  296. if (value === 3) return '${'\u52A0\u5FAE\u5931\u8D25'}'
  297. return '-'
  298. },
  299. getIsAddType(value) {
  300. if (value === 0) return 'info'
  301. if (value === 1) return 'success'
  302. if (value === 2) return 'warning'
  303. if (value === 3) return 'danger'
  304. return 'info'
  305. },
  306. handleQuery() {
  307. this.queryParams.pageNum = 1
  308. this.loadStatistics()
  309. this.getList()
  310. },
  311. resetQuery() {
  312. this.resetForm('queryForm')
  313. this.handleQuery()
  314. },
  315. openCustomerDetail(row) {
  316. if (!row || !row.customerId) {
  317. return
  318. }
  319. this.customerDrawer.open = true
  320. const that = this
  321. setTimeout(() => {
  322. if (that.$refs.customerDetails) {
  323. that.$refs.customerDetails.initDuplicate(row.isDuplicate, row.dcustomerId)
  324. that.$refs.customerDetails.getDetails(row.customerId)
  325. }
  326. }, 200)
  327. },
  328. handleExport() {
  329. this.$confirm('${'\u662F\u5426\u786E\u8BA4\u5BFC\u51FA\u5F53\u524D\u7B5B\u9009\u6761\u4EF6\u4E0B\u7684\u52A0\u5FAE\u8FDB\u5EA6\u6570\u636E\uFF1F'}', '${'\u8B66\u544A'}', {
  330. confirmButtonText: '${'\u786E\u5B9A'}',
  331. cancelButtonText: '${'\u53D6\u6D88'}',
  332. type: 'warning'
  333. }).then(() => {
  334. return exportCompanyClient(this.getQueryData())
  335. }).then(response => {
  336. this.download(response.msg)
  337. }).catch(() => {})
  338. }
  339. }
  340. }
  341. </script>
  342. <style rel="stylesheet/scss" lang="scss" scoped>
  343. .baseInfo {
  344. margin-bottom: 16px;
  345. }
  346. .list-stats-row {
  347. display: flex;
  348. flex-wrap: wrap;
  349. gap: 16px;
  350. .el-col {
  351. flex: 1;
  352. min-width: 200px;
  353. max-width: 320px;
  354. }
  355. }
  356. .stat-card {
  357. .card-header {
  358. font-size: 14px;
  359. color: #606266;
  360. }
  361. .card-number {
  362. display: inline-block;
  363. font-size: 28px;
  364. font-weight: 600;
  365. line-height: 1.2;
  366. }
  367. .total {
  368. color: #409eff;
  369. }
  370. .adding {
  371. color: #e6a23c;
  372. }
  373. .success {
  374. color: #67c23a;
  375. }
  376. .fail {
  377. color: #f56c6c;
  378. }
  379. }
  380. </style>
  381. `;
  382. const migration = `-- ${'\u79DF\u6237\u5E93\uFF1A\u52A0\u5FAE\u5931\u8D25\u65F6\u95F4\u5B57\u6BB5\uFF08MySQL 5.7+\uFF0C\u82E5\u5DF2\u5B58\u5728\u8BF7\u5FFD\u7565\u62A5\u9519\uFF09'}
  383. ALTER TABLE company_wx_client
  384. ADD COLUMN fail_add_time datetime NULL DEFAULT NULL COMMENT '${'\u52A0\u5FAE\u5931\u8D25\u65F6\u95F4'}' AFTER success_add_time;
  385. `;
  386. writeFileSync(vuePath, vue, 'utf8');
  387. writeFileSync(migrationPath, migration, 'utf8');
  388. console.log('UTF-8 files written:', vuePath, migrationPath);