This commit is contained in:
zlwu committed 2026-06-14 17:20:52 +08:00
1 parent a79aee2614
commit 427bcb1551
274 files changed
+59416 -38342

No files matched your search

+365 -90
View File
@@ -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>
+407 -136
View File
@@ -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>
+31 -4
View File
@@ -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>