Files
video-factory/ui-src/src/views/admin/AgentList.vue
T
2026-08-05 08:53:43 +08:00

323 lines
13 KiB
Vue

<template>
<div class="page">
<div class="page-body">
<div class="search-bar">
<el-input v-model="filters.keyword" placeholder="名称/账号" clearable style="width:160px" />
<el-input v-model="filters.phone" placeholder="手机号" clearable style="width:140px" />
<el-cascader v-model="filters.cascaderRegion" :options="cascadeOptions" style="width:200px" placeholder="所在地区" clearable @change="onFilterCascaderChange" @click="lazyLoadCascades" />
<el-date-picker v-model="filters.expiryRange" type="daterange" range-separator="至" start-placeholder="到期开始" end-placeholder="到期结束" value-format="YYYY-MM-DD" style="width:220px" />
<el-button type="primary" @click="doSearch">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
<span class="search-spacer"></span>
<button class="btn-primary" @click="showCreate = true; lazyLoadCascades()">新建代理商</button>
</div>
<div class="table-wrapper">
<el-table :data="list" border stripe v-loading="loading" height="100%">
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="username" label="账号" width="140" />
<el-table-column prop="phone" label="手机号" width="130" />
<el-table-column prop="name" label="名称" />
<el-table-column label="受保护" width="80" align="center">
<template #default="{ row }">{{ row.region_protected ? '是' : '否' }}</template>
</el-table-column>
<el-table-column label="省份" width="120">
<template #default="{ row }">{{ row.province || '-' }}</template>
</el-table-column>
<el-table-column label="区域" width="120">
<template #default="{ row }">{{ row.region || '-' }}</template>
</el-table-column>
<el-table-column label="客户上限" width="100" align="right">
<template #default="{ row }">{{ row.max_customers ?? '-' }}</template>
</el-table-column>
<el-table-column label="到期" width="170">
<template #default="{ row }">{{ row.expired_at || '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
<el-button size="small" @click="handleRenew(row)">续费</el-button>
<el-button size="small" @click="handleViewRenewals(row)">续费记录</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-if="total > 0" class="pagination-wrap">
<el-pagination v-model:current-page="page" :page-size="pageSize" :total="total" :page-sizes="[20, 50, 100]" layout="total, sizes, prev, pager, next" background @current-change="fetchData" @size-change="onSizeChange" />
</div>
</div>
<el-dialog v-model="showCreate" title="新建代理商" width="500px">
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="90px">
<el-form-item label="账号" prop="username"><el-input v-model="createForm.username" /></el-form-item>
<el-form-item label="密码" prop="password"><el-input v-model="createForm.password" type="password" /></el-form-item>
<el-form-item label="手机号" prop="phone"><el-input v-model="createForm.phone" /></el-form-item>
<el-form-item label="名称" prop="name"><el-input v-model="createForm.name" /></el-form-item>
<el-form-item label="区域" prop="region">
<el-cascader v-model="createForm.cascaderRegion" :options="cascadeOptions" style="width:100%" placeholder="请选择省/市" @change="onCascaderChange" />
</el-form-item>
<el-form-item label="地区保护">
<el-switch v-model="createForm.region_protected" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showCreate = false">取消</el-button>
<el-button type="primary" :loading="creating" @click="handleCreate">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="showEdit" title="编辑代理商" width="500px">
<el-form ref="editFormRef" :model="editForm" :rules="editRules" label-width="90px">
<el-form-item label="名称" prop="name"><el-input v-model="editForm.name" /></el-form-item>
<el-form-item label="手机号" prop="phone"><el-input v-model="editForm.phone" /></el-form-item>
<el-form-item label="区域" prop="region">
<el-cascader v-model="editForm.cascaderRegion" :options="cascadeOptions" style="width:100%" placeholder="请选择省/市" @change="onEditCascaderChange" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showEdit = false">取消</el-button>
<el-button type="primary" :loading="updating" @click="handleUpdate">保存</el-button>
</template>
</el-dialog>
<PaymentDialog v-model="showRenew" mode="admin-renew" :agent-info="renewAgentInfo" @success="onRenewSuccess" />
<el-dialog v-model="showRenewals" title="续费记录" width="800px">
<el-table :data="renewalList" stripe v-loading="renewalLoading" max-height="460">
<el-table-column label="序号" width="60" type="index" />
<el-table-column prop="order_no" label="订单号" min-width="180" />
<el-table-column label="金额" width="100">
<template #default="{ row }">{{ (row.amount / 100).toFixed(2) }} </template>
</el-table-column>
<el-table-column prop="subject" label="说明" min-width="160" />
<el-table-column label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === 'success' ? 'success' : 'danger'" size="small">
{{ row.status === 'success' ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="时间" width="180">
<template #default="{ row }">{{ row.created_at || row.paid_at || '-' }}</template>
</el-table-column>
</el-table>
<div v-if="renewalList.length === 0" style="text-align:center;color:#999;padding:32px 0">暂无续费记录</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { listAgents, createAgent, updateAgent, listAgentRenewals } from '../../api/agent.js'
import { listRegionCascades } from '../../api/regionPricing.js'
import PaymentDialog from '../../components/PaymentDialog.vue'
const list = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const loading = ref(false)
const cascadeOptions = ref([])
const cascadeLoaded = ref(false)
const filters = ref({ keyword: '', phone: '', province: '', region: '', cascaderRegion: [], expiryRange: null })
const pageSizes = ref([20, 50, 100])
async function buildParams() {
const params = { page: page.value, pageSize: pageSize.value }
if (filters.value.keyword) params.keyword = filters.value.keyword
if (filters.value.phone) params.phone = filters.value.phone
if (filters.value.province) params.province = filters.value.province
if (filters.value.region) params.region = filters.value.region
if (filters.value.expiryRange && filters.value.expiryRange.length === 2) {
params.expired_at_from = filters.value.expiryRange[0]
params.expired_at_to = filters.value.expiryRange[1]
}
return params
}
async function fetchData() {
loading.value = true
try { const r = await listAgents(await buildParams()); list.value = r.list; total.value = r.total }
finally { loading.value = false }
}
function onFilterCascaderChange(val) {
if (val && val.length >= 2) {
filters.value.province = val[0]
filters.value.region = val[1]
} else {
filters.value.province = ''
filters.value.region = ''
}
}
function doSearch() { page.value = 1; fetchData() }
function onSizeChange(size) { pageSize.value = size; page.value = 1; fetchData() }
function resetSearch() {
filters.value = { keyword: '', phone: '', province: '', region: '', cascaderRegion: [], expiryRange: null }
page.value = 1
fetchData()
}
async function lazyLoadCascades() {
if (cascadeLoaded.value) return
cascadeLoaded.value = true
try {
const r = await listRegionCascades()
cascadeOptions.value = (r.list || []).map(item => ({
value: item.province,
label: item.province,
children: (item.cities || []).map(city => ({ value: city, label: city }))
}))
} catch (_) {}
}
// create
const showCreate = ref(false); const creating = ref(false); const createFormRef = ref(null)
const createForm = ref({ username: '', password: '', phone: '', name: '', region: '', region_protected: false, cascaderRegion: [] })
const createRules = {
username: [
{ required: true, message: '请输入账号', trigger: 'blur' },
{ min: 5, max: 18, message: '账号长度5-18位', trigger: 'blur' },
{ pattern: /^[a-zA-Z][a-zA-Z0-9_]{4,17}$/, message: '账号以字母开头,只能包含字母、数字和下划线', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 18, message: '密码长度6-18位', trigger: 'blur' },
{ pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]).{6,18}$/, message: '密码必须包含大写字母、小写字母、数字和特殊符号', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的11位手机号', trigger: 'blur' }
],
name: [{ required: true, message: '必填', trigger: 'blur' }],
region: [{ required: true, message: '请选择省/市', trigger: 'change' }]
}
function onCascaderChange(val) {
if (val && val.length >= 2) {
createForm.value.province = val[0]
createForm.value.region = val[1]
} else {
createForm.value.province = ''
createForm.value.region = ''
}
}
async function handleCreate() {
const v = await createFormRef.value.validate().catch(() => false)
if (!v) return; creating.value = true
try {
const payload = { ...createForm.value }
delete payload.cascaderRegion
await createAgent(payload)
ElMessage.success('创建成功'); showCreate.value = false; createForm.value = { username: '', password: '', phone: '', name: '', region: '', region_protected: false, cascaderRegion: [] }; await fetchData()
}
finally { creating.value = false }
}
// edit
const showEdit = ref(false); const updating = ref(false); const editFormRef = ref(null)
const editForm = ref({ id: 0, phone: '', name: '', province: '', region: '', cascaderRegion: [] })
const editRules = { name: [{ required: true, message: '必填', trigger: 'blur' }], phone: [{ required: true, message: '必填', trigger: 'blur' }], region: [{ required: true, message: '请选择省/市', trigger: 'change' }] }
function onEditCascaderChange(val) {
if (val && val.length >= 2) {
editForm.value.province = val[0]
editForm.value.region = val[1]
} else {
editForm.value.province = ''
editForm.value.region = ''
}
}
function handleEdit(row) {
lazyLoadCascades()
editForm.value = {
id: row.id,
phone: row.phone || '',
name: row.name,
province: row.province || '',
region: row.region,
cascaderRegion: row.province && row.region ? [row.province, row.region] : []
}
showEdit.value = true
}
async function handleUpdate() {
const v = await editFormRef.value.validate().catch(() => false)
if (!v) return
updating.value = true
try {
const payload = { ...editForm.value }
delete payload.cascaderRegion
await updateAgent(payload)
ElMessage.success('保存成功'); showEdit.value = false; await fetchData()
}
finally { updating.value = false }
}
// renew
const showRenew = ref(false)
const renewAgentInfo = ref(null)
function handleRenew(row) {
renewAgentInfo.value = {
id: row.id,
name: row.name,
expiredAt: row.expired_at || '',
province: row.province || '',
region: row.region || ''
}
showRenew.value = true
}
function onRenewSuccess() {
fetchData()
}
// renewal records
const showRenewals = ref(false)
const renewalList = ref([])
const renewalLoading = ref(false)
async function handleViewRenewals(row) {
renewalLoading.value = true
showRenewals.value = true
renewalList.value = []
try {
const r = await listAgentRenewals(row.id)
renewalList.value = r.list || []
} finally {
renewalLoading.value = false
}
}
onMounted(fetchData)</script>
<style scoped>
.page { padding: 24px 28px; display: flex; flex-direction: column; flex: 1; min-height: 0; overflow: hidden; }
.page-body { background: #fff; border-radius: 6px; padding: 16px; border: 1px solid #e5e5e5; display: flex; flex-direction: column; flex: 1; min-height: 0; }
.table-wrapper { flex: 1; min-height: 0; overflow: hidden; }
.btn-primary {
display: inline-flex; align-items: center; gap: 5px; height: 34px; padding: 0 14px;
border-radius: 4px; border: none; background: #000; color: #fff; font-size: 12px; font-weight: 500; cursor: pointer;
}
.btn-primary:hover { background: #333; }
.search-bar { display: flex; gap: 8px; flex-shrink: 0; margin-bottom: 12px; }
.search-spacer { flex: 1; }
.pagination-wrap {
display: flex;
justify-content: center;
padding: 12px 0;
background: #fff;
border-top: 1px solid #e5e5e5;
flex-shrink: 0;
margin: 0 -16px;
padding-left: 16px;
padding-right: 16px;
}
</style>