index.vue 54 KB

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