提交
This commit is contained in:
1 parent
a79aee2614
commit
427bcb1551
274 files changed
+59416
-38342
No files matched your search
@@ -49,87 +49,64 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="expressCode"
|
||||
label="快递编码"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="expressName"
|
||||
label="快递名称"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="120"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
<div v-loading="loading" class="express-card-grid">
|
||||
<div
|
||||
v-for="row in tableData"
|
||||
:key="row.id"
|
||||
class="express-card"
|
||||
:class="{ 'is-selected': isRowSelected(row.id) }"
|
||||
@dblclick="handleView(row)"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px">
|
||||
<el-button class="btn-op-item" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-dropdown trigger="hover" @command="(cmd: string) => handleDropdownCommand(cmd, row)">
|
||||
<el-button class="btn-op-item">
|
||||
<el-icon><More /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<div class="express-card__select" @click.stop>
|
||||
<el-checkbox
|
||||
:model-value="isRowSelected(row.id)"
|
||||
@change="(val) => toggleRowSelection(row, !!val)"
|
||||
/>
|
||||
</div>
|
||||
<div class="express-card__media">
|
||||
<el-image
|
||||
v-if="row.expressLogo"
|
||||
:src="expressLogoUrl(row.expressLogo)"
|
||||
fit="cover"
|
||||
class="express-card__logo"
|
||||
/>
|
||||
<div v-else class="express-card__logo-placeholder">
|
||||
<el-icon><Van /></el-icon>
|
||||
<span>暂无Logo</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
|
||||
@current-change="(val) => ((pagination.page = val), loadData())"
|
||||
/>
|
||||
</div>
|
||||
<div class="express-card__body">
|
||||
<div class="express-card__title" :title="row.expressName">{{ row.expressName }}</div>
|
||||
<div class="express-card__code">{{ row.expressCode }}</div>
|
||||
<div v-if="row.createTime" class="express-card__meta">{{ row.createTime }}</div>
|
||||
</div>
|
||||
<div class="express-card__footer">
|
||||
<el-button class="btn-op-item" @click.stop="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-dropdown trigger="hover" @command="(cmd) => handleDropdownCommand(cmd, row)">
|
||||
<el-button class="btn-op-item" @click.stop>
|
||||
<el-icon><More /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty
|
||||
v-if="!loading && !tableData.length"
|
||||
description="暂无快递公司数据"
|
||||
:image-size="80"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
@@ -157,6 +134,37 @@
|
||||
<el-form-item label="快递名称" prop="expressName">
|
||||
<el-input v-model="formData.expressName" placeholder="请输入快递名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="快递Logo" style="grid-column: 1 / -1">
|
||||
<div class="express-logo-row">
|
||||
<el-image
|
||||
v-if="formData.expressLogo"
|
||||
:src="expressLogoUrl(formData.expressLogo)"
|
||||
fit="cover"
|
||||
class="express-logo-preview"
|
||||
/>
|
||||
<div v-else class="express-logo-placeholder">暂无</div>
|
||||
<el-upload
|
||||
ref="expressLogoUploadRef"
|
||||
accept="image/*"
|
||||
:show-file-list="false"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
:on-change="handleExpressLogoChange"
|
||||
:on-exceed="handleExpressLogoExceed"
|
||||
>
|
||||
<el-button type="primary" plain>
|
||||
<el-icon><Upload /></el-icon>上传Logo
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<el-button
|
||||
v-if="formData.expressLogo"
|
||||
class="btn-ghost"
|
||||
@click="formData.expressLogo = ''"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>移除
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="formData.remark" placeholder="可选" />
|
||||
</el-form-item>
|
||||
@@ -180,7 +188,23 @@
|
||||
:icon="Van"
|
||||
:meta="viewRow?.createTime || ''"
|
||||
:sections="viewSections"
|
||||
/>
|
||||
>
|
||||
<template v-if="viewRow?.expressLogo" #express-logo>
|
||||
<dl class="express-view-kv-grid">
|
||||
<div class="express-view-kv">
|
||||
<dt>Logo</dt>
|
||||
<dd>
|
||||
<el-image
|
||||
:src="expressLogoUrl(viewRow.expressLogo)"
|
||||
fit="cover"
|
||||
class="express-view-logo"
|
||||
:preview-src-list="[expressLogoUrl(viewRow.expressLogo)]"
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -188,8 +212,22 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Close, Check, View, Van, RefreshLeft, Search, More, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
Plus,
|
||||
Close,
|
||||
Check,
|
||||
View,
|
||||
Van,
|
||||
RefreshLeft,
|
||||
Search,
|
||||
More,
|
||||
Edit,
|
||||
Delete,
|
||||
Upload
|
||||
} from '@element-plus/icons-vue'
|
||||
import type { UploadFile } from 'element-plus'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
import {
|
||||
fetchExpressCompanyList,
|
||||
@@ -200,11 +238,48 @@
|
||||
|
||||
defineOptions({ name: 'SystemExpress' })
|
||||
|
||||
const expressLogoUploadRef = ref()
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
function expressLogoUrl(url: string) {
|
||||
if (!url) return ''
|
||||
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/')) return url
|
||||
return `${minioBaseUrl}/${url}`
|
||||
}
|
||||
|
||||
async function uploadExpressLogoFile(file: File) {
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('folder', 'express-company')
|
||||
fd.append('file', file)
|
||||
const res = await fetchUploadAttachment(fd)
|
||||
formData.expressLogo = res.fileUrl || ''
|
||||
ElMessage.success('Logo上传成功')
|
||||
} catch (e) {
|
||||
console.error('快递Logo上传失败:', e)
|
||||
ElMessage.error('Logo上传失败')
|
||||
} finally {
|
||||
expressLogoUploadRef.value?.clearFiles()
|
||||
}
|
||||
}
|
||||
|
||||
const handleExpressLogoChange = async (uploadFile: UploadFile) => {
|
||||
const file: any = (uploadFile as any).raw
|
||||
if (!file) return
|
||||
const status = (uploadFile as any).status
|
||||
if (status && status !== 'ready') return
|
||||
await uploadExpressLogoFile(file)
|
||||
}
|
||||
|
||||
const handleExpressLogoExceed = (files: File[]) => {
|
||||
const file = files[0]
|
||||
if (file) uploadExpressLogoFile(file)
|
||||
}
|
||||
|
||||
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
|
||||
const tableData = ref<Api.ExpressCompany.ExpressCompanyItem[]>([])
|
||||
const selectedRows = ref<Api.ExpressCompany.ExpressCompanyItem[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增快递公司')
|
||||
const isEdit = ref(false)
|
||||
@@ -217,6 +292,7 @@
|
||||
const formData = reactive({
|
||||
expressCode: '',
|
||||
expressName: '',
|
||||
expressLogo: '',
|
||||
orderNum: 0,
|
||||
remark: ''
|
||||
})
|
||||
@@ -226,8 +302,14 @@
|
||||
expressName: [{ required: true, message: '请输入快递名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const handleSelectionChange = (rows: Api.ExpressCompany.ExpressCompanyItem[]) => {
|
||||
selectedRows.value = rows
|
||||
const isRowSelected = (id: string) => selectedRows.value.some((r) => r.id === id)
|
||||
|
||||
const toggleRowSelection = (row: Api.ExpressCompany.ExpressCompanyItem, checked: boolean) => {
|
||||
if (checked) {
|
||||
if (!isRowSelected(row.id)) selectedRows.value = [...selectedRows.value, row]
|
||||
return
|
||||
}
|
||||
selectedRows.value = selectedRows.value.filter((r) => r.id !== row.id)
|
||||
}
|
||||
|
||||
const viewSections = computed(() => {
|
||||
@@ -241,7 +323,17 @@
|
||||
{ label: '快递名称', value: row.expressName },
|
||||
{ label: '备注', value: row.remark, span: true }
|
||||
]
|
||||
}
|
||||
},
|
||||
row.expressLogo
|
||||
? {
|
||||
title: '快递Logo',
|
||||
fields: [],
|
||||
slot: 'express-logo'
|
||||
}
|
||||
: {
|
||||
title: '快递Logo',
|
||||
fields: [{ label: 'Logo', value: '无' }]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@@ -249,31 +341,31 @@
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchExpressCompanyList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: searchForm.keyword || undefined
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
const validIds = new Set(tableData.value.map((r) => r.id))
|
||||
selectedRows.value = selectedRows.value.filter((r) => validIds.has(r.id))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.expressCode = ''
|
||||
formData.expressName = ''
|
||||
formData.expressLogo = ''
|
||||
formData.orderNum = 0
|
||||
formData.remark = ''
|
||||
editingId.value = null
|
||||
@@ -293,6 +385,7 @@
|
||||
editingId.value = row.id
|
||||
formData.expressCode = row.expressCode || ''
|
||||
formData.expressName = row.expressName || ''
|
||||
formData.expressLogo = row.expressLogo || ''
|
||||
formData.orderNum = row.orderNum ?? 0
|
||||
formData.remark = row.remark || ''
|
||||
dialogVisible.value = true
|
||||
@@ -306,6 +399,7 @@
|
||||
const payload = {
|
||||
expressCode: formData.expressCode.trim(),
|
||||
expressName: formData.expressName.trim(),
|
||||
expressLogo: formData.expressLogo?.trim() ?? '',
|
||||
orderNum: formData.orderNum,
|
||||
remark: formData.remark?.trim() || undefined
|
||||
}
|
||||
@@ -368,7 +462,6 @@
|
||||
watchDebounced(
|
||||
searchForm,
|
||||
() => {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
},
|
||||
{ debounce: 300, deep: true }
|
||||
@@ -385,4 +478,186 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.express-logo-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.express-logo-preview {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--mint-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.express-logo-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed var(--mint-border, #e2e8f0);
|
||||
color: var(--mint-text-muted, #94a3b8);
|
||||
font-size: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.express-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
min-height: 120px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.express-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border: 1px solid var(--mint-border, #e2e8f0);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #a8d4c4;
|
||||
box-shadow: 0 2px 12px rgba(4, 120, 87, 0.1);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
border-color: var(--mint-primary, #10b981);
|
||||
box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
.express-card__select {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.express-card__media {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 136px;
|
||||
padding: 16px 16px 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.express-card__logo {
|
||||
display: block;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--mint-border, #e2e8f0);
|
||||
background: #fff;
|
||||
|
||||
:deep(.el-image__inner),
|
||||
:deep(img) {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.express-card__logo-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border-radius: 10px;
|
||||
border: 1px dashed var(--mint-border, #e2e8f0);
|
||||
color: var(--mint-text-muted, #94a3b8);
|
||||
font-size: 12px;
|
||||
background: #fff;
|
||||
|
||||
.el-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.express-card__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
padding: 10px 16px 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.express-card__title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text, #0f172a);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.express-card__code {
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.express-card__meta {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #94a3b8);
|
||||
}
|
||||
|
||||
.express-card__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 10px 16px 14px;
|
||||
border-top: 1px solid var(--mint-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.express-view-kv-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 20px;
|
||||
}
|
||||
|
||||
.express-view-kv {
|
||||
display: grid;
|
||||
grid-template-columns: 96px 1fr;
|
||||
gap: 8px 12px;
|
||||
font-size: 13px;
|
||||
align-items: start;
|
||||
|
||||
dt {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.express-view-logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -49,133 +49,81 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="siteName"
|
||||
label="站点名称"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="siteCode"
|
||||
label="站点编码"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="siteType"
|
||||
label="站点类型"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
<div v-loading="loading" class="site-card-grid">
|
||||
<div
|
||||
v-for="row in tableData"
|
||||
:key="row.id"
|
||||
class="site-card"
|
||||
:class="{ 'is-selected': isRowSelected(row.id) }"
|
||||
@dblclick="handleView(row)"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ getSiteTypeLabel(row.siteType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="status"
|
||||
label="状态"
|
||||
min-width="80"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">
|
||||
{{ row.status === '1' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="address"
|
||||
label="地址"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="contactName"
|
||||
label="联系人"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="contactPhone"
|
||||
label="联系电话"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="120"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px">
|
||||
<el-button class="btn-op-item" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-dropdown trigger="hover" @command="(cmd: string) => handleDropdownCommand(cmd, row)">
|
||||
<el-button class="btn-op-item">
|
||||
<el-icon><More /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<div class="site-card__select" @click.stop>
|
||||
<el-checkbox
|
||||
:model-value="isRowSelected(row.id)"
|
||||
@change="(val) => toggleRowSelection(row, !!val)"
|
||||
/>
|
||||
</div>
|
||||
<div class="site-card__media">
|
||||
<el-image
|
||||
v-if="row.siteImage"
|
||||
:src="siteImageUrl(row.siteImage)"
|
||||
fit="cover"
|
||||
class="site-card__image"
|
||||
/>
|
||||
<div v-else class="site-card__image-placeholder">
|
||||
<el-icon><Picture /></el-icon>
|
||||
<span>暂无图片</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
|
||||
@current-change="(val) => ((pagination.page = val), loadData())"
|
||||
/>
|
||||
</div>
|
||||
<div class="site-card__body">
|
||||
<div class="site-card__title" :title="row.siteName">{{ row.siteName }}</div>
|
||||
<div class="site-card__code">{{ row.siteCode }}</div>
|
||||
<div class="site-card__tags">
|
||||
<el-tag v-if="row.siteType" type="primary" size="small">
|
||||
{{ getSiteTypeLabel(row.siteType) }}
|
||||
</el-tag>
|
||||
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">
|
||||
{{ row.status === '1' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="site-card__meta">
|
||||
<div v-if="row.address" class="site-card__meta-item" :title="row.address">
|
||||
<el-icon><Location /></el-icon>
|
||||
<span>{{ row.address }}</span>
|
||||
</div>
|
||||
<div v-if="row.contactName || row.contactPhone" class="site-card__meta-item">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>{{ [row.contactName, row.contactPhone].filter(Boolean).join(' · ') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-card__footer">
|
||||
<el-button class="btn-op-item" @click.stop="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-dropdown trigger="hover" @command="(cmd) => handleDropdownCommand(cmd, row)">
|
||||
<el-button class="btn-op-item" @click.stop>
|
||||
<el-icon><More /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty
|
||||
v-if="!loading && !tableData.length"
|
||||
description="暂无站点数据"
|
||||
:image-size="80"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
@@ -227,6 +175,37 @@
|
||||
<el-form-item label="地址" style="grid-column: 1 / -1">
|
||||
<el-input v-model="formData.address" placeholder="请输入地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="站点图片" style="grid-column: 1 / -1">
|
||||
<div class="site-image-row">
|
||||
<el-image
|
||||
v-if="formData.siteImage"
|
||||
:src="siteImageUrl(formData.siteImage)"
|
||||
fit="cover"
|
||||
class="site-image-preview"
|
||||
/>
|
||||
<div v-else class="site-image-placeholder">暂无</div>
|
||||
<el-upload
|
||||
ref="siteImageUploadRef"
|
||||
accept="image/*"
|
||||
:show-file-list="false"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
:on-change="handleSiteImageChange"
|
||||
:on-exceed="handleSiteImageExceed"
|
||||
>
|
||||
<el-button type="primary" plain>
|
||||
<el-icon><Upload /></el-icon>上传图片
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<el-button
|
||||
v-if="formData.siteImage"
|
||||
class="btn-ghost"
|
||||
@click="formData.siteImage = ''"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>移除
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系人">
|
||||
<el-select
|
||||
v-model="contactUserId"
|
||||
@@ -292,7 +271,23 @@
|
||||
:icon="Location"
|
||||
:meta="`站点编码:${viewRow?.siteCode || ''}${viewRow?.createTime ? ' · ' + viewRow.createTime : ''}`"
|
||||
:sections="viewSections"
|
||||
/>
|
||||
>
|
||||
<template v-if="viewRow?.siteImage" #site-image>
|
||||
<dl class="site-view-kv-grid">
|
||||
<div class="site-view-kv">
|
||||
<dt>图片</dt>
|
||||
<dd>
|
||||
<el-image
|
||||
:src="siteImageUrl(viewRow.siteImage)"
|
||||
fit="cover"
|
||||
class="site-view-image"
|
||||
:preview-src-list="[siteImageUrl(viewRow.siteImage)]"
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -300,9 +295,25 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Close, Check, View, Edit, Delete, More, Location, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
Plus,
|
||||
Close,
|
||||
Check,
|
||||
View,
|
||||
Edit,
|
||||
Delete,
|
||||
More,
|
||||
Location,
|
||||
RefreshLeft,
|
||||
Search,
|
||||
Picture,
|
||||
User,
|
||||
Upload
|
||||
} from '@element-plus/icons-vue'
|
||||
import type { UploadFile } from 'element-plus'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import {
|
||||
fetchSiteList,
|
||||
fetchCreateSite,
|
||||
@@ -314,6 +325,44 @@
|
||||
|
||||
defineOptions({ name: 'SystemSite' })
|
||||
|
||||
const siteImageUploadRef = ref()
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
function siteImageUrl(url: string) {
|
||||
if (!url) return ''
|
||||
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/')) return url
|
||||
return `${minioBaseUrl}/${url}`
|
||||
}
|
||||
|
||||
async function uploadSiteImageFile(file: File) {
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('folder', 'site')
|
||||
fd.append('file', file)
|
||||
const res = await fetchUploadAttachment(fd)
|
||||
formData.siteImage = res.fileUrl || ''
|
||||
ElMessage.success('图片上传成功')
|
||||
} catch (e) {
|
||||
console.error('站点图片上传失败:', e)
|
||||
ElMessage.error('图片上传失败')
|
||||
} finally {
|
||||
siteImageUploadRef.value?.clearFiles()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSiteImageChange = async (uploadFile: UploadFile) => {
|
||||
const file: any = (uploadFile as any).raw
|
||||
if (!file) return
|
||||
const status = (uploadFile as any).status
|
||||
if (status && status !== 'ready') return
|
||||
await uploadSiteImageFile(file)
|
||||
}
|
||||
|
||||
const handleSiteImageExceed = (files: File[]) => {
|
||||
const file = files[0]
|
||||
if (file) uploadSiteImageFile(file)
|
||||
}
|
||||
|
||||
// ---- 查看弹窗 ----
|
||||
const viewVisible = ref(false)
|
||||
const viewRow = ref<Api.Site.SiteItem | null>(null)
|
||||
@@ -356,7 +405,17 @@
|
||||
{ label: '备注', value: row.remark, span: true },
|
||||
{ label: '创建时间', value: row.createTime }
|
||||
]
|
||||
}
|
||||
},
|
||||
row.siteImage
|
||||
? {
|
||||
title: '站点图片',
|
||||
fields: [],
|
||||
slot: 'site-image'
|
||||
}
|
||||
: {
|
||||
title: '站点图片',
|
||||
fields: [{ label: '图片', value: '无' }]
|
||||
}
|
||||
]
|
||||
})
|
||||
function handleView(row: Api.Site.SiteItem) {
|
||||
@@ -368,7 +427,6 @@
|
||||
const tableData = ref<Api.Site.SiteItem[]>([])
|
||||
const selectedRows = ref<Api.Site.SiteItem[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增站点')
|
||||
const isEdit = ref(false)
|
||||
@@ -385,6 +443,7 @@
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
businessScopeList: [] as string[],
|
||||
siteImage: '',
|
||||
orderNum: 0,
|
||||
remark: ''
|
||||
})
|
||||
@@ -520,33 +579,38 @@
|
||||
return opt ? opt.label : code
|
||||
}
|
||||
|
||||
const handleSelectionChange = (rows: Api.Site.SiteItem[]) => {
|
||||
selectedRows.value = rows
|
||||
const isRowSelected = (id: string) => selectedRows.value.some((r) => r.id === id)
|
||||
|
||||
const toggleRowSelection = (row: Api.Site.SiteItem, checked: boolean) => {
|
||||
if (checked) {
|
||||
if (!isRowSelected(row.id)) selectedRows.value = [...selectedRows.value, row]
|
||||
return
|
||||
}
|
||||
selectedRows.value = selectedRows.value.filter((r) => r.id !== row.id)
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchSiteList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: searchForm.keyword || undefined
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
const validIds = new Set(tableData.value.map((r) => r.id))
|
||||
selectedRows.value = selectedRows.value.filter((r) => validIds.has(r.id))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
@@ -559,6 +623,7 @@
|
||||
formData.contactName = ''
|
||||
formData.contactPhone = ''
|
||||
formData.businessScopeList = []
|
||||
formData.siteImage = ''
|
||||
formData.orderNum = 0
|
||||
formData.remark = ''
|
||||
editingId.value = null
|
||||
@@ -592,6 +657,7 @@
|
||||
formData.contactName = row.contactName || ''
|
||||
formData.contactPhone = row.contactPhone || ''
|
||||
formData.businessScopeList = row.businessScope ? row.businessScope.split(',') : []
|
||||
formData.siteImage = row.siteImage || ''
|
||||
formData.orderNum = row.orderNum ?? 0
|
||||
formData.remark = row.remark || ''
|
||||
dialogVisible.value = true
|
||||
@@ -616,6 +682,7 @@
|
||||
businessScope: formData.businessScopeList.length
|
||||
? formData.businessScopeList.join(',')
|
||||
: undefined,
|
||||
siteImage: formData.siteImage?.trim() ?? '',
|
||||
orderNum: formData.orderNum,
|
||||
remark: formData.remark?.trim() || undefined
|
||||
}
|
||||
@@ -671,7 +738,6 @@
|
||||
watchDebounced(
|
||||
searchForm,
|
||||
() => {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
},
|
||||
{ debounce: 300, deep: true }
|
||||
@@ -689,4 +755,209 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.site-image-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.site-image-preview {
|
||||
width: 120px;
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--mint-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.site-image-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 120px;
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed var(--mint-border, #e2e8f0);
|
||||
color: var(--mint-text-muted, #94a3b8);
|
||||
font-size: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.site-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
min-height: 120px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.site-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border: 1px solid var(--mint-border, #e2e8f0);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #a8d4c4;
|
||||
box-shadow: 0 2px 12px rgba(4, 120, 87, 0.1);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
border-color: var(--mint-primary, #10b981);
|
||||
box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
.site-card__select {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.site-card__media {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
flex-shrink: 0;
|
||||
background: #f1f5f9;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.site-card__image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
|
||||
:deep(.el-image__inner),
|
||||
:deep(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.site-card__image-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
color: var(--mint-text-muted, #94a3b8);
|
||||
font-size: 12px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.site-card__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
padding: 14px 16px 10px;
|
||||
}
|
||||
|
||||
.site-card__title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text, #0f172a);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.site-card__code {
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.site-card__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.site-card__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-top: auto;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.site-card__meta-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
line-height: 1.45;
|
||||
|
||||
.el-icon {
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
.site-card__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 10px 16px 14px;
|
||||
border-top: 1px solid var(--mint-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.site-view-kv-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 20px;
|
||||
}
|
||||
|
||||
.site-view-kv {
|
||||
display: grid;
|
||||
grid-template-columns: 96px 1fr;
|
||||
gap: 8px 12px;
|
||||
font-size: 13px;
|
||||
align-items: start;
|
||||
|
||||
dt {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.site-view-image {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
height: 200px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -120,10 +120,17 @@
|
||||
<el-table-column
|
||||
prop="nickName"
|
||||
label="用户名"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
min-width="140"
|
||||
align="left"
|
||||
header-align="left"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="user-nick-cell">
|
||||
<el-avatar :size="24" :src="resolveUserAvatarFromInfo(row)" />
|
||||
<span class="user-nick-cell__name">{{ row.nickName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="deptName"
|
||||
label="所属组织"
|
||||
@@ -342,6 +349,7 @@
|
||||
fetchRoleOptions
|
||||
} from '@/api/admin'
|
||||
import { buildPathLabelMapFromTreeRoots } from '@/utils/tree-path'
|
||||
import { resolveUserAvatarFromInfo } from '@/utils/user-avatar'
|
||||
|
||||
const roleOptions = ref<Array<{ label: string; value: string }>>([])
|
||||
|
||||
@@ -730,4 +738,23 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.user-nick-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
max-width: 100%;
|
||||
|
||||
:deep(.el-avatar) {
|
||||
flex-shrink: 0;
|
||||
--el-avatar-bg-color: transparent;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&__name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user