index.vue 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207
  1. <template>
  2. <div class="page-container">
  3. <el-card shadow="never" class="table-card">
  4. <template #header>
  5. <div class="card-header">
  6. <div class="left-panel">
  7. <span class="title">履约者池</span>
  8. <el-tag type="info" effect="plain" style="margin-left: 10px;">共 {{ total }} 人</el-tag>
  9. </div>
  10. <div class="right-panel">
  11. <el-button type="primary" icon="Plus" style="margin-right: 15px" @click="handleCreate" v-hasPermi="['fulfiller:pool:add']">新增履约者</el-button>
  12. <el-input
  13. v-model="searchKey"
  14. placeholder="搜索姓名/手机号/身份证"
  15. class="search-input"
  16. prefix-icon="Search"
  17. clearable
  18. @keyup.enter="handleSearch"
  19. @clear="handleSearch"
  20. />
  21. <el-cascader
  22. v-model="filterCascaderValue"
  23. :options="cityCascaderOptions"
  24. :props="{ checkStrictly: true }"
  25. placeholder="所属城市/区域"
  26. clearable
  27. style="width: 200px; margin-left: 10px;"
  28. @change="handleFilterCascaderChange"
  29. />
  30. <el-select v-model="queryParams.stationId" placeholder="所属站点" style="width: 150px; margin-left: 10px;" clearable @change="getList">
  31. <el-option v-for="station in stationOptions" :key="station.id" :label="station.name" :value="station.id" />
  32. </el-select>
  33. </div>
  34. </div>
  35. <!-- Tab切换 (无图标) -->
  36. <el-tabs v-model="activeTab" class="status-tabs" @tab-click="handleTabClick">
  37. <el-tab-pane label="全部" name="all" />
  38. <el-tab-pane label="休息" name="resting" />
  39. <el-tab-pane label="接单中" name="busy" />
  40. <el-tab-pane label="禁用" name="disabled" />
  41. </el-tabs>
  42. </template>
  43. <el-table v-loading="loading" :data="tableData" style="width: 100%" :header-cell-style="{ background: '#f5f7fa' }">
  44. <el-table-column label="基本信息" width="280">
  45. <template #default="scope">
  46. <div class="user-info">
  47. <el-avatar :size="45" :src="scope.row.avatarUrl">{{ scope.row.name.charAt(0) }}</el-avatar>
  48. <div class="text-col">
  49. <div class="name-row">
  50. <span class="name">{{ scope.row.name }}</span>
  51. <span class="gender-tag">
  52. <el-icon v-if="scope.row.gender === '0'" color="#409eff"><Male /></el-icon>
  53. <el-icon v-else color="#f56c6c"><Female /></el-icon>
  54. </span>
  55. </div>
  56. <div class="tags-row" style="margin: 3px 0">
  57. <!-- work type -->
  58. <el-tag size="small" :type="scope.row.workType === 'full_time' ? 'warning' : 'info'" effect="light" style="margin-right: 5px">
  59. {{ scope.row.workType === 'full_time' ? '全职专送' : '兼职众包' }}
  60. </el-tag>
  61. <!-- 等级展示 -->
  62. <el-tag size="small" :type="getLevelType(scope.row.levelName)" effect="plain" class="level-tag">
  63. {{ getLevelText(scope.row.levelName) }}
  64. </el-tag>
  65. </div>
  66. <div class="sub-text">{{ scope.row.age }}岁 | {{ scope.row.phone }}</div>
  67. </div>
  68. </div>
  69. </template>
  70. </el-table-column>
  71. <el-table-column label="资质信息" width="220">
  72. <template #default="scope">
  73. <div class="auth-row">
  74. <div class="auth-card" :class="{ active: scope.row.authId }">
  75. <el-icon><Postcard /></el-icon> 身份证
  76. </div>
  77. <div class="auth-card" :class="{ active: scope.row.authQual }">
  78. <el-icon><Medal /></el-icon> 资质证
  79. </div>
  80. </div>
  81. <div style="margin-top:5px;">
  82. <el-tag v-if="scope.row.authId" type="success" size="small" effect="plain">已认证</el-tag>
  83. <el-tag v-else type="info" size="small" effect="plain">未认证</el-tag>
  84. </div>
  85. <div class="sub-text" style="margin-top:5px;">ID: {{ scope.row.idCard }}</div>
  86. </template>
  87. </el-table-column>
  88. <el-table-column label="服务区域" width="180">
  89. <template #default="scope">
  90. <div class="text-col">
  91. <span style="font-size: 13px; color: #333;">{{ scope.row.cityName }}</span>
  92. <span style="font-size: 12px; color: #999;">{{ scope.row.stationName }}</span>
  93. </div>
  94. </template>
  95. </el-table-column>
  96. <el-table-column label="技能标签" min-width="180">
  97. <template #default="scope">
  98. <el-tag
  99. v-for="tag in scope.row.tags"
  100. :key="tag.id"
  101. :type="tag.colorType"
  102. size="small"
  103. class="skill-tag"
  104. effect="plain"
  105. >
  106. {{ tag.name }}
  107. </el-tag>
  108. </template>
  109. </el-table-column>
  110. <el-table-column label="订单数据" width="180">
  111. <template #default="scope">
  112. <div class="finance-item">服务单: <span class="num">{{ scope.row.orderCount }}</span></div>
  113. <div class="finance-item">拒/转单: <span class="num error">{{ scope.row.rejectCount }}</span></div>
  114. </template>
  115. </el-table-column>
  116. <el-table-column label="账户资产" width="160">
  117. <template #default="scope">
  118. <div class="finance-item">积分: <span class="num">{{ scope.row.points }}</span></div>
  119. <div class="finance-item">余额: <span class="num">¥{{ (scope.row.balance / 100).toFixed(2) }}</span></div>
  120. </template>
  121. </el-table-column>
  122. <el-table-column prop="status" label="状态" width="100">
  123. <template #default="scope">
  124. <div class="status-cell">
  125. <div class="status-dot" :class="scope.row.status"></div>
  126. <span class="status-text">{{ getStatusText(scope.row.status) }}</span>
  127. </div>
  128. </template>
  129. </el-table-column>
  130. <el-table-column label="操作" width="240" fixed="right">
  131. <template #default="scope">
  132. <div class="op-cell">
  133. <el-button link type="primary" size="small" @click="handleDetail(scope.row)" v-hasPermi="['fulfiller:pool:query']">详情</el-button>
  134. <el-button link type="primary" size="small" @click="handleEdit(scope.row)" v-hasPermi="['fulfiller:pool:edit']">编辑</el-button>
  135. <el-button link type="warning" size="small" @click="handleReward(scope.row)" v-hasPermi="['fulfiller:pool:edit']">奖惩</el-button>
  136. <el-dropdown trigger="click" @command="(cmd) => handleCommand(cmd, scope.row)">
  137. <el-button link type="primary">更多<el-icon class="el-icon--right"><arrow-down /></el-icon></el-button>
  138. <template #dropdown>
  139. <el-dropdown-menu>
  140. <el-dropdown-item command="adjustPoints" v-hasPermi="['fulfiller:pool:edit']">修改积分</el-dropdown-item>
  141. <el-dropdown-item command="adjustBalance" v-hasPermi="['fulfiller:pool:edit']">余额增减</el-dropdown-item>
  142. <el-dropdown-item v-if="scope.row.status !== 'disabled'" command="disable" divided style="color: #f56c6c" v-hasPermi="['fulfiller:pool:edit']">禁用账号</el-dropdown-item>
  143. <el-dropdown-item v-else command="enable" divided style="color: #67c23a" v-hasPermi="['fulfiller:pool:edit']">启用账号</el-dropdown-item>
  144. <el-dropdown-item command="resetPwd" v-hasPermi="['fulfiller:pool:edit']">重置密码</el-dropdown-item>
  145. </el-dropdown-menu>
  146. </template>
  147. </el-dropdown>
  148. </div>
  149. </template>
  150. </el-table-column>
  151. </el-table>
  152. <div class="pagination-container">
  153. <el-pagination
  154. v-model:current-page="queryParams.pageNum"
  155. v-model:page-size="queryParams.pageSize"
  156. :page-sizes="[10, 20, 50, 100]"
  157. layout="total, sizes, prev, pager, next, jumper"
  158. :total="total"
  159. @size-change="handleSizeChange"
  160. @current-change="handleCurrentChange"
  161. />
  162. </div>
  163. </el-card>
  164. <!-- 详情侧边栏 Drawer -->
  165. <el-drawer
  166. v-model="detailVisible"
  167. title="履约者档案详情"
  168. size="750px"
  169. direction="rtl"
  170. custom-class="detail-drawer"
  171. >
  172. <div class="drawer-content" v-if="currentItem">
  173. <!-- 头部概览 -->
  174. <div class="user-header-card">
  175. <el-avatar :size="70" :src="currentItem.avatarUrl" class="header-avatar">{{ currentItem.name?.charAt(0) }}</el-avatar>
  176. <div class="header-info">
  177. <div class="top-row">
  178. <span class="user-name">{{ currentItem.name }}</span>
  179. <el-tag size="small" :type="currentItem.gender === '0' ? '' : 'danger'" effect="plain" round style="margin-left: 8px;">
  180. {{ currentItem.gender === '0' ? '男' : '女' }} {{ currentItem.age }}岁
  181. </el-tag>
  182. <span class="status-badge" :class="currentItem.status">{{ getStatusText(currentItem.status) }}</span>
  183. </div>
  184. <div class="sub-row">
  185. <span class="info-item"><el-icon><Iphone /></el-icon> {{ currentItem.phone }}</span>
  186. <span class="divider">|</span>
  187. <span class="info-item"><el-icon><Location /></el-icon> {{ currentItem.cityName }}</span>
  188. </div>
  189. <div class="tags-row">
  190. <el-tag size="small" :type="getLevelType(currentItem.levelName)" effect="dark">{{ getLevelText(currentItem.levelName) }}</el-tag>
  191. <el-tag size="small" type="warning" effect="plain" v-if="currentItem.workType === 'full_time'" style="margin-left:5px">全职专送</el-tag>
  192. </div>
  193. </div>
  194. </div>
  195. <!-- 核心数据指标 -->
  196. <div class="data-metrics-row">
  197. <div class="metric-item">
  198. <div class="val text-primary">{{ currentItem.points }}</div>
  199. <div class="lbl">当前积分</div>
  200. </div>
  201. <div class="divider-v"></div>
  202. <div class="metric-item">
  203. <div class="val text-danger">¥{{ (currentItem.balance / 100).toFixed(2) }}</div>
  204. <div class="lbl">账户余额</div>
  205. </div>
  206. <div class="divider-v"></div>
  207. <div class="metric-item">
  208. <div class="val">{{ currentItem.orderCount }}</div>
  209. <div class="lbl">服务单量</div>
  210. </div>
  211. <div class="divider-v"></div>
  212. <div class="metric-item">
  213. <div class="val text-warning">{{ currentItem.rejectCount || 0 }}</div>
  214. <div class="lbl">拒绝单量</div>
  215. </div>
  216. </div>
  217. <el-tabs v-model="activeDetailTab" class="detail-tabs">
  218. <el-tab-pane label="档案概览" name="info">
  219. <div class="tab-content-wrapper">
  220. <div class="section-block">
  221. <div class="section-title">基础信息</div>
  222. <el-descriptions :column="2" border>
  223. <el-descriptions-item label="身份证号">{{ currentItem.idCard }}</el-descriptions-item>
  224. <el-descriptions-item label="真实姓名">{{ currentItem.realName || currentItem.name }}</el-descriptions-item>
  225. <el-descriptions-item label="归属站点">{{ currentItem.stationName }}</el-descriptions-item>
  226. <el-descriptions-item label="证件有效期">{{ currentItem.idCardExpiry || '-' }}</el-descriptions-item>
  227. <el-descriptions-item label="入驻时间">{{ currentItem.createTime }}</el-descriptions-item>
  228. <el-descriptions-item label="工作性质">{{ currentItem.workType === 'full_time' ? '全职' : '兼职' }}</el-descriptions-item>
  229. </el-descriptions>
  230. </div>
  231. <div class="section-block">
  232. <div class="section-title">实名认证</div>
  233. <div class="cert-row">
  234. <div class="cert-item" @click="handleViewImage(currentItem.idCardFrontUrl)">
  235. <el-image :src="currentItem.idCardFrontUrl || ''" fit="cover" class="cert-img">
  236. <template #error><div class="img-slot"><el-icon><Picture /></el-icon></div></template>
  237. </el-image>
  238. <div class="cert-name">身份证人像面</div>
  239. </div>
  240. <div class="cert-item" @click="handleViewImage(currentItem.idCardBackUrl)">
  241. <el-image :src="currentItem.idCardBackUrl || ''" fit="cover" class="cert-img">
  242. <template #error><div class="img-slot"><el-icon><Picture /></el-icon></div></template>
  243. </el-image>
  244. <div class="cert-name">身份证国徽面</div>
  245. </div>
  246. </div>
  247. </div>
  248. <div class="section-block">
  249. <div class="section-title">资质认证</div>
  250. <div class="cert-row" v-if="qualImageUrlList.length">
  251. <div class="cert-item" v-for="(img, index) in qualImageUrlList" :key="index" @click="handleViewImage(img)">
  252. <el-image :src="img" fit="cover" class="cert-img">
  253. <template #error><div class="img-slot"><el-icon><Picture /></el-icon></div></template>
  254. </el-image>
  255. </div>
  256. </div>
  257. <div v-else class="empty-text">暂无资质图片</div>
  258. </div>
  259. <div class="section-block">
  260. <div class="section-title">技能标签</div>
  261. <div class="tag-list">
  262. <el-tag v-for="tag in currentItem.tags" :key="tag.id" :type="tag.colorType" size="large" style="margin-right: 12px; margin-bottom: 8px;">{{ tag.name }}</el-tag>
  263. </div>
  264. </div>
  265. </div>
  266. </el-tab-pane>
  267. <el-tab-pane label="服务订单" name="orders">
  268. <div class="tab-content-wrapper">
  269. <el-table v-loading="logLoading" :data="serviceOrderData" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
  270. <el-table-column prop="code" label="订单号" width="160" show-overflow-tooltip />
  271. <el-table-column label="服务项目" show-overflow-tooltip>
  272. <template #default="{ row }">
  273. {{ getServiceName(row.service) }}
  274. </template>
  275. </el-table-column>
  276. <el-table-column prop="price" label="收入" width="100">
  277. <template #default="{ row }">
  278. <span style="color: #67c23a; font-weight: bold; font-size: 15px;">+{{ (row.price / 100).toFixed(2) }}</span>
  279. </template>
  280. </el-table-column>
  281. <el-table-column prop="serviceTime" label="时间" width="160" show-overflow-tooltip />
  282. <el-table-column prop="status" label="状态" width="90">
  283. <template #default="{ row }">
  284. <el-tag :type="getSubOrderStatusType(row.status)" size="small">
  285. {{ getSubOrderStatusName(row.status) }}
  286. </el-tag>
  287. </template>
  288. </el-table-column>
  289. </el-table>
  290. </div>
  291. </el-tab-pane>
  292. <el-tab-pane label="积分记录" name="pointLogs">
  293. <div class="tab-content-wrapper">
  294. <el-table v-loading="logLoading" :data="pointsLogData" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
  295. <el-table-column prop="createTime" label="变动时间" width="180" />
  296. <el-table-column prop="bizType" label="业务类型" width="120">
  297. <template #default="{ row }">
  298. <el-tag :type="getPointsBizTypeTag(row.bizType)" size="small" effect="plain">{{ getPointsBizTypeName(row.bizType) }}</el-tag>
  299. </template>
  300. </el-table-column>
  301. <el-table-column prop="amount" label="变动数值" width="120">
  302. <template #default="{ row }">
  303. <span :style="{ color: ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '#67c23a' : '#f56c6c', fontWeight: 'bold', fontSize: '15px' }">
  304. {{ ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '+' : '-' }}{{ row.amount }}
  305. </span>
  306. </template>
  307. </el-table-column>
  308. <el-table-column prop="reason" label="变动原因" show-overflow-tooltip />
  309. <el-table-column prop="operatorId" label="操作人" width="120" />
  310. </el-table>
  311. </div>
  312. </el-tab-pane>
  313. <el-tab-pane label="余额变动" name="balanceLogs">
  314. <div class="tab-content-wrapper">
  315. <el-table v-loading="logLoading" :data="balanceLogData" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
  316. <el-table-column prop="createTime" label="变动时间" width="180" show-overflow-tooltip />
  317. <el-table-column prop="subType" label="资金类型" width="120">
  318. <template #default="{ row }">
  319. <el-tag :type="getBalanceBizTypeTag(row.subType)" size="small" effect="plain">{{ getBalanceBizTypeName(row.bizType) }}</el-tag>
  320. </template>
  321. </el-table-column>
  322. <el-table-column prop="amount" label="变动金额" width="120">
  323. <template #default="{ row }">
  324. <span :style="{ color: ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '#67c23a' : '#f56c6c', fontWeight: 'bold', fontSize: '15px' }">
  325. {{ ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '+' : '-' }}{{ (row.amount / 100).toFixed(2) }}
  326. </span>
  327. </template>
  328. </el-table-column>
  329. <el-table-column prop="balanceAfter" label="变动后余额" width="120">
  330. <template #default="{ row }">
  331. <span>¥{{ (row.balanceAfter / 100).toFixed(2) }}</span>
  332. </template>
  333. </el-table-column>
  334. <el-table-column prop="reason" label="备注说明" show-overflow-tooltip />
  335. <el-table-column prop="operatorId" label="操作人" width="100" />
  336. </el-table>
  337. </div>
  338. </el-tab-pane>
  339. <el-tab-pane label="奖惩记录" name="rewards">
  340. <div class="tab-content-wrapper">
  341. <el-table v-loading="logLoading" :data="rewardLogData" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
  342. <el-table-column prop="createTime" label="操作时间" width="180" />
  343. <el-table-column prop="bizType" label="奖惩类型" width="100">
  344. <template #default="{ row }">
  345. <el-tag :type="fulfillerEnums.RewardBizType[row.bizType]?.tagType || 'info'" size="small" effect="plain">
  346. {{ getRewardBizTypeName(row.bizType) }}
  347. </el-tag>
  348. </template>
  349. </el-table-column>
  350. <!-- <el-table-column prop="type" label="操作" width="80">
  351. <template #default="{ row }">
  352. <el-tag :type="['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? 'success' : 'danger'" size="small">{{ ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '增加' : '减少' }}</el-tag>
  353. </template>
  354. </el-table-column> -->
  355. <el-table-column prop="target" label="关联项目" width="100">
  356. <template #default="{ row }">
  357. <el-tag type="info" size="small" effect="plain">{{ row.target === 'points' ? '积分' : '余额' }}</el-tag>
  358. </template>
  359. </el-table-column>
  360. <el-table-column prop="amount" label="涉及数值" width="120">
  361. <template #default="{ row }">
  362. <span :style="{ color: ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '#67c23a' : '#f56c6c', fontWeight: 'bold' }">
  363. {{ ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '+' : '-' }}{{ row.target === 'balance' ? (row.amount / 100).toFixed(2) : row.amount }} {{ row.target === 'points' ? '分' : '元' }}
  364. </span>
  365. </template>
  366. </el-table-column>
  367. <el-table-column prop="reason" label="奖惩原因" show-overflow-tooltip />
  368. <el-table-column prop="operatorName" label="操作人" width="100" />
  369. </el-table>
  370. </div>
  371. </el-tab-pane>
  372. </el-tabs>
  373. </div>
  374. </el-drawer>
  375. <!-- 编辑弹窗 -->
  376. <el-dialog v-model="editDialog.visible" title="编辑履约者" width="600px" top="5vh">
  377. <el-form :model="editDialog.form" label-width="90px">
  378. <el-row :gutter="20">
  379. <el-col :span="12">
  380. <el-form-item label="姓名" required>
  381. <el-input v-model="editDialog.form.name" />
  382. </el-form-item>
  383. </el-col>
  384. <el-col :span="12">
  385. <el-form-item label="手机号" required>
  386. <el-input v-model="editDialog.form.phone" />
  387. </el-form-item>
  388. </el-col>
  389. </el-row>
  390. <el-form-item label="登录密码">
  391. <el-input v-model="editDialog.form.password" type="password" placeholder="不修改请留空" show-password />
  392. </el-form-item>
  393. <el-row :gutter="20">
  394. <el-col :span="12">
  395. <el-form-item label="性别">
  396. <el-radio-group v-model="editDialog.form.gender">
  397. <el-radio label="0">男</el-radio>
  398. <el-radio label="1">女</el-radio>
  399. </el-radio-group>
  400. </el-form-item>
  401. </el-col>
  402. <el-col :span="12">
  403. <el-form-item label="身份证号">
  404. <el-input v-model="editDialog.form.idCard" />
  405. </el-form-item>
  406. </el-col>
  407. </el-row>
  408. <el-row :gutter="20">
  409. <el-col :span="12">
  410. <el-form-item label="服务城市">
  411. <el-cascader v-model="editDialog.cascaderValue" :options="cityCascaderOptions" :props="{ checkStrictly: true }" placeholder="请选择城市/区域" clearable style="width: 100%" @change="handleEditCascaderChange" />
  412. </el-form-item>
  413. </el-col>
  414. <el-col :span="12">
  415. <el-form-item label="归属站点">
  416. <el-select v-model="editDialog.form.stationId" placeholder="请选择站点" style="width: 100%">
  417. <el-option v-for="station in editDialog.stationOptions" :key="station.id" :label="station.name" :value="station.id" />
  418. </el-select>
  419. </el-form-item>
  420. </el-col>
  421. </el-row>
  422. <el-row :gutter="20">
  423. <el-col :span="12">
  424. <el-form-item label="等级">
  425. <el-input v-model="editDialog.form.levelId" placeholder="等级ID" />
  426. </el-form-item>
  427. </el-col>
  428. <el-col :span="12">
  429. <el-form-item label="当前状态">
  430. <el-select v-model="editDialog.form.status" style="width: 100%">
  431. <el-option label="接单中" value="busy" />
  432. <el-option label="休息" value="resting" />
  433. <el-option label="禁用" value="disabled" />
  434. </el-select>
  435. </el-form-item>
  436. </el-col>
  437. </el-row>
  438. <el-form-item label="认证状态">
  439. <el-checkbox v-model="editDialog.form.authId">身份证认证</el-checkbox>
  440. <el-checkbox v-model="editDialog.form.authQual">专业资质认证</el-checkbox>
  441. </el-form-item>
  442. <el-form-item label="技能标签">
  443. <el-checkbox-group v-model="editDialog.form.tagIds">
  444. <el-checkbox v-for="t in allTags" :key="t.id" :label="t.id" :value="t.id">{{ t.name }}</el-checkbox>
  445. </el-checkbox-group>
  446. </el-form-item>
  447. <el-form-item label="服务类型">
  448. <el-select v-model="editDialog.serviceTypesArray" multiple placeholder="请选择服务类型" style="width: 100%">
  449. <el-option v-for="item in serviceOptions" :key="item.id" :label="item.name" :value="String(item.id)" />
  450. </el-select>
  451. </el-form-item>
  452. </el-form>
  453. <template #footer>
  454. <span class="dialog-footer">
  455. <el-button @click="editDialog.visible = false">取消</el-button>
  456. <el-button type="primary" @click="saveEdit">保存变更</el-button>
  457. </span>
  458. </template>
  459. </el-dialog>
  460. <!-- 奖惩弹窗 -->
  461. <el-dialog v-model="rewardDialog.visible" title="人工奖惩操作" width="450px">
  462. <div class="user-preview-box">
  463. 当前操作对象:<b>{{ rewardDialog.userName }}</b>
  464. </div>
  465. <el-form :model="rewardDialog.form" label-width="80px" style="margin-top: 20px;">
  466. <el-form-item label="操作类型">
  467. <el-radio-group v-model="rewardDialog.form.type">
  468. <el-radio :label="fulfillerEnums.ActionType.ADD">增加</el-radio>
  469. <el-radio :label="fulfillerEnums.ActionType.REDUCE">减少</el-radio>
  470. </el-radio-group>
  471. </el-form-item>
  472. <el-form-item label="调整项目">
  473. <el-radio-group v-model="rewardDialog.form.target">
  474. <el-radio label="points">积分</el-radio>
  475. <el-radio label="balance">金额 (元)</el-radio>
  476. </el-radio-group>
  477. </el-form-item>
  478. <el-form-item label="数额" required>
  479. <el-input-number v-model="rewardDialog.form.amount" :min="1" />
  480. </el-form-item>
  481. <el-form-item label="原因备注" required>
  482. <el-input v-model="rewardDialog.form.reason" type="textarea" placeholder="请输入奖惩原因..." />
  483. </el-form-item>
  484. </el-form>
  485. <template #footer>
  486. <span class="dialog-footer">
  487. <el-button @click="rewardDialog.visible = false">取消</el-button>
  488. <el-button type="primary" @click="submitReward">确认执行</el-button>
  489. </span>
  490. </template>
  491. </el-dialog>
  492. <!-- 新增履约者弹窗 -->
  493. <el-dialog v-model="createDialog.visible" title="新增履约者" width="500px">
  494. <el-form :model="createDialog.form" label-width="80px">
  495. <el-form-item label="姓名" required>
  496. <el-input v-model="createDialog.form.name" placeholder="请输入真实姓名" />
  497. </el-form-item>
  498. <el-form-item label="手机号" required>
  499. <el-input v-model="createDialog.form.phone" placeholder="作为登录账号" />
  500. </el-form-item>
  501. <el-form-item label="登录密码" required>
  502. <el-input v-model="createDialog.form.password" show-password placeholder="设置初始密码" />
  503. </el-form-item>
  504. <el-form-item label="性别">
  505. <el-radio-group v-model="createDialog.form.gender">
  506. <el-radio label="0">男</el-radio>
  507. <el-radio label="1">女</el-radio>
  508. </el-radio-group>
  509. </el-form-item>
  510. <el-form-item label="服务城市">
  511. <el-cascader v-model="createDialog.cascaderValue" :options="cityCascaderOptions" :props="{ checkStrictly: true }" placeholder="请选择城市/区域" clearable style="width: 100%" @change="handleCreateCascaderChange" />
  512. </el-form-item>
  513. <el-form-item label="归属站点">
  514. <el-select v-model="createDialog.form.stationId" placeholder="请选择站点" style="width: 100%">
  515. <el-option v-for="station in createDialog.stationOptions" :key="station.id" :label="station.name" :value="station.id" />
  516. </el-select>
  517. </el-form-item>
  518. <el-form-item label="服务类型">
  519. <el-select v-model="createDialog.serviceTypesArray" multiple placeholder="请选择服务类型" style="width: 100%">
  520. <el-option v-for="item in serviceOptions" :key="item.id" :label="item.name" :value="String(item.id)" />
  521. </el-select>
  522. </el-form-item>
  523. </el-form>
  524. <template #footer>
  525. <el-button @click="createDialog.visible = false">取消</el-button>
  526. <el-button type="primary" @click="submitCreate">确认创建</el-button>
  527. </template>
  528. </el-dialog>
  529. <!-- 积分调整弹窗 -->
  530. <el-dialog v-model="pointsDialog.visible" title="修改积分" width="400px">
  531. <el-form :model="pointsDialog.form" label-width="80px">
  532. <el-form-item label="当前积分">
  533. <strong>{{ pointsDialog.currentRow?.points }}</strong>
  534. </el-form-item>
  535. <el-form-item label="调整方式">
  536. <el-radio-group v-model="pointsDialog.form.type">
  537. <el-radio :label="fulfillerEnums.ActionType.ADD">增加</el-radio>
  538. <el-radio :label="fulfillerEnums.ActionType.REDUCE">扣除</el-radio>
  539. </el-radio-group>
  540. </el-form-item>
  541. <el-form-item label="调整数值" required>
  542. <el-input-number v-model="pointsDialog.form.amount" :min="1" />
  543. </el-form-item>
  544. <el-form-item label="调整原因" required>
  545. <el-input v-model="pointsDialog.form.reason" type="textarea" placeholder="请输入备注说明" />
  546. </el-form-item>
  547. </el-form>
  548. <template #footer>
  549. <el-button @click="pointsDialog.visible = false">取消</el-button>
  550. <el-button type="primary" @click="submitPointsAdjust">确认调整</el-button>
  551. </template>
  552. </el-dialog>
  553. <!-- 余额调整弹窗 -->
  554. <el-dialog v-model="balanceDialog.visible" title="余额增减" width="450px">
  555. <el-form :model="balanceDialog.form" label-width="80px">
  556. <el-form-item label="当前余额">
  557. <span style="color: #f56c6c; font-weight: bold">¥{{ (balanceDialog.currentRow?.balance / 100).toFixed(2) }}</span>
  558. </el-form-item>
  559. <el-form-item label="扣减类型">
  560. <el-radio-group v-model="balanceDialog.form.type" @change="balanceDialog.form.subType = balanceDialog.form.type === fulfillerEnums.ActionType.ADD ? 'admin_reward' : 'admin_punish'">
  561. <el-radio :label="fulfillerEnums.ActionType.ADD">增加</el-radio>
  562. <el-radio :label="fulfillerEnums.ActionType.REDUCE">减少</el-radio>
  563. </el-radio-group>
  564. </el-form-item>
  565. <el-form-item label="调整类型">
  566. <el-radio-group v-model="balanceDialog.form.subType">
  567. <template v-if="balanceDialog.form.type === fulfillerEnums.ActionType.ADD">
  568. <el-radio label="admin_reward">奖励</el-radio>
  569. <el-radio label="admin_ajust">其他 (后台调整)</el-radio>
  570. </template>
  571. <template v-else>
  572. <el-radio label="admin_punish">惩罚</el-radio>
  573. <el-radio label="salary">工资发放</el-radio>
  574. <el-radio label="admin_ajust">其他 (后台调整)</el-radio>
  575. </template>
  576. </el-radio-group>
  577. </el-form-item>
  578. <el-form-item label="金额" required>
  579. <el-input-number v-model="balanceDialog.form.amount" :min="0.01" :precision="2" :step="10" />
  580. </el-form-item>
  581. <el-form-item label="备注说明" required>
  582. <el-input v-model="balanceDialog.form.reason" type="textarea" placeholder="请输入资金变动说明" />
  583. </el-form-item>
  584. </el-form>
  585. <template #footer>
  586. <el-button @click="balanceDialog.visible = false">取消</el-button>
  587. <el-button type="primary" @click="submitBalanceAdjust">确认执行</el-button>
  588. </template>
  589. </el-dialog>
  590. </div>
  591. </template>
  592. <script setup lang="ts">
  593. import { ref, reactive, computed, onMounted } from 'vue'
  594. import { ElMessage, ElMessageBox } from 'element-plus'
  595. import {
  596. listFulfiller, getFulfiller, addFulfiller, updateFulfiller,
  597. changeStatus, resetPwd, reward, adjustPoints, adjustBalance,
  598. listPointsLog, listBalanceLog, listRewardLog
  599. } from '@/api/fulfiller/pool'
  600. import { listSubOrderOnFulfiller } from '@/api/order/subOrder/index'
  601. import { listOnStore as listServiceOnStore } from '@/api/service/list/index'
  602. import type {
  603. FlfFulfillerVO, FlfFulfillerForm, FlfFulfillerQuery,
  604. FlfRewardForm, FlfAdjustPointsForm, FlfAdjustBalanceForm,
  605. FlfPointsLogVO, FlfBalanceLogVO, FlfRewardLogVO
  606. } from '@/api/fulfiller/pool/types'
  607. import { listAllTag } from '@/api/fulfiller/tag'
  608. import type { FlfTagVO } from '@/api/fulfiller/tag/types'
  609. import { listOnStore } from '@/api/system/areaStation'
  610. import type { SysAreaStationOnStoreVo } from '@/api/system/areaStation/types'
  611. import fulfillerEnums from '@/enums/fulfiller.json'
  612. const loading = ref(false)
  613. const searchKey = ref('')
  614. const activeTab = ref('all')
  615. const total = ref(0)
  616. const tableData = ref<FlfFulfillerVO[]>([])
  617. const allTags = ref<FlfTagVO[]>([])
  618. const areaStationList = ref<SysAreaStationOnStoreVo[]>([])
  619. const cityCascaderOptions = ref<any[]>([])
  620. const stationOptions = ref<SysAreaStationOnStoreVo[]>([])
  621. const filterCascaderValue = ref<any[]>([])
  622. const queryParams = reactive<FlfFulfillerQuery>({
  623. pageNum: 1,
  624. pageSize: 10
  625. })
  626. // Drawer State
  627. const detailVisible = ref(false)
  628. const activeDetailTab = ref('info')
  629. const currentItem = ref<FlfFulfillerVO | null>(null)
  630. /** 资质图片URL列表(从逗号分隔字符串解析) */
  631. const qualImageUrlList = computed(() => {
  632. if (!currentItem.value?.qualImageUrls) return []
  633. return currentItem.value.qualImageUrls.split(',').filter(Boolean)
  634. })
  635. // Log data for detail tabs
  636. const pointsLogData = ref<FlfPointsLogVO[]>([])
  637. const balanceLogData = ref<FlfBalanceLogVO[]>([])
  638. const rewardLogData = ref<FlfRewardLogVO[]>([])
  639. const serviceOrderData = ref<any[]>([])
  640. const serviceOptions = ref<any[]>([])
  641. const logLoading = ref(false)
  642. /** 查询列表 */
  643. const getList = async () => {
  644. loading.value = true
  645. try {
  646. const params: FlfFulfillerQuery = {
  647. pageNum: queryParams.pageNum,
  648. pageSize: queryParams.pageSize,
  649. status: activeTab.value === 'all' ? undefined : activeTab.value,
  650. keyword: searchKey.value || undefined,
  651. cityCode: queryParams.cityCode || undefined,
  652. stationId: queryParams.stationId || undefined
  653. }
  654. const res = await listFulfiller(params)
  655. tableData.value = res.rows
  656. total.value = res.total
  657. } finally {
  658. loading.value = false
  659. }
  660. }
  661. /** 加载全部标签(选择器用) */
  662. const loadAllTags = async () => {
  663. try {
  664. const res = await listAllTag({ category: 'fulfiller' })
  665. allTags.value = res.data || []
  666. } catch { /* ignore */ }
  667. }
  668. /** 加载区域站点数据并构建级联树 */
  669. const loadAreaStations = async () => {
  670. try {
  671. const res = await listOnStore()
  672. const list = res.data || []
  673. areaStationList.value = list
  674. // 构建城市→区域的级联树(不含站点type=2)
  675. const cities = list.filter(item => item.type === 0)
  676. cityCascaderOptions.value = cities.map(city => {
  677. const districts = list.filter(d => d.parentId == city.id && d.type === 1)
  678. return {
  679. value: city.id,
  680. label: city.name,
  681. children: districts.length > 0 ? districts.map(d => ({ value: d.id, label: d.name })) : undefined
  682. }
  683. })
  684. } catch { /* ignore */ }
  685. }
  686. /** 根据级联选择的最后一级ID加载站点列表 */
  687. const loadStationsByAreaId = (areaId: string | number) => {
  688. stationOptions.value = areaStationList.value.filter(item => item.parentId == areaId && item.type === 2)
  689. }
  690. /** 根据级联值获取cityCode和cityName */
  691. const getCityInfoFromCascader = (cascaderValue: any[]) => {
  692. if (!cascaderValue || cascaderValue.length === 0) return { cityCode: '', cityName: '' }
  693. const lastId = cascaderValue[cascaderValue.length - 1]
  694. const names: string[] = []
  695. for (const id of cascaderValue) {
  696. const item = areaStationList.value.find(i => i.id == id)
  697. if (item) names.push(item.name || '')
  698. }
  699. return { cityCode: String(lastId), cityName: names.join(' ') }
  700. }
  701. /** 搜索框回车/清除触发查询 */
  702. const handleSearch = () => {
  703. queryParams.pageNum = 1
  704. getList()
  705. }
  706. const handleSizeChange = (val: number) => { queryParams.pageSize = val; getList() }
  707. const handleCurrentChange = (val: number) => { queryParams.pageNum = val; getList() }
  708. /** 积分业务类型标签颜色 */
  709. const getPointsBizTypeTag = (type: string) => {
  710. return fulfillerEnums.PointsBizType[type]?.tagType || 'info'
  711. }
  712. /** 积分业务类型中文名称 */
  713. const getPointsBizTypeName = (type: string) => {
  714. return fulfillerEnums.PointsBizType[type]?.label || type
  715. }
  716. /** 余额资金类型颜色 */
  717. const getBalanceBizTypeTag = (type: string) => {
  718. return fulfillerEnums.BalanceBizType[type]?.tagType || 'info'
  719. }
  720. /** 余额资金类型中文名称 */
  721. const getBalanceBizTypeName = (type: string) => {
  722. return fulfillerEnums.BalanceBizType[type]?.label || type
  723. }
  724. /** 奖惩业务类型名称 */
  725. const getRewardBizTypeName = (type: string) => {
  726. return fulfillerEnums.RewardBizType[type]?.label || type
  727. }
  728. /** 加载服务项目列表用于名称映射 */
  729. const loadServiceOptions = async () => {
  730. try {
  731. const res = await listServiceOnStore()
  732. serviceOptions.value = res.data || []
  733. } catch { /* ignore */ }
  734. }
  735. const getServiceName = (serviceId: number | string) => {
  736. const item = serviceOptions.value.find(i => i.id === serviceId)
  737. return item ? item.name : '未知服务'
  738. }
  739. const getSubOrderStatusName = (status: number) => {
  740. const map: Record<number, string> = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '已完成', 5: '已取消' }
  741. return map[status] || '未知'
  742. }
  743. const getSubOrderStatusType = (status: number) => {
  744. const map: Record<number, string> = { 0: 'info', 1: 'warning', 2: 'primary', 3: 'success', 4: 'success', 5: 'danger' }
  745. return map[status] || 'info'
  746. }
  747. const rewardDialog = reactive({
  748. visible: false,
  749. userName: '',
  750. fulfillerId: 0 as number | string,
  751. form: { type: fulfillerEnums.ActionType.ADD, target: 'points', amount: 10, reason: '' }
  752. })
  753. const editDialog = reactive({
  754. visible: false,
  755. form: {} as FlfFulfillerForm,
  756. cascaderValue: [] as any[],
  757. stationOptions: [] as SysAreaStationOnStoreVo[],
  758. serviceTypesArray: [] as string[]
  759. })
  760. const createDialog = reactive({
  761. visible: false,
  762. form: {
  763. name: '', phone: '', password: '', cityCode: '', cityName: '', stationId: undefined as any, gender: '0', workType: 'full_time'
  764. },
  765. cascaderValue: [] as any[],
  766. stationOptions: [] as SysAreaStationOnStoreVo[],
  767. serviceTypesArray: [] as string[]
  768. })
  769. const pointsDialog = reactive({
  770. visible: false,
  771. currentRow: null as FlfFulfillerVO | null,
  772. form: { type: fulfillerEnums.ActionType.ADD, amount: 0, reason: '' }
  773. })
  774. const balanceDialog = reactive({
  775. visible: false,
  776. currentRow: null as FlfFulfillerVO | null,
  777. form: { type: fulfillerEnums.ActionType.ADD, subType: 'admin_reward', amount: 0, reason: '' }
  778. })
  779. const getStatusText = (status: string) => {
  780. const map: Record<string, string> = { busy: '接单中', resting: '休息', disabled: '禁用', frozen: '冻结' }
  781. return map[status] || '未知'
  782. }
  783. const getLevelText = (levelName: string) => {
  784. return levelName || '普通'
  785. }
  786. const getLevelType = (levelName: string) => {
  787. if (!levelName) return 'info'
  788. if (levelName.includes('金')) return 'warning'
  789. if (levelName.includes('银')) return 'info'
  790. if (levelName.includes('铜')) return 'danger'
  791. return 'info'
  792. }
  793. const handleTabClick = (tab: any) => {
  794. activeTab.value = tab.paneName
  795. queryParams.pageNum = 1
  796. getList()
  797. }
  798. /** 加载日志数据 */
  799. const loadLogs = async (fulfillerId: string | number) => {
  800. logLoading.value = true
  801. try {
  802. const [pRes, bRes, rRes, sRes] = await Promise.all([
  803. listPointsLog(fulfillerId, { pageNum: 1, pageSize: 20 }),
  804. listBalanceLog(fulfillerId, { pageNum: 1, pageSize: 20 }),
  805. listRewardLog(fulfillerId, { pageNum: 1, pageSize: 20 }),
  806. listSubOrderOnFulfiller(fulfillerId)
  807. ])
  808. pointsLogData.value = pRes.rows || []
  809. balanceLogData.value = bRes.rows || []
  810. rewardLogData.value = rRes.rows || []
  811. serviceOrderData.value = sRes.data || []
  812. } catch { /* ignore */ } finally {
  813. logLoading.value = false
  814. }
  815. }
  816. const handleDetail = async (row: FlfFulfillerVO) => {
  817. try {
  818. const res = await getFulfiller(row.id)
  819. currentItem.value = res.data
  820. } catch {
  821. currentItem.value = { ...row }
  822. }
  823. activeDetailTab.value = 'info'
  824. detailVisible.value = true
  825. loadLogs(row.id)
  826. }
  827. const handleEdit = (row: FlfFulfillerVO) => {
  828. editDialog.form = {
  829. id: row.id,
  830. name: row.name,
  831. phone: row.phone,
  832. gender: row.gender,
  833. idCard: row.idCard,
  834. cityCode: row.cityCode,
  835. cityName: row.cityName,
  836. stationId: row.stationId,
  837. levelId: row.levelId,
  838. status: row.status,
  839. authId: row.authId,
  840. authQual: row.authQual,
  841. tagIds: row.tags ? row.tags.map(t => t.id) : [],
  842. serviceTypes: row.serviceTypes
  843. }
  844. editDialog.serviceTypesArray = row.serviceTypes ? row.serviceTypes.split(',') : []
  845. // 根据cityCode构建级联选择器的值
  846. editDialog.cascaderValue = []
  847. editDialog.stationOptions = []
  848. if (row.cityCode) {
  849. const item = areaStationList.value.find(i => String(i.id) === row.cityCode)
  850. if (item) {
  851. if (item.type === 1 && item.parentId) {
  852. // 区域级:cascaderValue = [城市ID, 区域ID]
  853. editDialog.cascaderValue = [item.parentId, item.id]
  854. } else {
  855. // 城市级:cascaderValue = [城市ID]
  856. editDialog.cascaderValue = [item.id]
  857. }
  858. loadStationsByAreaId(item.id)
  859. editDialog.stationOptions = stationOptions.value
  860. }
  861. }
  862. editDialog.visible = true
  863. }
  864. const handleCreate = () => {
  865. createDialog.form = { name: '', phone: '', password: '', cityCode: '', cityName: '', stationId: undefined, gender: '0', workType: 'full_time' }
  866. createDialog.cascaderValue = []
  867. createDialog.stationOptions = []
  868. createDialog.serviceTypesArray = []
  869. createDialog.visible = true
  870. }
  871. const submitCreate = async () => {
  872. if (!createDialog.form.name || !createDialog.form.phone || !createDialog.form.password) {
  873. ElMessage.warning('请填写完整信息')
  874. return
  875. }
  876. try {
  877. const submitForm = { ...createDialog.form, serviceTypes: createDialog.serviceTypesArray.join(',') };
  878. await addFulfiller(submitForm as FlfFulfillerForm)
  879. createDialog.visible = false
  880. ElMessage.success('创建成功')
  881. getList()
  882. } catch { /* handled by interceptor */ }
  883. }
  884. const saveEdit = async () => {
  885. try {
  886. const submitForm = { ...editDialog.form, serviceTypes: editDialog.serviceTypesArray.join(',') };
  887. await updateFulfiller(submitForm)
  888. ElMessage.success('更新成功')
  889. editDialog.visible = false
  890. getList()
  891. } catch { /* handled by interceptor */ }
  892. }
  893. const handleReward = (row: FlfFulfillerVO) => {
  894. rewardDialog.userName = row.name
  895. rewardDialog.fulfillerId = row.id
  896. rewardDialog.form = { type: fulfillerEnums.ActionType.ADD, target: 'points', amount: 10, reason: '' }
  897. rewardDialog.visible = true
  898. }
  899. const submitReward = async () => {
  900. try {
  901. // 余额目标时将元转为分
  902. const amount = rewardDialog.form.target === 'balance'
  903. ? Math.round(rewardDialog.form.amount * 100)
  904. : rewardDialog.form.amount
  905. await reward({
  906. fulfillerId: rewardDialog.fulfillerId,
  907. type: rewardDialog.form.type,
  908. target: rewardDialog.form.target,
  909. amount: amount,
  910. reason: rewardDialog.form.reason,
  911. bizType: rewardDialog.form.type === fulfillerEnums.ActionType.ADD ? 'admin_reward' : 'admin_punish'
  912. } as any)
  913. ElMessage.success('操作成功')
  914. rewardDialog.visible = false
  915. getList()
  916. } catch { /* handled by interceptor */ }
  917. }
  918. const handleCommand = async (cmd: string, row: FlfFulfillerVO) => {
  919. if (cmd === 'adjustPoints') {
  920. pointsDialog.currentRow = row
  921. pointsDialog.form = { type: fulfillerEnums.ActionType.ADD, amount: 0, reason: '' }
  922. pointsDialog.visible = true
  923. } else if (cmd === 'adjustBalance') {
  924. balanceDialog.currentRow = row
  925. balanceDialog.form = { type: fulfillerEnums.ActionType.ADD, subType: 'admin_reward', amount: 0, reason: '' }
  926. balanceDialog.visible = true
  927. } else if (cmd === 'disable') {
  928. await ElMessageBox.confirm(`确定禁用履约者【${row.name}】吗?禁用后将无法接单。`, '提示', { type: 'warning' })
  929. try {
  930. await changeStatus(row.id, 'disabled')
  931. ElMessage.success('账号已禁用')
  932. getList()
  933. } catch { /* handled by interceptor */ }
  934. } else if (cmd === 'enable') {
  935. await ElMessageBox.confirm(`确定启用履约者【${row.name}】吗?`, '提示', { type: 'success' })
  936. try {
  937. await changeStatus(row.id, 'resting')
  938. ElMessage.success('账号已启用')
  939. getList()
  940. } catch { /* handled by interceptor */ }
  941. } else if (cmd === 'resetPwd') {
  942. await ElMessageBox.confirm('确定重置密码为默认密码 [123456] 吗?', '提示', { type: 'info' })
  943. try {
  944. await resetPwd(row.id, '123456')
  945. ElMessage.success('密码重置成功')
  946. } catch { /* handled by interceptor */ }
  947. }
  948. }
  949. const submitPointsAdjust = async () => {
  950. if (!pointsDialog.currentRow) return
  951. try {
  952. await adjustPoints({
  953. fulfillerId: pointsDialog.currentRow.id,
  954. type: pointsDialog.form.type,
  955. amount: pointsDialog.form.amount,
  956. reason: pointsDialog.form.reason,
  957. bizType: pointsDialog.form.type === fulfillerEnums.ActionType.ADD ? 'admin_reward' : 'admin_punish'
  958. } as any)
  959. ElMessage.success('积分调整成功')
  960. pointsDialog.visible = false
  961. getList()
  962. } catch { /* handled by interceptor */ }
  963. }
  964. const submitBalanceAdjust = async () => {
  965. if (!balanceDialog.currentRow) return
  966. try {
  967. // 将元转为分
  968. await adjustBalance({
  969. fulfillerId: balanceDialog.currentRow.id,
  970. type: balanceDialog.form.type,
  971. subType: balanceDialog.form.subType,
  972. amount: Math.round(balanceDialog.form.amount * 100),
  973. reason: balanceDialog.form.reason
  974. })
  975. ElMessage.success('余额调整成功')
  976. balanceDialog.visible = false
  977. getList()
  978. } catch { /* handled by interceptor */ }
  979. }
  980. const handleViewImage = (url: string) => {
  981. // Already handled by el-image preview
  982. }
  983. /** 顶部筛选:级联选择变化时 */
  984. const handleFilterCascaderChange = (val: any[]) => {
  985. if (val && val.length > 0) {
  986. const lastId = val[val.length - 1]
  987. queryParams.cityCode = String(lastId)
  988. loadStationsByAreaId(lastId)
  989. } else {
  990. queryParams.cityCode = undefined
  991. stationOptions.value = []
  992. }
  993. queryParams.stationId = undefined
  994. getList()
  995. }
  996. /** 编辑对话框:级联选择变化时 */
  997. const handleEditCascaderChange = (val: any[]) => {
  998. const { cityCode, cityName } = getCityInfoFromCascader(val)
  999. editDialog.form.cityCode = cityCode
  1000. editDialog.form.cityName = cityName
  1001. if (val && val.length > 0) {
  1002. const lastId = val[val.length - 1]
  1003. loadStationsByAreaId(lastId)
  1004. editDialog.stationOptions = stationOptions.value
  1005. } else {
  1006. editDialog.stationOptions = []
  1007. }
  1008. editDialog.form.stationId = undefined
  1009. }
  1010. /** 新增对话框:级联选择变化时 */
  1011. const handleCreateCascaderChange = (val: any[]) => {
  1012. const { cityCode, cityName } = getCityInfoFromCascader(val)
  1013. createDialog.form.cityCode = cityCode
  1014. createDialog.form.cityName = cityName
  1015. if (val && val.length > 0) {
  1016. const lastId = val[val.length - 1]
  1017. loadStationsByAreaId(lastId)
  1018. createDialog.stationOptions = stationOptions.value
  1019. } else {
  1020. createDialog.stationOptions = []
  1021. }
  1022. createDialog.form.stationId = undefined
  1023. }
  1024. onMounted(() => {
  1025. getList()
  1026. loadAllTags()
  1027. loadAreaStations()
  1028. loadServiceOptions()
  1029. })
  1030. </script>
  1031. <style scoped>
  1032. .page-container { padding: 20px; }
  1033. .table-card { border-radius: 8px; border: none; }
  1034. .card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
  1035. .status-tabs { margin-top: -15px; }
  1036. :deep(.el-tabs__header) { margin-bottom: 0; }
  1037. :deep(.el-tabs__nav-wrap::after) { height: 1px; background-color: #f0f2f5; }
  1038. .title { font-size: 18px; font-weight: bold; color: #303133; }
  1039. .right-panel { display: flex; align-items: center; }
  1040. .search-input { width: 240px; }
  1041. /* Table Content Styles */
  1042. .user-info { display: flex; align-items: center; }
  1043. .text-col { margin-left: 10px; display: flex; flex-direction: column; justify-content: center; }
  1044. .name-row { font-weight: bold; font-size: 14px; color: #333; display: flex; align-items: center; }
  1045. .gender-tag { margin-left: 5px; display: flex; align-items: center; }
  1046. .sub-text { font-size: 12px; color: #999; margin-top: 2px; }
  1047. .auth-row { display: flex; gap: 8px; flex-wrap: wrap; }
  1048. .auth-card {
  1049. font-size: 12px; padding: 2px 6px; border-radius: 4px; background: #f4f4f5; color: #909399;
  1050. display: flex; align-items: center; gap: 4px;
  1051. }
  1052. .auth-card.active { background: #ecf5ff; color: #409eff; }
  1053. .auth-card.need-review { background: #fef0f0; color: #f56c6c; border: 1px dashed #f56c6c; }
  1054. .finance-item { font-size: 13px; color: #606266; line-height: 1.6; }
  1055. .num { font-weight: bold; font-family: DIN, sans-serif; margin-left: 5px; color: #303133; }
  1056. .num.error { color: #f56c6c; }
  1057. .status-cell { display: flex; align-items: center; }
  1058. .status-dot { width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
  1059. .status-dot.resting { background: #e6a23c; }
  1060. .status-dot.busy { background: #409eff; }
  1061. .status-dot.disabled { background: #f56c6c; }
  1062. .status-dot.frozen { background: #909399; }
  1063. .op-cell { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
  1064. .pagination-container { display: flex; justify-content: flex-end; margin-top: 20px; }
  1065. /* Drawer Styles */
  1066. .user-header-card {
  1067. display: flex;
  1068. align-items: center;
  1069. padding: 20px;
  1070. background: linear-gradient(135deg, #f5f7fa 0%, #eef1f6 100%);
  1071. border-radius: 8px;
  1072. margin-bottom: 25px;
  1073. }
  1074. .header-info { margin-left: 20px; flex: 1; }
  1075. .top-row { display: flex; align-items: center; margin-bottom: 8px; }
  1076. .user-name { font-size: 20px; font-weight: bold; color: #303133; }
  1077. .status-badge {
  1078. margin-left: auto;
  1079. font-size: 12px; padding: 4px 10px; border-radius: 12px;
  1080. background: #e1f3d8; color: #67c23a;
  1081. }
  1082. .status-badge.resting { background: #faecd8; color: #e6a23c; }
  1083. .status-badge.disabled { background: #fde2e2; color: #f56c6c; }
  1084. .status-badge.busy { background: #d9ecff; color: #409eff; }
  1085. .status-badge.frozen { background: #f0f9eb; color: #909399; }
  1086. .sub-row { display: flex; align-items: center; font-size: 13px; color: #606266; margin-bottom: 8px; }
  1087. .info-item { display: flex; align-items: center; gap: 4px; }
  1088. .divider { margin: 0 10px; color: #dcdfe6; }
  1089. .tags-row { display: flex; align-items: center; gap: 5px; }
  1090. .data-metrics-row {
  1091. display: flex;
  1092. justify-content: space-around;
  1093. padding: 15px 0;
  1094. margin-bottom: 10px;
  1095. background: #fff;
  1096. border-bottom: 1px solid #f0f0f0;
  1097. }
  1098. .metric-item { text-align: center; flex: 1; }
  1099. .val { font-size: 20px; font-weight: bold; color: #303133; font-family: DIN, sans-serif; margin-bottom: 4px; }
  1100. .lbl { font-size: 12px; color: #909399; }
  1101. .text-primary { color: #409eff; }
  1102. .text-danger { color: #f56c6c; }
  1103. .text-warning { color: #e6a23c; }
  1104. .divider-v { width: 1px; background: #e0e0e0; height: 30px; align-self: center; }
  1105. .detail-tabs { margin-top: 0; }
  1106. .section-block { margin-bottom: 25px; }
  1107. .section-title { font-size: 15px; font-weight: bold; margin-bottom: 15px; border-left: 4px solid #409eff; padding-left: 10px; }
  1108. .cert-row { display: flex; gap: 15px; }
  1109. .cert-item { text-align: center; cursor: pointer; }
  1110. .cert-img { width: 120px; height: 80px; border-radius: 6px; border: 1px solid #dcdfe6; background: #f5f7fa; }
  1111. .img-slot { display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; color: #909399; font-size: 24px; }
  1112. .cert-name { font-size: 12px; color: #606266; margin-top: 5px; }
  1113. .tag-list { display: flex; flex-wrap: wrap; }
  1114. .tab-content-wrapper { padding: 10px 0; }
  1115. :deep(.el-table .el-table__cell) { padding: 12px 0; }
  1116. </style>