315 lines
9.1 KiB
Vue
315 lines
9.1 KiB
Vue
<script setup lang="ts">
|
|
import { onMounted, reactive, ref } from 'vue'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { Plus, Edit, Delete, Refresh, Search } from '@element-plus/icons-vue'
|
|
import type {
|
|
Position,
|
|
CreatePositionRequest,
|
|
UpdatePositionRequest,
|
|
PositionFilter,
|
|
Country,
|
|
CategoryTree,
|
|
} from '@/types'
|
|
import { positionsApi } from '@/api/positions'
|
|
import { countriesApi } from '@/api/countries'
|
|
import { categoriesApi } from '@/api/categories'
|
|
|
|
const loading = ref(false)
|
|
const list = ref<Position[]>([])
|
|
const total = ref(0)
|
|
|
|
const countries = ref<Country[]>([])
|
|
const categoriesTree = ref<CategoryTree[]>([])
|
|
|
|
const filter = reactive<Required<PositionFilter>>({
|
|
countryId: '',
|
|
categoryId: '',
|
|
page: 1,
|
|
pageSize: 10,
|
|
})
|
|
|
|
async function loadLookups() {
|
|
const [c, ct] = await Promise.all([
|
|
countriesApi.getCountriesList({ page: 1, pageSize: 500 }),
|
|
categoriesApi.getCategoryTree(),
|
|
])
|
|
countries.value = c.items
|
|
categoriesTree.value = ct
|
|
}
|
|
|
|
async function fetchList() {
|
|
loading.value = true
|
|
try {
|
|
await positionsApi.getPositionsList(filter)
|
|
const data = await positionsApi.getPositionsList(filter) as any
|
|
const arr = Array.isArray(data) ? data : (data.items ?? [])
|
|
list.value = arr
|
|
total.value = Array.isArray(data) ? data.length : (data.total ?? 0)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function handleSearch() {
|
|
filter.page = 1
|
|
fetchList()
|
|
}
|
|
|
|
function handleReset() {
|
|
filter.countryId = ''
|
|
filter.categoryId = ''
|
|
filter.page = 1
|
|
fetchList()
|
|
}
|
|
|
|
// ---------- Cascader ----------
|
|
interface CascaderNode {
|
|
value: string
|
|
label: string
|
|
children?: CascaderNode[]
|
|
}
|
|
|
|
const cascaderOptions = ref<CascaderNode[]>([])
|
|
|
|
function buildCascader(nodes: CategoryTree[]): CascaderNode[] {
|
|
return nodes.map((n) => ({
|
|
value: n.id,
|
|
label: n.categoryName,
|
|
children: n.children?.length ? buildCascader(n.children) : undefined,
|
|
}))
|
|
}
|
|
|
|
// ---------- Dialog ----------
|
|
const dialogRef = ref()
|
|
const dialogVisible = ref(false)
|
|
const dialogMode = ref<'create' | 'edit'>('create')
|
|
const dialogLoading = ref(false)
|
|
|
|
const dialogForm = reactive<CreatePositionRequest & { id?: string }>({
|
|
indexVal: 0,
|
|
})
|
|
|
|
const dialogRules = {
|
|
indexVal: [{ required: true, message: '排序值是必填项', trigger: 'blur' }],
|
|
}
|
|
|
|
function rebuildCascader() {
|
|
cascaderOptions.value = buildCascader(categoriesTree.value)
|
|
}
|
|
|
|
function openAddDialog() {
|
|
dialogMode.value = 'create'
|
|
Object.assign(dialogForm, { id: undefined, indexVal: 0, countryId: '', categoryId: '' })
|
|
rebuildCascader()
|
|
dialogVisible.value = true
|
|
}
|
|
|
|
function openEditDialog(p: Position) {
|
|
dialogMode.value = 'edit'
|
|
Object.assign(dialogForm, {
|
|
id: p.id,
|
|
indexVal: p.indexVal,
|
|
countryId: p.countryId || '',
|
|
categoryId: p.categoryId || '',
|
|
})
|
|
rebuildCascader()
|
|
dialogVisible.value = true
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
if (!dialogRef.value) return
|
|
await dialogRef.value.validate(async (valid: boolean) => {
|
|
if (!valid) return
|
|
dialogLoading.value = true
|
|
try {
|
|
const payload: CreatePositionRequest = {
|
|
indexVal: Number(dialogForm.indexVal),
|
|
countryId: dialogForm.countryId || undefined,
|
|
categoryId: dialogForm.categoryId || undefined,
|
|
}
|
|
if (dialogMode.value === 'create') {
|
|
await positionsApi.createPosition(payload)
|
|
ElMessage.success('位置创建成功')
|
|
} else {
|
|
await positionsApi.updatePosition(dialogForm.id!, payload as UpdatePositionRequest)
|
|
ElMessage.success('位置更新成功')
|
|
}
|
|
dialogVisible.value = false
|
|
fetchList()
|
|
} finally {
|
|
dialogLoading.value = false
|
|
}
|
|
})
|
|
}
|
|
|
|
async function handleDelete(p: Position) {
|
|
try {
|
|
await ElMessageBox.confirm(`确定删除位置 #${p.indexVal} 吗?`, '确认', {
|
|
type: 'warning',
|
|
confirmButtonText: '删除',
|
|
cancelButtonText: '取消',
|
|
})
|
|
} catch {
|
|
return
|
|
}
|
|
await positionsApi.deletePosition(p.id)
|
|
ElMessage.success('删除成功')
|
|
fetchList()
|
|
}
|
|
|
|
function getCategoryName(p: Position): string {
|
|
if (!p.category) return p.categoryId || '-'
|
|
return p.category.parent ? `${p.category.parent.categoryName} / ${p.category.categoryName}` : p.category.categoryName
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await loadLookups()
|
|
await fetchList()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="page-container">
|
|
<div class="page-card">
|
|
<div class="filter-bar">
|
|
<el-select
|
|
v-model="filter.countryId"
|
|
placeholder="请选择国家"
|
|
clearable
|
|
@change="handleSearch"
|
|
>
|
|
<el-option
|
|
v-for="c in countries"
|
|
:key="c.id"
|
|
:label="c.countryName"
|
|
:value="c.id"
|
|
/>
|
|
</el-select>
|
|
|
|
<el-cascader
|
|
v-model="filter.categoryId"
|
|
:options="cascaderOptions"
|
|
:props="{ checkStrictly: true, value: 'value', label: 'label', children: 'children', emitPath: false }"
|
|
placeholder="请选择分类"
|
|
clearable
|
|
@change="handleSearch"
|
|
/>
|
|
|
|
<el-button type="primary" @click="handleSearch">
|
|
<el-icon><Search /></el-icon>
|
|
<span>搜索</span>
|
|
</el-button>
|
|
<el-button @click="handleReset">
|
|
<el-icon><Refresh /></el-icon>
|
|
<span>重置</span>
|
|
</el-button>
|
|
|
|
<div class="filter-spacer" />
|
|
|
|
<el-button type="primary" @click="openAddDialog">
|
|
<el-icon><Plus /></el-icon>
|
|
<span>新增位置</span>
|
|
</el-button>
|
|
</div>
|
|
|
|
<el-table v-loading="loading" :data="list" border stripe>
|
|
<el-table-column label="排序值" prop="indexVal" width="100" sortable />
|
|
<el-table-column label="国家" min-width="160">
|
|
<template #default="{ row }: any">
|
|
{{ row.country?.countryName || '-' }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="分类" min-width="200">
|
|
<template #default="{ row }: any">
|
|
{{ getCategoryName(row) }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="180" fixed="right">
|
|
<template #default="{ row }: any">
|
|
<div class="table-actions">
|
|
<el-button size="small" type="primary" plain @click="openEditDialog(row)">
|
|
<el-icon><Edit /></el-icon>
|
|
<span>编辑</span>
|
|
</el-button>
|
|
<el-button size="small" type="danger" plain @click="handleDelete(row)">
|
|
<el-icon><Delete /></el-icon>
|
|
<span>删除</span>
|
|
</el-button>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<template #empty>
|
|
<el-empty description="暂无位置" />
|
|
</template>
|
|
</el-table>
|
|
|
|
<el-pagination
|
|
class="pagination"
|
|
v-model:current-page="filter.page"
|
|
v-model:page-size="filter.pageSize"
|
|
:total="total"
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
@current-change="(p: number) => { filter.page = p; fetchList() }"
|
|
@size-change="(s: number) => { filter.pageSize = s; filter.page = 1; fetchList() }"
|
|
/>
|
|
</div>
|
|
|
|
<el-dialog
|
|
v-model="dialogVisible"
|
|
:title="dialogMode === 'create' ? '新增位置' : '编辑位置'"
|
|
width="520px"
|
|
destroy-on-close
|
|
>
|
|
<el-form ref="dialogRef" :model="dialogForm" :rules="dialogRules" label-width="100px">
|
|
<el-form-item label="排序值" prop="indexVal">
|
|
<el-input-number v-model="dialogForm.indexVal" :min="0" :max="9999" />
|
|
</el-form-item>
|
|
<el-form-item label="国家">
|
|
<el-select v-model="dialogForm.countryId" placeholder="可选" clearable style="width: 100%">
|
|
<el-option
|
|
v-for="c in countries"
|
|
:key="c.id"
|
|
:label="c.countryName"
|
|
:value="c.id"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="分类">
|
|
<el-cascader
|
|
v-model="dialogForm.categoryId"
|
|
:options="cascaderOptions"
|
|
:props="{
|
|
checkStrictly: true,
|
|
value: 'value',
|
|
label: 'label',
|
|
children: 'children',
|
|
emitPath: false,
|
|
}"
|
|
placeholder="可选"
|
|
clearable
|
|
style="width: 100%"
|
|
/>
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="dialogVisible = false">取消</el-button>
|
|
<el-button type="primary" :loading="dialogLoading" @click="handleSubmit">
|
|
{{ dialogMode === 'create' ? '创建' : '保存' }}
|
|
</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.filter-spacer {
|
|
flex: 1;
|
|
}
|
|
|
|
.pagination {
|
|
margin-top: 16px;
|
|
justify-content: flex-end;
|
|
}
|
|
</style>
|