You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
322 lines
13 KiB
322 lines
13 KiB
<template>
|
|
<div>
|
|
<!-- 列表视图 -->
|
|
<div v-show="view === 'list'">
|
|
<div class="flex items-center justify-between mb-5">
|
|
<h2 class="text-lg font-semibold text-slate-800">工厂列表</h2>
|
|
<button class="btn btn-primary btn-sm text-white" @click="showForm()">+ 新增工厂</button>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-4 mb-4">
|
|
<div class="flex flex-wrap items-end gap-3">
|
|
<div class="form-control">
|
|
<label class="label py-1"><span class="label-text text-xs text-slate-500">状态</span></label>
|
|
<select v-model.number="filterEnabled" class="select select-bordered select-sm w-32">
|
|
<option :value="-1">全部</option>
|
|
<option :value="1">合作中</option>
|
|
<option :value="0">已停用</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-control flex-1 min-w-[220px]">
|
|
<label class="label py-1"><span class="label-text text-xs text-slate-500">关键字</span></label>
|
|
<input v-model="keyword" type="text" class="input input-bordered input-sm w-full" placeholder="工厂名称 / 联系人 / 地址 / 说明" />
|
|
</div>
|
|
<button class="btn btn-ghost btn-sm" @click="resetFilter">重置</button>
|
|
<span class="text-xs text-slate-400 ml-auto">共 {{ viewRows.length }} / {{ rows.length }} 条</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-xl shadow-sm border border-slate-100">
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-zebra">
|
|
<thead>
|
|
<tr>
|
|
<th class="cursor-pointer select-none" @click="toggleSort('id')">ID{{ sortMark('id') }}</th>
|
|
<th class="cursor-pointer select-none" @click="toggleSort('name')">工厂名称{{ sortMark('name') }}</th>
|
|
<th>联系人</th>
|
|
<th>联系电话</th>
|
|
<th>地址</th>
|
|
<th>说明</th>
|
|
<th class="cursor-pointer select-none" @click="toggleSort('sort')">排序{{ sortMark('sort') }}</th>
|
|
<th>状态</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-if="loading">
|
|
<td colspan="9" class="text-center py-8"><span class="loading loading-spinner loading-sm"></span></td>
|
|
</tr>
|
|
<tr v-else-if="!viewRows.length">
|
|
<td colspan="9" class="text-center text-slate-400 py-8">暂无数据</td>
|
|
</tr>
|
|
<tr v-for="f in viewRows" :key="f.id">
|
|
<td class="font-mono text-sm">{{ f.id }}</td>
|
|
<td class="font-medium">{{ f.name }}</td>
|
|
<td class="text-sm">{{ f.contact || '-' }}</td>
|
|
<td class="text-sm">{{ f.phone || '-' }}</td>
|
|
<td class="text-sm text-slate-500 max-w-[180px] truncate" :title="f.address">{{ f.address || '-' }}</td>
|
|
<td class="text-sm text-slate-500 max-w-xs truncate" :title="f.description">{{ f.description || '' }}</td>
|
|
<td class="text-sm text-slate-500">{{ f.sort || 0 }}</td>
|
|
<td>
|
|
<span class="badge badge-sm" :class="f.enabled === false ? 'badge-ghost' : 'badge-success badge-outline'">
|
|
{{ f.enabled === false ? '已停用' : '合作中' }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div class="flex gap-1">
|
|
<button class="btn btn-ghost btn-xs" @click="showForm(f)">编辑</button>
|
|
<button class="btn btn-ghost btn-xs text-red-500" @click="delFactory(f)">删除</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="text-xs text-slate-400 mt-3">
|
|
工厂是「这批货由哪家厂生产组装」,与
|
|
品牌商(这货挂谁的牌子卖)是两回事:
|
|
同一品牌可以换厂、同一家厂也可以给多个品牌代工,所以分开维护。
|
|
</p>
|
|
</div>
|
|
|
|
<!-- 表单视图 -->
|
|
<div v-show="view === 'form'">
|
|
<div class="flex items-center gap-3 mb-5">
|
|
<button class="btn btn-ghost btn-sm" @click="showList()">← 返回列表</button>
|
|
<h2 class="text-lg font-semibold text-slate-800">{{ isEditing ? '编辑工厂' : '新增工厂' }}</h2>
|
|
</div>
|
|
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-6 max-w-2xl">
|
|
<form @submit.prevent="saveFactory">
|
|
<div v-if="isEditing" class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">ID</span></label>
|
|
<input :value="form.id" type="text" class="input input-bordered w-full bg-slate-50" disabled />
|
|
</div>
|
|
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">工厂名称 *</span></label>
|
|
<input v-model="form.name" type="text" class="input input-bordered w-full" placeholder="如:东莞市XX电子有限公司" required />
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-medium">联系人</span></label>
|
|
<input v-model="form.contact" type="text" class="input input-bordered w-full" placeholder="如:张工" />
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-medium">联系电话</span></label>
|
|
<input v-model="form.phone" type="text" class="input input-bordered w-full" placeholder="如:13800000000" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">工厂地址</span></label>
|
|
<input v-model="form.address" type="text" class="input input-bordered w-full" placeholder="如:广东省东莞市XX镇XX路X号" />
|
|
</div>
|
|
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">说明</span></label>
|
|
<textarea v-model="form.description" class="textarea textarea-bordered w-full" rows="3" placeholder="产能、擅长品类、合作情况等"></textarea>
|
|
</div>
|
|
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">联系邮箱</span></label>
|
|
<div class="flex gap-2 mb-2">
|
|
<input v-model="newEmail" type="email" class="input input-bordered input-sm flex-1" placeholder="输入邮箱后点添加" @keydown.enter.prevent="addEmail" />
|
|
<button type="button" class="btn btn-sm" @click="addEmail">添加</button>
|
|
</div>
|
|
<div v-if="emails.length" class="flex flex-wrap gap-2">
|
|
<span v-for="(e, i) in emails" :key="i" class="badge badge-ghost gap-1">
|
|
{{ e }}
|
|
<button type="button" class="text-red-500" @click="emails.splice(i, 1)">×</button>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-medium">排序</span></label>
|
|
<input v-model.number="form.sort" type="number" class="input input-bordered w-full" />
|
|
<span class="label-text-alt text-slate-400 mt-1">小的排前面,相同则按 ID</span>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text font-medium">状态</span></label>
|
|
<label class="label cursor-pointer justify-start gap-3">
|
|
<input v-model="form.enabled" type="checkbox" class="toggle toggle-primary" />
|
|
<span class="label-text">{{ form.enabled ? '合作中' : '已停用' }}</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-2 pt-2">
|
|
<button type="submit" class="btn btn-primary text-white" :disabled="submitting">
|
|
<span v-if="submitting" class="loading loading-spinner loading-xs"></span>
|
|
{{ isEditing ? '保存' : '创建' }}
|
|
</button>
|
|
<button type="button" class="btn btn-ghost" @click="showList()">取消</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|
import { useApi } from '~/composables/useApi'
|
|
import { useToast } from '~/composables/useToast'
|
|
import { useTableSort } from '~/composables/useTableSort'
|
|
|
|
definePageMeta({ title: '工厂管理' })
|
|
|
|
interface OemFactory {
|
|
id: number
|
|
name: string
|
|
contact?: string
|
|
phone?: string
|
|
contactmails?: string
|
|
address?: string
|
|
description?: string
|
|
enabled?: boolean
|
|
sort?: number
|
|
createtime?: number
|
|
updatetime?: number
|
|
}
|
|
|
|
const { webApi } = useApi()
|
|
const { success, error, warn } = useToast()
|
|
|
|
const view = ref<'list' | 'form'>('list')
|
|
const rows = ref<OemFactory[]>([])
|
|
const loading = ref(false)
|
|
const submitting = ref(false)
|
|
const isEditing = ref(false)
|
|
const emails = ref<string[]>([])
|
|
const newEmail = ref('')
|
|
|
|
const filterEnabled = ref(-1) // -1 全部;1 合作中;0 已停用
|
|
const keyword = ref('')
|
|
|
|
const form = reactive({ id: 0, name: '', contact: '', phone: '', address: '', description: '', sort: 0, enabled: true })
|
|
|
|
function mailList(s?: string) {
|
|
return (s || '').split(',').map((e) => e.trim()).filter(Boolean)
|
|
}
|
|
|
|
const { toggleSort, sortMark, applySort } = useTableSort<OemFactory>({
|
|
id: (a, b) => Number(a.id) - Number(b.id),
|
|
name: (a, b) => String(a.name || '').localeCompare(String(b.name || ''), 'zh') || Number(a.id) - Number(b.id),
|
|
sort: (a, b) => Number(a.sort || 0) - Number(b.sort || 0) || Number(a.id) - Number(b.id),
|
|
}, 'sort')
|
|
|
|
const viewRows = computed(() => {
|
|
const kw = keyword.value.trim().toLowerCase()
|
|
const list = rows.value.filter((f) => {
|
|
if (filterEnabled.value >= 0 && (f.enabled === false ? 0 : 1) !== filterEnabled.value) return false
|
|
if (!kw) return true
|
|
return (f.name || '').toLowerCase().includes(kw) ||
|
|
(f.contact || '').toLowerCase().includes(kw) ||
|
|
(f.address || '').toLowerCase().includes(kw) ||
|
|
(f.description || '').toLowerCase().includes(kw)
|
|
})
|
|
return applySort(list)
|
|
})
|
|
function resetFilter() {
|
|
filterEnabled.value = -1
|
|
keyword.value = ''
|
|
}
|
|
|
|
async function loadData() {
|
|
loading.value = true
|
|
try {
|
|
const d = await webApi<{ factories: OemFactory[] }>('getoemfactories', {})
|
|
rows.value = d?.factories ?? []
|
|
} catch (e: any) {
|
|
error(e?.message ?? '获取工厂列表失败')
|
|
rows.value = []
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function showList() {
|
|
view.value = 'list'
|
|
}
|
|
|
|
function showForm(f?: OemFactory) {
|
|
if (f) {
|
|
isEditing.value = true
|
|
form.id = Number(f.id)
|
|
form.name = f.name || ''
|
|
form.contact = f.contact || ''
|
|
form.phone = f.phone || ''
|
|
form.address = f.address || ''
|
|
form.description = f.description || ''
|
|
form.sort = Number(f.sort || 0)
|
|
form.enabled = f.enabled !== false
|
|
emails.value = mailList(f.contactmails)
|
|
} else {
|
|
isEditing.value = false
|
|
form.id = 0
|
|
form.name = ''
|
|
form.contact = ''
|
|
form.phone = ''
|
|
form.address = ''
|
|
form.description = ''
|
|
form.sort = 0
|
|
form.enabled = true
|
|
emails.value = []
|
|
}
|
|
newEmail.value = ''
|
|
view.value = 'form'
|
|
}
|
|
|
|
function addEmail() {
|
|
const e = newEmail.value.trim()
|
|
if (!e) return
|
|
if (emails.value.includes(e)) { warn('该邮箱已添加'); return }
|
|
emails.value.push(e)
|
|
newEmail.value = ''
|
|
}
|
|
|
|
async function saveFactory() {
|
|
if (!form.name.trim()) { warn('请填写工厂名称'); return }
|
|
submitting.value = true
|
|
try {
|
|
const payload = {
|
|
factory: {
|
|
id: form.id,
|
|
name: form.name.trim(),
|
|
contact: form.contact,
|
|
phone: form.phone,
|
|
contactmails: emails.value.join(','),
|
|
address: form.address,
|
|
description: form.description,
|
|
sort: Number(form.sort || 0),
|
|
enabled: form.enabled,
|
|
},
|
|
}
|
|
await webApi(isEditing.value ? 'updateoemfactory' : 'addoemfactory', payload)
|
|
success(isEditing.value ? '已保存' : '已创建')
|
|
showList()
|
|
await loadData()
|
|
} catch (e: any) {
|
|
error(e?.message ?? '保存失败')
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
function delFactory(f: OemFactory) {
|
|
if (!window.confirm(`确认删除工厂「${f.name}」?`)) return
|
|
webApi('deloemfactory', { id: f.id })
|
|
.then(async () => {
|
|
success('已删除')
|
|
await loadData()
|
|
})
|
|
.catch((e: any) => error(e?.message ?? '删除失败'))
|
|
}
|
|
|
|
onMounted(loadData)
|
|
</script>
|
|
|