增加CRM模块
This commit is contained in:
parent
117e2b3440
commit
695bf2194f
33
pc/src/api/contact.js
Normal file
33
pc/src/api/contact.js
Normal file
@ -0,0 +1,33 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function listContacts(params) {
|
||||
return request({
|
||||
url: '/api/crm/contact/list',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export function createContact(data) {
|
||||
return request({
|
||||
url: '/api/crm/contact/add',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function updateContact(data) {
|
||||
return request({
|
||||
url: '/api/crm/contact/edit',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteContact(data) {
|
||||
return request({
|
||||
url: '/api/crm/contact/delete',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
72
pc/src/api/customer.js
Normal file
72
pc/src/api/customer.js
Normal file
@ -0,0 +1,72 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取客户列表
|
||||
* @param {Object} params 查询参数
|
||||
* @param {number} params.tenantId 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function listCustomers(params) {
|
||||
return request({
|
||||
url: '/api/crm/customer/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户详情
|
||||
* @param {number|string} id 客户ID
|
||||
* @param {number} params.tenantId 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getCustomer(id) {
|
||||
return request({
|
||||
url: '/api/crm/customer/detail',
|
||||
method: 'get',
|
||||
params: { id }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建客户
|
||||
* @param {Object} data 客户数据
|
||||
* @param {number} data.tenantId 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function createCustomer(data) {
|
||||
return request({
|
||||
url: '/api/crm/customer/add',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新客户
|
||||
* @param {number|string} id 客户ID
|
||||
* @param {Object} data 更新的数据
|
||||
* @param {number} data.tenantId 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function updateCustomer(id, data) {
|
||||
return request({
|
||||
url: '/api/crm/customer/edit',
|
||||
method: 'post',
|
||||
data: { id, ...data }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除客户
|
||||
* @param {number|string} id 客户ID
|
||||
* @param {number} tenantId 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function deleteCustomer(id, tenantId) {
|
||||
return request({
|
||||
url: '/api/crm/customer/delete',
|
||||
method: 'post',
|
||||
data: { id, tenantId }
|
||||
})
|
||||
}
|
||||
72
pc/src/api/supplier.js
Normal file
72
pc/src/api/supplier.js
Normal file
@ -0,0 +1,72 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取供应商列表
|
||||
* @param {Object} params 查询参数
|
||||
* @param {number} params.tenantId 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function listSuppliers(params) {
|
||||
return request({
|
||||
url: '/api/crm/supplier/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取供应商详情
|
||||
* @param {number|string} id 供应商ID
|
||||
* @param {number} params.tenantId 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getSupplier(id) {
|
||||
return request({
|
||||
url: '/api/crm/supplier/detail',
|
||||
method: 'get',
|
||||
params: { id }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建供应商
|
||||
* @param {Object} data 供应商数据
|
||||
* @param {number} data.tenantId 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function createSupplier(data) {
|
||||
return request({
|
||||
url: '/api/crm/supplier/add',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新供应商
|
||||
* @param {number|string} id 供应商ID
|
||||
* @param {Object} data 更新的数据
|
||||
* @param {number} data.tenantId 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function updateSupplier(id, data) {
|
||||
return request({
|
||||
url: '/api/crm/supplier/edit',
|
||||
method: 'post',
|
||||
data: { id, ...data }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除供应商
|
||||
* @param {number|string} id 供应商ID
|
||||
* @param {number} tenantId 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function deleteSupplier(id, tenantId) {
|
||||
return request({
|
||||
url: '/api/apps/crm/supplier/delete',
|
||||
method: 'post',
|
||||
data: { id, tenantId }
|
||||
})
|
||||
}
|
||||
@ -139,10 +139,11 @@ export const useMenuStore = defineStore('menu', () => {
|
||||
if (res.data !== undefined && res.data !== null) {
|
||||
// 确保 data 是数组
|
||||
const menuData = Array.isArray(res.data) ? res.data : [];
|
||||
menus.value = menuData;
|
||||
const filtered = menuData.filter(m => (m.isShow ?? 1) !== 0);
|
||||
menus.value = filtered;
|
||||
// 保存到缓存
|
||||
saveToCache(menuData);
|
||||
return menuData;
|
||||
saveToCache(filtered);
|
||||
return filtered;
|
||||
} else {
|
||||
// data 为 null 或 undefined,使用空数组
|
||||
console.warn('菜单数据为空,使用空数组');
|
||||
@ -155,9 +156,10 @@ export const useMenuStore = defineStore('menu', () => {
|
||||
// 如果响应格式不符合预期,尝试直接使用 res.data
|
||||
if (res.data !== undefined) {
|
||||
const menuData = Array.isArray(res.data) ? res.data : [];
|
||||
menus.value = menuData;
|
||||
saveToCache(menuData);
|
||||
return menuData;
|
||||
const filtered = menuData.filter(m => (m.isShow ?? 1) !== 0);
|
||||
menus.value = filtered;
|
||||
saveToCache(filtered);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// 如果都不符合,抛出错误
|
||||
|
||||
201
pc/src/views/apps/crm/customer/components/contact.vue
Normal file
201
pc/src/views/apps/crm/customer/components/contact.vue
Normal file
@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
title="联系人管理"
|
||||
size="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="contact-container">
|
||||
<el-button type="primary" @click="handleAdd" style="margin-bottom: 16px">
|
||||
新增联系人
|
||||
</el-button>
|
||||
|
||||
<el-table :data="contactList" border>
|
||||
<el-table-column prop="name" label="姓名" />
|
||||
<el-table-column prop="phone" label="电话" />
|
||||
<el-table-column prop="email" label="邮箱" />
|
||||
<el-table-column prop="position" label="职位" />
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑联系人' : '新增联系人'"
|
||||
width="500px"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="form.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="电话" prop="phone">
|
||||
<el-input v-model="form.phone" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="form.email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="职位" prop="position">
|
||||
<el-input v-model="form.position" />
|
||||
</el-form-item>
|
||||
<el-form-item label="性别" prop="gender">
|
||||
<el-select v-model="form.gender" placeholder="请选择性别" clearable style="width: 100%">
|
||||
<el-option label="未知" :value="0" />
|
||||
<el-option label="男" :value="1" />
|
||||
<el-option label="女" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-input v-model="form.department" />
|
||||
</el-form-item>
|
||||
<el-form-item label="主联系人" prop="is_primary">
|
||||
<el-switch v-model="form.is_primary" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { listContacts, createContact, updateContact, deleteContact } from '@/api/contact.js'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
model: Object
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'saved'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const contactList = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
const form = ref({
|
||||
id: null,
|
||||
name: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
position: '',
|
||||
gender: 0,
|
||||
department: '',
|
||||
is_primary: 0,
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false
|
||||
form.value = { id: null, name: '', phone: '', email: '', position: '' }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleEdit(row) {
|
||||
isEdit.value = true
|
||||
form.value = { ...row }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function getTenantId() {
|
||||
try {
|
||||
const ui = localStorage.getItem('userInfo')
|
||||
if (ui) {
|
||||
const u = JSON.parse(ui)
|
||||
const tid = u.tenant_id || u.tenantId || ''
|
||||
return tid != null && tid !== undefined ? String(tid) : ''
|
||||
}
|
||||
} catch (e) {}
|
||||
return ''
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
const tenant_id = getTenantId()
|
||||
if (!props.model?.id || !tenant_id) return
|
||||
const params = { tenant_id, related_type: 1, related_id: props.model.id }
|
||||
const res = await listContacts(params)
|
||||
const resp = (res && typeof res.code !== 'undefined') ? res : (res && res.data ? res.data : res)
|
||||
if (resp && resp.code === 0) {
|
||||
contactList.value = (resp.data || []).map(it => ({
|
||||
id: it.id,
|
||||
name: it.contact_name,
|
||||
phone: it.mobile || it.phone || '',
|
||||
email: it.email || '',
|
||||
position: it.position || '',
|
||||
gender: Number(it.gender || 0),
|
||||
department: it.department || '',
|
||||
is_primary: Number(it.is_primary || 0),
|
||||
_raw: it,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete(row) {
|
||||
ElMessageBox.confirm('确认删除该联系人?', '提示', {
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const tenant_id = getTenantId()
|
||||
await deleteContact({ id: row.id, tenant_id })
|
||||
ElMessage.success('删除成功')
|
||||
fetchList()
|
||||
emit('saved')
|
||||
})
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate()
|
||||
const tenant_id = getTenantId()
|
||||
const base = {
|
||||
tenant_id,
|
||||
related_type: 1,
|
||||
related_id: props.model?.id,
|
||||
contact_name: form.value.name,
|
||||
mobile: form.value.phone,
|
||||
email: form.value.email,
|
||||
position: form.value.position,
|
||||
gender: form.value.gender,
|
||||
department: form.value.department,
|
||||
is_primary: form.value.is_primary,
|
||||
}
|
||||
if (isEdit.value) {
|
||||
await updateContact({ id: form.value.id, ...base })
|
||||
} else {
|
||||
await createContact(base)
|
||||
}
|
||||
ElMessage.success(isEdit.value ? '修改成功' : '新增成功')
|
||||
dialogVisible.value = false
|
||||
fetchList()
|
||||
emit('saved')
|
||||
}
|
||||
|
||||
watch(() => props.model, (m) => {
|
||||
if (m && m.id) {
|
||||
fetchList()
|
||||
} else {
|
||||
contactList.value = []
|
||||
}
|
||||
}, { immediate: true, deep: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.contact-container {
|
||||
padding: 0 20px;
|
||||
}
|
||||
</style>
|
||||
72
pc/src/views/apps/crm/customer/components/detail.vue
Normal file
72
pc/src/views/apps/crm/customer/components/detail.vue
Normal file
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" :title="title" size="40%" destroy-on-close>
|
||||
<el-descriptions :column="2" border v-if="model">
|
||||
<el-descriptions-item label="客户名称">{{ model.name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系人">{{ model.contact || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="电话">{{ model.phone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">{{ model.email || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户类型">{{ getLabel(customerTypeOptions, model.customer_type) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户等级">{{ getLabel(customerLevelOptions, model.customer_level) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属行业">{{ getLabel(industryOptions, model.industry) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户状态">
|
||||
<el-tag :type="(model.status===1||model.status==='1') ? 'success' : 'info'">
|
||||
{{ getLabel(customerStatusOptions, String(model.status ?? '')) || ((model.status===1||model.status==='1') ? '正常' : '停用') }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="地址" :span="2">{{ model.address || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button @click="visible=false">关闭</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const props = defineProps<{ modelValue: boolean, model?: any }>()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v: boolean) => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const title = computed(() => `客户详情${props.model?.name ? ' - ' + props.model.name : ''}`)
|
||||
|
||||
const customerTypeOptions = ref<{label:string,value:string}[]>([])
|
||||
const customerLevelOptions = ref<{label:string,value:string}[]>([])
|
||||
const industryOptions = ref<{label:string,value:string}[]>([])
|
||||
const customerStatusOptions = ref<{label:string,value:string}[]>([])
|
||||
|
||||
function mapDictItems(items: any[]) {
|
||||
return (items || []).map(it => ({ label: it.dict_label, value: String(it.dict_value) }))
|
||||
}
|
||||
function getLabel(options: {label:string,value:string}[], value: any) {
|
||||
const val = value != null && value !== undefined ? String(value) : ''
|
||||
const found = options.find(o => o.value === val)
|
||||
return found ? found.label : '-'
|
||||
}
|
||||
|
||||
async function loadDictOptions() {
|
||||
const [typeRes, levelRes, industryRes, statusRes] = await Promise.all([
|
||||
request({ url: '/api/dict/items/code/customer_type', method: 'get' }),
|
||||
request({ url: '/api/dict/items/code/customer_level', method: 'get' }),
|
||||
request({ url: '/api/dict/items/code/industry', method: 'get' }),
|
||||
request({ url: '/api/dict/items/code/customer_status', method: 'get' }),
|
||||
])
|
||||
const getData = (r:any) => (r && r.data && (r.data.data || r.data)) || []
|
||||
customerTypeOptions.value = mapDictItems(getData(typeRes))
|
||||
customerLevelOptions.value = mapDictItems(getData(levelRes))
|
||||
industryOptions.value = mapDictItems(getData(industryRes))
|
||||
customerStatusOptions.value = mapDictItems(getData(statusRes))
|
||||
}
|
||||
|
||||
watch(() => visible.value, (v) => {
|
||||
if (v) loadDictOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
225
pc/src/views/apps/crm/customer/components/edit.vue
Normal file
225
pc/src/views/apps/crm/customer/components/edit.vue
Normal file
@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="isEdit ? '编辑客户' : '新增客户'"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<el-form-item label="客户名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入客户名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="客户类型" prop="customer_type">
|
||||
<el-select v-model="form.customer_type" placeholder="请选择客户类型" filterable clearable>
|
||||
<el-option
|
||||
v-for="item in customerTypeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户等级" prop="customer_level">
|
||||
<el-select v-model="form.customer_level" placeholder="请选择客户等级" filterable clearable>
|
||||
<el-option
|
||||
v-for="item in customerLevelOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属行业" prop="industry">
|
||||
<el-select v-model="form.industry" placeholder="请选择所属行业" filterable clearable>
|
||||
<el-option
|
||||
v-for="item in industryOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系人" prop="contact">
|
||||
<el-input v-model="form.contact" placeholder="请输入联系人" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入联系电话" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="地址" prop="address">
|
||||
<el-input v-model="form.address" type="textarea" placeholder="请输入地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="客户状态" prop="status">
|
||||
<el-select v-model="form.status" placeholder="请选择客户状态" clearable>
|
||||
<el-option
|
||||
v-for="item in customerStatusOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="onCancel">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmit" :loading="submitting">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed, onMounted } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { createCustomer, updateCustomer } from '../../../../../api/customer.js'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const props = defineProps<{ modelValue: boolean, isEdit: boolean, model?: any }>()
|
||||
const emit = defineEmits(['update:modelValue', 'saved'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v: boolean) => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitting = ref(false)
|
||||
const form = reactive({
|
||||
id: null as string | null,
|
||||
name: '',
|
||||
customer_type: '',
|
||||
customer_level: '',
|
||||
industry: '',
|
||||
contact: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
address: '',
|
||||
status: '' as any
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
name: [{ required: true, message: '请输入客户名称', trigger: 'blur' }],
|
||||
customer_type: [{ required: true, message: '请选择客户类型', trigger: 'change' }],
|
||||
customer_level: [{ required: true, message: '请选择客户等级', trigger: 'change' }],
|
||||
industry: [{ required: true, message: '请选择所属行业', trigger: 'change' }],
|
||||
contact: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
|
||||
phone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
const customerTypeOptions = ref<{label:string,value:string}[]>([])
|
||||
const customerLevelOptions = ref<{label:string,value:string}[]>([])
|
||||
const industryOptions = ref<{label:string,value:string}[]>([])
|
||||
const customerStatusOptions = ref<{label:string,value:string}[]>([])
|
||||
|
||||
function mapDictItems(items: any[]) {
|
||||
return (items || []).map(it => ({ label: it.dict_label, value: it.dict_value }))
|
||||
}
|
||||
|
||||
async function loadDictOptions() {
|
||||
const [typeRes, levelRes, industryRes, statusRes] = await Promise.all([
|
||||
request({ url: '/api/dict/items/code/customer_type', method: 'get' }),
|
||||
request({ url: '/api/dict/items/code/customer_level', method: 'get' }),
|
||||
request({ url: '/api/dict/items/code/industry', method: 'get' }),
|
||||
request({ url: '/api/dict/items/code/customer_status', method: 'get' }),
|
||||
])
|
||||
const getData = (r:any) => (r && r.data && (r.data.data || r.data)) || []
|
||||
customerTypeOptions.value = mapDictItems(getData(typeRes))
|
||||
customerLevelOptions.value = mapDictItems(getData(levelRes))
|
||||
industryOptions.value = mapDictItems(getData(industryRes))
|
||||
customerStatusOptions.value = mapDictItems(getData(statusRes))
|
||||
}
|
||||
|
||||
watch(() => props.model, (m) => {
|
||||
if (m) {
|
||||
form.id = m.id ?? null
|
||||
form.name = (m.customer_name ?? m.name) ?? ''
|
||||
form.customer_type = m.customer_type != null && m.customer_type !== undefined ? String(m.customer_type) : ''
|
||||
form.customer_level = m.customer_level != null && m.customer_level !== undefined ? String(m.customer_level) : ''
|
||||
form.industry = m.industry != null && m.industry !== undefined ? String(m.industry) : ''
|
||||
form.contact = (m.contact_person ?? m.contact) ?? ''
|
||||
form.phone = (m.contact_phone ?? m.phone) ?? ''
|
||||
form.email = (m.contact_email ?? m.email) ?? ''
|
||||
form.address = m.address ?? ''
|
||||
form.status = m.status != null && m.status !== undefined ? String(m.status) : ''
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
}, { immediate: true, deep: true })
|
||||
|
||||
function resetForm() {
|
||||
form.id = null
|
||||
form.name = ''
|
||||
form.customer_type = ''
|
||||
form.customer_level = ''
|
||||
form.industry = ''
|
||||
form.contact = ''
|
||||
form.phone = ''
|
||||
form.email = ''
|
||||
form.address = ''
|
||||
form.status = ''
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate()
|
||||
try {
|
||||
submitting.value = true
|
||||
const tenantId = (() => {
|
||||
try {
|
||||
const ui = localStorage.getItem('userInfo')
|
||||
if (ui) {
|
||||
const u = JSON.parse(ui)
|
||||
const tid = u.tenant_id || u.tenantId || ''
|
||||
return tid != null && tid !== undefined ? String(tid) : ''
|
||||
}
|
||||
} catch (e) {}
|
||||
return ''
|
||||
})()
|
||||
const payload: any = {
|
||||
customer_name: form.name,
|
||||
customer_type: form.customer_type,
|
||||
customer_level: form.customer_level,
|
||||
industry: form.industry,
|
||||
contact_person: form.contact,
|
||||
contact_phone: form.phone,
|
||||
contact_email: form.email,
|
||||
address: form.address,
|
||||
status: String(form.status),
|
||||
tenant_id: tenantId,
|
||||
}
|
||||
if (props.isEdit && form.id) {
|
||||
await updateCustomer(form.id, payload)
|
||||
} else {
|
||||
await createCustomer(payload)
|
||||
}
|
||||
ElMessage.success(props.isEdit ? '修改成功' : '新增成功')
|
||||
emit('saved')
|
||||
visible.value = false
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
watch(() => visible.value, (v) => {
|
||||
if (v) {
|
||||
loadDictOptions()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (visible.value) {
|
||||
loadDictOptions()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
107
pc/src/views/apps/crm/customer/components/invoice.vue
Normal file
107
pc/src/views/apps/crm/customer/components/invoice.vue
Normal file
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
title="开票信息"
|
||||
size="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
|
||||
<el-form-item label="发票抬头" prop="invoice_title">
|
||||
<el-input v-model="form.invoice_title" placeholder="请输入发票抬头" />
|
||||
</el-form-item>
|
||||
<el-form-item label="纳税人识别号" prop="tax_number">
|
||||
<el-input v-model="form.tax_number" placeholder="请输入纳税人识别号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开户银行" prop="bank_name">
|
||||
<el-input v-model="form.bank_name" placeholder="请输入开户银行" />
|
||||
</el-form-item>
|
||||
<el-form-item label="银行账号" prop="bank_account">
|
||||
<el-input v-model="form.bank_account" placeholder="请输入银行账号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="注册地址" prop="registered_address">
|
||||
<el-input v-model="form.registered_address" type="textarea" placeholder="请输入注册地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="注册电话" prop="registered_phone">
|
||||
<el-input v-model="form.registered_phone" placeholder="请输入注册电话" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="onCancel">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmit" :loading="submitting">保存</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps<{ modelValue: boolean, model?: any }>()
|
||||
const emit = defineEmits(['update:modelValue', 'saved'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v: boolean) => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitting = ref(false)
|
||||
const form = reactive({
|
||||
invoice_title: '',
|
||||
tax_number: '',
|
||||
bank_name: '',
|
||||
bank_account: '',
|
||||
registered_address: '',
|
||||
registered_phone: ''
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
invoice_title: [{ required: true, message: '请输入发票抬头', trigger: 'blur' }],
|
||||
tax_number: [{ required: true, message: '请输入纳税人识别号', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.invoice_title = ''
|
||||
form.tax_number = ''
|
||||
form.bank_name = ''
|
||||
form.bank_account = ''
|
||||
form.registered_address = ''
|
||||
form.registered_phone = ''
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate()
|
||||
try {
|
||||
submitting.value = true
|
||||
// TODO: 调用保存开票信息的API
|
||||
ElMessage.success('保存成功')
|
||||
emit('saved')
|
||||
visible.value = false
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.model, (m) => {
|
||||
if (m) {
|
||||
form.invoice_title = m.invoice_title ?? ''
|
||||
form.tax_number = m.tax_number ?? ''
|
||||
form.bank_name = m.bank_name ?? ''
|
||||
form.bank_account = m.bank_account ?? ''
|
||||
form.registered_address = m.registered_address ?? ''
|
||||
form.registered_phone = m.registered_phone ?? ''
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
}, { immediate: true, deep: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
318
pc/src/views/apps/crm/customer/index.vue
Normal file
318
pc/src/views/apps/crm/customer/index.vue
Normal file
@ -0,0 +1,318 @@
|
||||
<template>
|
||||
<div class="crm-customer">
|
||||
<div class="customer-container">
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="handleAdd">新增客户</el-button>
|
||||
<el-button @click="handleRefresh">刷新</el-button>
|
||||
<div class="search-bar">
|
||||
<el-input v-model="searchQuery" placeholder="搜索客户名称/联系人/电话" clearable @clear="handleSearch">
|
||||
<template #append>
|
||||
<el-button :icon="Search" @click="handleSearch" />
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 客户列表 -->
|
||||
<el-table :data="customerList" v-loading="loading" stripe border style="width: 100%">
|
||||
<el-table-column prop="name" label="客户名称" width="220" />
|
||||
<el-table-column prop="contact" label="联系人" width="100" />
|
||||
<el-table-column prop="phone" label="联系电话" min-width="80" />
|
||||
<el-table-column prop="email" label="邮箱" min-width="100" />
|
||||
<el-table-column prop="address" label="地址" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'">
|
||||
{{ row.status === 1 ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="info" @click="handleContactView(row)">联系人</el-button>
|
||||
<el-button link type="info" @click="handleView(row)">详情</el-button>
|
||||
<el-button link type="info" @click="handleInvoiceView(row)">开票信息</el-button>
|
||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :total="total"
|
||||
:page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange" @current-change="handleCurrentChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<Edit v-model="dialogVisible" :is-edit="isEdit" :model="currentRow" @saved="onSaved" />
|
||||
|
||||
<!-- 详情抽屉 -->
|
||||
<Detail v-model="detailVisible" :model="currentRow" />
|
||||
|
||||
<!-- 开票信息抽屉 -->
|
||||
<Invoice v-model="invoiceVisible" :model="currentRow" />
|
||||
|
||||
<!-- 联系人抽屉 -->
|
||||
<Contact v-model="contactVisible" :model="currentRow" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import Edit from './components/edit.vue'
|
||||
import Detail from './components/detail.vue'
|
||||
import Invoice from './components/invoice.vue'
|
||||
import Contact from './components/contact.vue'
|
||||
import { listCustomers, getCustomer, deleteCustomer } from '@/api/customer.js'
|
||||
|
||||
const loading = ref(false);
|
||||
const customerList = ref([]);
|
||||
const searchQuery = ref('');
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
const dialogVisible = ref(false);
|
||||
const detailVisible = ref(false);
|
||||
const contactVisible = ref(false);
|
||||
const invoiceVisible = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const currentRow = ref(null);
|
||||
|
||||
function fetchCustomerList() {
|
||||
loading.value = true;
|
||||
const params = {
|
||||
keyword: searchQuery.value,
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
}
|
||||
listCustomers(params)
|
||||
.then((res) => {
|
||||
const resp = (res && typeof res.code !== 'undefined') ? res : (res && res.data ? res.data : res)
|
||||
if (resp && resp.code === 0 && resp.data) {
|
||||
const rows = resp.data.list || []
|
||||
total.value = resp.data.total || 0
|
||||
customerList.value = rows.map((m) => ({
|
||||
id: m.id,
|
||||
name: m.customer_name || m.name || '',
|
||||
contact: m.contact_person || m.contact || '',
|
||||
phone: m.contact_phone || m.phone || '',
|
||||
email: m.contact_email || m.email || '',
|
||||
address: m.address || '',
|
||||
status: typeof m.status === 'number' ? m.status : (m.status === '0' ? 0 : 1),
|
||||
_raw: m,
|
||||
}))
|
||||
} else {
|
||||
customerList.value = []
|
||||
total.value = 0
|
||||
if (resp && resp.message) ElMessage.error(resp.message)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
currentRow.value = null;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(row) {
|
||||
isEdit.value = true;
|
||||
// 获取详情,保证带上字典字段值
|
||||
getCustomer(row.id).then((res) => {
|
||||
const resp = (res && typeof res.code !== 'undefined') ? res : (res && res.data ? res.data : res)
|
||||
if (resp && resp.code === 0 && resp.data) {
|
||||
const m = resp.data
|
||||
currentRow.value = {
|
||||
id: m.id,
|
||||
name: m.customer_name || m.name || '',
|
||||
contact: m.contact_person || m.contact || '',
|
||||
phone: m.contact_phone || m.phone || '',
|
||||
email: m.contact_email || m.email || '',
|
||||
address: m.address || '',
|
||||
status: typeof m.status === 'number' ? m.status : (m.status === '0' ? 0 : 1),
|
||||
// 传递字典字段原始值
|
||||
customer_type: m.customer_type,
|
||||
customer_level: m.customer_level,
|
||||
industry: m.industry,
|
||||
_raw: m,
|
||||
}
|
||||
} else {
|
||||
// 回退使用列表行数据
|
||||
currentRow.value = { ...row }
|
||||
}
|
||||
dialogVisible.value = true;
|
||||
})
|
||||
}
|
||||
|
||||
function handleView(row) {
|
||||
// 拉取最新详情再展示
|
||||
getCustomer(row.id).then((res) => {
|
||||
const resp = (res && typeof res.code !== 'undefined') ? res : (res && res.data ? res.data : res)
|
||||
if (resp && resp.code === 0 && resp.data) {
|
||||
const m = resp.data
|
||||
currentRow.value = {
|
||||
id: m.id,
|
||||
name: m.customer_name || m.name || '',
|
||||
contact: m.contact_person || m.contact || '',
|
||||
phone: m.contact_phone || m.phone || '',
|
||||
email: m.contact_email || m.email || '',
|
||||
address: m.address || '',
|
||||
status: typeof m.status === 'number' ? m.status : (m.status === '0' ? 0 : 1),
|
||||
// 传递字典字段原始值供详情组件显示
|
||||
customer_type: m.customer_type,
|
||||
customer_level: m.customer_level,
|
||||
industry: m.industry,
|
||||
_raw: m,
|
||||
}
|
||||
detailVisible.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleContactView(row) {
|
||||
// 先打开联系人抽屉,再异步拉详情以便得到 _raw.contacts
|
||||
currentRow.value = { ...row }
|
||||
contactVisible.value = true
|
||||
getCustomer(row.id).then((res) => {
|
||||
const resp = (res && typeof res.code !== 'undefined') ? res : (res && res.data ? res.data : res)
|
||||
if (resp && resp.code === 0 && resp.data) {
|
||||
const m = resp.data
|
||||
currentRow.value = {
|
||||
id: m.id,
|
||||
name: m.customer_name || m.name || '',
|
||||
contact: m.contact_person || m.contact || '',
|
||||
phone: m.contact_phone || m.phone || '',
|
||||
email: m.contact_email || m.email || '',
|
||||
address: m.address || '',
|
||||
status: typeof m.status === 'number' ? m.status : (m.status === '0' ? 0 : 1),
|
||||
customer_type: m.customer_type,
|
||||
customer_level: m.customer_level,
|
||||
industry: m.industry,
|
||||
_raw: m,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleInvoiceView(row) {
|
||||
// 先打开抽屉,后异步加载详情
|
||||
currentRow.value = { ...row }
|
||||
invoiceVisible.value = true
|
||||
getCustomer(row.id)
|
||||
.then((res) => {
|
||||
const resp = (res && typeof res.code !== 'undefined') ? res : (res && res.data ? res.data : res)
|
||||
if (resp && resp.code === 0 && resp.data) {
|
||||
const m = resp.data
|
||||
currentRow.value = {
|
||||
id: m.id,
|
||||
name: m.customer_name || m.name || '',
|
||||
contact: m.contact_person || m.contact || '',
|
||||
phone: m.contact_phone || m.phone || '',
|
||||
email: m.contact_email || m.email || '',
|
||||
address: m.address || '',
|
||||
status: typeof m.status === 'number' ? m.status : (m.status === '0' ? 0 : 1),
|
||||
customer_type: m.customer_type,
|
||||
customer_level: m.customer_level,
|
||||
industry: m.industry,
|
||||
invoice_title: m.invoice_title,
|
||||
tax_number: m.tax_number,
|
||||
bank_name: m.bank_name,
|
||||
bank_account: m.bank_account,
|
||||
registered_address: m.registered_address,
|
||||
registered_phone: m.registered_phone,
|
||||
_raw: m,
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
function handleDelete(row) {
|
||||
ElMessageBox.confirm('确定要删除该客户吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
deleteCustomer(row.id)
|
||||
.then((res) => {
|
||||
const resp = (res && typeof res.code !== 'undefined') ? res : (res && res.data ? res.data : res)
|
||||
if (resp && resp.code === 0) {
|
||||
ElMessage.success('删除成功');
|
||||
fetchCustomerList();
|
||||
} else {
|
||||
ElMessage.error((resp && resp.message) || '删除失败')
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function onSaved() {
|
||||
fetchCustomerList();
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
currentPage.value = 1;
|
||||
fetchCustomerList();
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
searchQuery.value = '';
|
||||
currentPage.value = 1;
|
||||
fetchCustomerList();
|
||||
}
|
||||
|
||||
function handleSizeChange() {
|
||||
fetchCustomerList();
|
||||
}
|
||||
|
||||
function handleCurrentChange() {
|
||||
fetchCustomerList();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchCustomerList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.crm-customer {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
background: #f5f7fa;
|
||||
|
||||
.customer-container {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.search-bar {
|
||||
margin-left: auto;
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
7
pc/src/views/apps/crm/index.vue
Normal file
7
pc/src/views/apps/crm/index.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
201
pc/src/views/apps/crm/supplier/components/contact.vue
Normal file
201
pc/src/views/apps/crm/supplier/components/contact.vue
Normal file
@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
title="联系人管理"
|
||||
size="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="contact-container">
|
||||
<el-button type="primary" @click="handleAdd" style="margin-bottom: 16px">
|
||||
新增联系人
|
||||
</el-button>
|
||||
|
||||
<el-table :data="contactList" border>
|
||||
<el-table-column prop="name" label="姓名" />
|
||||
<el-table-column prop="phone" label="电话" />
|
||||
<el-table-column prop="email" label="邮箱" />
|
||||
<el-table-column prop="position" label="职位" />
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑联系人' : '新增联系人'"
|
||||
width="500px"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="form.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="电话" prop="phone">
|
||||
<el-input v-model="form.phone" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="form.email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="职位" prop="position">
|
||||
<el-input v-model="form.position" />
|
||||
</el-form-item>
|
||||
<el-form-item label="性别" prop="gender">
|
||||
<el-select v-model="form.gender" placeholder="请选择性别" clearable style="width: 100%">
|
||||
<el-option label="未知" :value="0" />
|
||||
<el-option label="男" :value="1" />
|
||||
<el-option label="女" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="部门" prop="department">
|
||||
<el-input v-model="form.department" />
|
||||
</el-form-item>
|
||||
<el-form-item label="主联系人" prop="is_primary">
|
||||
<el-switch v-model="form.is_primary" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { listContacts, createContact, updateContact, deleteContact } from '@/api/contact.js'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
model: Object
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'saved'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const contactList = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
const form = ref({
|
||||
id: null,
|
||||
name: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
position: '',
|
||||
gender: 0,
|
||||
department: '',
|
||||
is_primary: 0,
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false
|
||||
form.value = { id: null, name: '', phone: '', email: '', position: '' }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleEdit(row) {
|
||||
isEdit.value = true
|
||||
form.value = { ...row }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function getTenantId() {
|
||||
try {
|
||||
const ui = localStorage.getItem('userInfo')
|
||||
if (ui) {
|
||||
const u = JSON.parse(ui)
|
||||
const tid = u.tenant_id || u.tenantId || ''
|
||||
return tid != null && tid !== undefined ? String(tid) : ''
|
||||
}
|
||||
} catch (e) {}
|
||||
return ''
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
const tenant_id = getTenantId()
|
||||
if (!props.model?.id || !tenant_id) return
|
||||
const params = { tenant_id, related_type: 2, related_id: props.model.id }
|
||||
const res = await listContacts(params)
|
||||
const resp = (res && typeof res.code !== 'undefined') ? res : (res && res.data ? res.data : res)
|
||||
if (resp && resp.code === 0) {
|
||||
contactList.value = (resp.data || []).map(it => ({
|
||||
id: it.id,
|
||||
name: it.contact_name,
|
||||
phone: it.mobile || it.phone || '',
|
||||
email: it.email || '',
|
||||
position: it.position || '',
|
||||
gender: Number(it.gender || 0),
|
||||
department: it.department || '',
|
||||
is_primary: Number(it.is_primary || 0),
|
||||
_raw: it,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete(row) {
|
||||
ElMessageBox.confirm('确认删除该联系人?', '提示', {
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const tenant_id = getTenantId()
|
||||
await deleteContact({ id: row.id, tenant_id })
|
||||
ElMessage.success('删除成功')
|
||||
fetchList()
|
||||
emit('saved')
|
||||
})
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate()
|
||||
const tenant_id = getTenantId()
|
||||
const base = {
|
||||
tenant_id,
|
||||
related_type: 2,
|
||||
related_id: props.model?.id,
|
||||
contact_name: form.value.name,
|
||||
mobile: form.value.phone,
|
||||
email: form.value.email,
|
||||
position: form.value.position,
|
||||
gender: form.value.gender,
|
||||
department: form.value.department,
|
||||
is_primary: form.value.is_primary,
|
||||
}
|
||||
if (isEdit.value) {
|
||||
await updateContact({ id: form.value.id, ...base })
|
||||
} else {
|
||||
await createContact(base)
|
||||
}
|
||||
ElMessage.success(isEdit.value ? '修改成功' : '新增成功')
|
||||
dialogVisible.value = false
|
||||
fetchList()
|
||||
emit('saved')
|
||||
}
|
||||
|
||||
watch(() => props.model, (m) => {
|
||||
if (m && m.id) {
|
||||
fetchList()
|
||||
} else {
|
||||
contactList.value = []
|
||||
}
|
||||
}, { immediate: true, deep: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.contact-container {
|
||||
padding: 0 20px;
|
||||
}
|
||||
</style>
|
||||
72
pc/src/views/apps/crm/supplier/components/detail.vue
Normal file
72
pc/src/views/apps/crm/supplier/components/detail.vue
Normal file
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" :title="title" size="40%" destroy-on-close>
|
||||
<el-descriptions :column="2" border v-if="model">
|
||||
<el-descriptions-item label="供应商名称">{{ model.name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系人">{{ model.contact || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="电话">{{ model.phone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">{{ model.email || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="供应商类型">{{ getLabel(supplierTypeOptions, model.supplier_type) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="供应商等级">{{ getLabel(supplierLevelOptions, model.supplier_level) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属行业">{{ getLabel(industryOptions, model.industry) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="供应商状态">
|
||||
<el-tag :type="(model.status===1||model.status==='1') ? 'success' : 'info'">
|
||||
{{ getLabel(supplierStatusOptions, String(model.status ?? '')) || ((model.status===1||model.status==='1') ? '正常' : '停用') }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="地址" :span="2">{{ model.address || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button @click="visible=false">关闭</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const props = defineProps<{ modelValue: boolean, model?: any }>()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v: boolean) => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const title = computed(() => `供应商详情${props.model?.name ? ' - ' + props.model.name : ''}`)
|
||||
|
||||
const supplierTypeOptions = ref<{label:string,value:string}[]>([])
|
||||
const supplierLevelOptions = ref<{label:string,value:string}[]>([])
|
||||
const industryOptions = ref<{label:string,value:string}[]>([])
|
||||
const supplierStatusOptions = ref<{label:string,value:string}[]>([])
|
||||
|
||||
function mapDictItems(items: any[]) {
|
||||
return (items || []).map(it => ({ label: it.dict_label, value: String(it.dict_value) }))
|
||||
}
|
||||
function getLabel(options: {label:string,value:string}[], value: any) {
|
||||
const val = value != null && value !== undefined ? String(value) : ''
|
||||
const found = options.find(o => o.value === val)
|
||||
return found ? found.label : '-'
|
||||
}
|
||||
|
||||
async function loadDictOptions() {
|
||||
const [typeRes, levelRes, industryRes, statusRes] = await Promise.all([
|
||||
request({ url: '/api/dict/items/code/supplier_type', method: 'get' }),
|
||||
request({ url: '/api/dict/items/code/supplier_level', method: 'get' }),
|
||||
request({ url: '/api/dict/items/code/industry', method: 'get' }),
|
||||
request({ url: '/api/dict/items/code/supplier_status', method: 'get' }),
|
||||
])
|
||||
const getData = (r:any) => (r && r.data && (r.data.data || r.data)) || []
|
||||
supplierTypeOptions.value = mapDictItems(getData(typeRes))
|
||||
supplierLevelOptions.value = mapDictItems(getData(levelRes))
|
||||
industryOptions.value = mapDictItems(getData(industryRes))
|
||||
supplierStatusOptions.value = mapDictItems(getData(statusRes))
|
||||
}
|
||||
|
||||
watch(() => visible.value, (v) => {
|
||||
if (v) loadDictOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
204
pc/src/views/apps/crm/supplier/components/edit.vue
Normal file
204
pc/src/views/apps/crm/supplier/components/edit.vue
Normal file
@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="isEdit ? '编辑供应商' : '新增供应商'"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<el-form-item label="供应商名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入供应商名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="供应商类型" prop="supplier_type">
|
||||
<el-select v-model="form.supplier_type" placeholder="请选择供应商类型" filterable clearable>
|
||||
<el-option v-for="item in supplierTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="供应商等级" prop="supplier_level">
|
||||
<el-select v-model="form.supplier_level" placeholder="请选择供应商等级" filterable clearable>
|
||||
<el-option v-for="item in supplierLevelOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属行业" prop="industry">
|
||||
<el-select v-model="form.industry" placeholder="请选择所属行业" filterable clearable>
|
||||
<el-option v-for="item in industryOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系人" prop="contact">
|
||||
<el-input v-model="form.contact" placeholder="请输入联系人" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入联系电话" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="地址" prop="address">
|
||||
<el-input v-model="form.address" type="textarea" placeholder="请输入地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="供应商状态" prop="status">
|
||||
<el-select v-model="form.status" placeholder="请选择供应商状态" clearable>
|
||||
<el-option v-for="item in supplierStatusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="onCancel">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmit" :loading="submitting">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed, onMounted } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { createSupplier, updateSupplier } from '../../../../../api/supplier.js'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const props = defineProps<{ modelValue: boolean, isEdit: boolean, model?: any }>()
|
||||
const emit = defineEmits(['update:modelValue', 'saved'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v: boolean) => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitting = ref(false)
|
||||
const form = reactive({
|
||||
id: null as string | null,
|
||||
name: '',
|
||||
supplier_type: '',
|
||||
supplier_level: '',
|
||||
industry: '',
|
||||
contact: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
address: '',
|
||||
status: '' as any
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
name: [{ required: true, message: '请输入供应商名称', trigger: 'blur' }],
|
||||
supplier_type: [{ required: true, message: '请选择供应商类型', trigger: 'change' }],
|
||||
supplier_level: [{ required: true, message: '请选择供应商等级', trigger: 'change' }],
|
||||
industry: [{ required: true, message: '请选择所属行业', trigger: 'change' }],
|
||||
contact: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
|
||||
phone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
const supplierTypeOptions = ref<{label:string,value:string}[]>([])
|
||||
const supplierLevelOptions = ref<{label:string,value:string}[]>([])
|
||||
const industryOptions = ref<{label:string,value:string}[]>([])
|
||||
const supplierStatusOptions = ref<{label:string,value:string}[]>([])
|
||||
|
||||
function mapDictItems(items: any[]) {
|
||||
return (items || []).map(it => ({ label: it.dict_label, value: String(it.dict_value) }))
|
||||
}
|
||||
|
||||
async function loadDictOptions() {
|
||||
const [typeRes, levelRes, industryRes, statusRes] = await Promise.all([
|
||||
request({ url: '/api/dict/items/code/supplier_type', method: 'get' }),
|
||||
request({ url: '/api/dict/items/code/supplier_level', method: 'get' }),
|
||||
request({ url: '/api/dict/items/code/industry', method: 'get' }),
|
||||
request({ url: '/api/dict/items/code/supplier_status', method: 'get' }),
|
||||
])
|
||||
const getData = (r:any) => (r && r.data && (r.data.data || r.data)) || []
|
||||
supplierTypeOptions.value = mapDictItems(getData(typeRes))
|
||||
supplierLevelOptions.value = mapDictItems(getData(levelRes))
|
||||
industryOptions.value = mapDictItems(getData(industryRes))
|
||||
supplierStatusOptions.value = mapDictItems(getData(statusRes))
|
||||
}
|
||||
|
||||
watch(() => props.model, (m) => {
|
||||
if (m) {
|
||||
form.id = m.id ?? null
|
||||
form.name = (m.supplier_name ?? m.name) ?? ''
|
||||
form.supplier_type = m.supplier_type != null && m.supplier_type !== undefined ? String(m.supplier_type) : ''
|
||||
form.supplier_level = m.supplier_level != null && m.supplier_level !== undefined ? String(m.supplier_level) : ''
|
||||
form.industry = m.industry != null && m.industry !== undefined ? String(m.industry) : ''
|
||||
form.contact = (m.contact_person ?? m.contact) ?? ''
|
||||
form.phone = (m.contact_phone ?? m.phone) ?? ''
|
||||
form.email = (m.contact_email ?? m.email) ?? ''
|
||||
form.address = m.address ?? ''
|
||||
form.status = m.status != null && m.status !== undefined ? String(m.status) : ''
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
}, { immediate: true, deep: true })
|
||||
|
||||
function resetForm() {
|
||||
form.id = null
|
||||
form.name = ''
|
||||
form.supplier_type = ''
|
||||
form.supplier_level = ''
|
||||
form.industry = ''
|
||||
form.contact = ''
|
||||
form.phone = ''
|
||||
form.email = ''
|
||||
form.address = ''
|
||||
form.status = ''
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate()
|
||||
try {
|
||||
submitting.value = true
|
||||
const tenantId = (() => {
|
||||
try {
|
||||
const ui = localStorage.getItem('userInfo')
|
||||
if (ui) {
|
||||
const u = JSON.parse(ui)
|
||||
const tid = u.tenant_id || u.tenantId || ''
|
||||
return tid != null && tid !== undefined ? String(tid) : ''
|
||||
}
|
||||
} catch (e) {}
|
||||
return ''
|
||||
})()
|
||||
const payload: any = {
|
||||
supplier_name: form.name,
|
||||
supplier_type: form.supplier_type,
|
||||
supplier_level: form.supplier_level,
|
||||
industry: form.industry,
|
||||
contact_person: form.contact,
|
||||
contact_phone: form.phone,
|
||||
contact_email: form.email,
|
||||
address: form.address,
|
||||
status: String(form.status),
|
||||
tenant_id: tenantId,
|
||||
}
|
||||
if (props.isEdit && form.id) {
|
||||
await updateSupplier(form.id, payload)
|
||||
} else {
|
||||
await createSupplier(payload)
|
||||
}
|
||||
ElMessage.success(props.isEdit ? '修改成功' : '新增成功')
|
||||
emit('saved')
|
||||
visible.value = false
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
watch(() => visible.value, (v) => {
|
||||
if (v) {
|
||||
loadDictOptions()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (visible.value) {
|
||||
loadDictOptions()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
107
pc/src/views/apps/crm/supplier/components/invoice.vue
Normal file
107
pc/src/views/apps/crm/supplier/components/invoice.vue
Normal file
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
title="开票信息"
|
||||
size="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
|
||||
<el-form-item label="发票抬头" prop="invoice_title">
|
||||
<el-input v-model="form.invoice_title" placeholder="请输入发票抬头" />
|
||||
</el-form-item>
|
||||
<el-form-item label="纳税人识别号" prop="tax_number">
|
||||
<el-input v-model="form.tax_number" placeholder="请输入纳税人识别号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开户银行" prop="bank_name">
|
||||
<el-input v-model="form.bank_name" placeholder="请输入开户银行" />
|
||||
</el-form-item>
|
||||
<el-form-item label="银行账号" prop="bank_account">
|
||||
<el-input v-model="form.bank_account" placeholder="请输入银行账号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="注册地址" prop="registered_address">
|
||||
<el-input v-model="form.registered_address" type="textarea" placeholder="请输入注册地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="注册电话" prop="registered_phone">
|
||||
<el-input v-model="form.registered_phone" placeholder="请输入注册电话" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="onCancel">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmit" :loading="submitting">保存</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps<{ modelValue: boolean, model?: any }>()
|
||||
const emit = defineEmits(['update:modelValue', 'saved'])
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v: boolean) => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitting = ref(false)
|
||||
const form = reactive({
|
||||
invoice_title: '',
|
||||
tax_number: '',
|
||||
bank_name: '',
|
||||
bank_account: '',
|
||||
registered_address: '',
|
||||
registered_phone: ''
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
invoice_title: [{ required: true, message: '请输入发票抬头', trigger: 'blur' }],
|
||||
tax_number: [{ required: true, message: '请输入纳税人识别号', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.invoice_title = ''
|
||||
form.tax_number = ''
|
||||
form.bank_name = ''
|
||||
form.bank_account = ''
|
||||
form.registered_address = ''
|
||||
form.registered_phone = ''
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate()
|
||||
try {
|
||||
submitting.value = true
|
||||
// TODO: 调用保存开票信息的API
|
||||
ElMessage.success('保存成功')
|
||||
emit('saved')
|
||||
visible.value = false
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.model, (m) => {
|
||||
if (m) {
|
||||
form.invoice_title = m.invoice_title ?? ''
|
||||
form.tax_number = m.tax_number ?? ''
|
||||
form.bank_name = m.bank_name ?? ''
|
||||
form.bank_account = m.bank_account ?? ''
|
||||
form.registered_address = m.registered_address ?? ''
|
||||
form.registered_phone = m.registered_phone ?? ''
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
}, { immediate: true, deep: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
310
pc/src/views/apps/crm/supplier/index.vue
Normal file
310
pc/src/views/apps/crm/supplier/index.vue
Normal file
@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<div class="crm-customer">
|
||||
<div class="customer-container">
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="handleAdd">新增供应商</el-button>
|
||||
<el-button @click="handleRefresh">刷新</el-button>
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索供应商名称/联系人/电话"
|
||||
clearable
|
||||
@clear="handleSearch"
|
||||
>
|
||||
<template #append>
|
||||
<el-button :icon="Search" @click="handleSearch" />
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 供应商列表 -->
|
||||
<el-table
|
||||
:data="customerList"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
border
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="name" label="供应商名称" width="180" />
|
||||
<el-table-column prop="contact" label="联系人" width="120" />
|
||||
<el-table-column prop="phone" label="联系电话" width="150" />
|
||||
<el-table-column prop="email" label="邮箱" width="200" />
|
||||
<el-table-column prop="address" label="地址" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'">
|
||||
{{ row.status === 1 ? '正常' : '停用' }}
|
||||
|
||||
function handleContactView(row) {
|
||||
currentRow.value = { ...row }
|
||||
contactVisible.value = true
|
||||
getSupplier(row.id).then((res) => {
|
||||
const resp = (res && typeof res.code !== 'undefined') ? res : (res && res.data ? res.data : res)
|
||||
if (resp && resp.code === 0 && resp.data) {
|
||||
const m = resp.data
|
||||
currentRow.value = {
|
||||
id: m.id,
|
||||
name: m.supplier_name || m.name || '',
|
||||
contact: m.contact_person || m.contact || '',
|
||||
phone: m.contact_phone || m.phone || '',
|
||||
email: m.contact_email || m.email || '',
|
||||
address: m.address || '',
|
||||
status: typeof m.status === 'number' ? m.status : (m.status === '0' ? 0 : 1),
|
||||
supplier_type: m.supplier_type,
|
||||
supplier_level: m.supplier_level,
|
||||
industry: m.industry,
|
||||
_raw: m,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleInvoiceView(row) {
|
||||
currentRow.value = { ...row }
|
||||
invoiceVisible.value = true
|
||||
getSupplier(row.id).then((res) => {
|
||||
const resp = (res && typeof res.code !== 'undefined') ? res : (res && res.data ? res.data : res)
|
||||
if (resp && resp.code === 0 && resp.data) {
|
||||
const m = resp.data
|
||||
currentRow.value = {
|
||||
id: m.id,
|
||||
name: m.supplier_name || m.name || '',
|
||||
contact: m.contact_person || m.contact || '',
|
||||
phone: m.contact_phone || m.phone || '',
|
||||
email: m.contact_email || m.email || '',
|
||||
address: m.address || '',
|
||||
status: typeof m.status === 'number' ? m.status : (m.status === '0' ? 0 : 1),
|
||||
supplier_type: m.supplier_type,
|
||||
supplier_level: m.supplier_level,
|
||||
industry: m.industry,
|
||||
// 开票相关(如果后端返回则透传给抽屉)
|
||||
invoice_title: m.invoice_title,
|
||||
tax_number: m.tax_number,
|
||||
bank_name: m.bank_name,
|
||||
bank_account: m.bank_account,
|
||||
registered_address: m.registered_address,
|
||||
registered_phone: m.registered_phone,
|
||||
_raw: m,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="info" @click="handleContactView(row)">联系人</el-button>
|
||||
<el-button link type="info" @click="handleView(row)">详情</el-button>
|
||||
<el-button link type="info" @click="handleInvoiceView(row)">开票信息</el-button>
|
||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<Edit
|
||||
v-model="dialogVisible"
|
||||
:is-edit="isEdit"
|
||||
:model="currentRow"
|
||||
@saved="onSaved"
|
||||
/>
|
||||
|
||||
<!-- 详情抽屉 -->
|
||||
<Detail v-model="detailVisible" :model="currentRow" />
|
||||
|
||||
<!-- 联系人抽屉 -->
|
||||
<Contact v-model="contactVisible" :model="currentRow" />
|
||||
|
||||
<!-- 开票信息抽屉 -->
|
||||
<Invoice v-model="invoiceVisible" :model="currentRow" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import Edit from './components/edit.vue'
|
||||
import Detail from './components/detail.vue'
|
||||
import Contact from './components/contact.vue'
|
||||
import Invoice from './components/invoice.vue'
|
||||
import { listSuppliers, getSupplier, deleteSupplier } from '../../../../api/supplier.js'
|
||||
|
||||
const loading = ref(false);
|
||||
const customerList = ref([]);
|
||||
const searchQuery = ref('');
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
const dialogVisible = ref(false);
|
||||
const detailVisible = ref(false);
|
||||
const contactVisible = ref(false);
|
||||
const invoiceVisible = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const currentRow = ref(null);
|
||||
|
||||
function fetchCustomerList() {
|
||||
loading.value = true;
|
||||
const params = {
|
||||
keyword: searchQuery.value,
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
}
|
||||
listSuppliers(params)
|
||||
.then((res) => {
|
||||
const resp = res && res.data ? res.data : res
|
||||
if (resp && resp.code === 0 && resp.data) {
|
||||
const rows = resp.data.list || []
|
||||
total.value = resp.data.total || 0
|
||||
customerList.value = rows.map((m) => ({
|
||||
id: m.id,
|
||||
name: m.supplier_name || m.name || '',
|
||||
contact: m.contact_person || m.contact || '',
|
||||
phone: m.contact_phone || m.phone || '',
|
||||
email: m.contact_email || m.email || '',
|
||||
address: m.address || '',
|
||||
status: typeof m.status === 'number' ? m.status : (m.status === '0' ? 0 : 1),
|
||||
_raw: m,
|
||||
}))
|
||||
} else {
|
||||
customerList.value = []
|
||||
total.value = 0
|
||||
if (resp && resp.message) ElMessage.error(resp.message)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
currentRow.value = null;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(row) {
|
||||
isEdit.value = true;
|
||||
currentRow.value = { ...row };
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleView(row) {
|
||||
// 拉取最新详情再展示
|
||||
getSupplier(row.id).then((res) => {
|
||||
const resp = res && res.data ? res.data : res
|
||||
if (resp && resp.code === 0 && resp.data) {
|
||||
const m = resp.data
|
||||
currentRow.value = {
|
||||
id: m.id,
|
||||
name: m.supplier_name || m.name || '',
|
||||
contact: m.contact_person || m.contact || '',
|
||||
phone: m.contact_phone || m.phone || '',
|
||||
email: m.contact_email || m.email || '',
|
||||
address: m.address || '',
|
||||
status: typeof m.status === 'number' ? m.status : (m.status === '0' ? 0 : 1),
|
||||
_raw: m,
|
||||
}
|
||||
detailVisible.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleDelete(row) {
|
||||
ElMessageBox.confirm('确定要删除该供应商吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
deleteSupplier(row.id)
|
||||
.then((res) => {
|
||||
const resp = res && res.data ? res.data : res
|
||||
if (resp && resp.code === 0) {
|
||||
ElMessage.success('删除成功');
|
||||
fetchCustomerList();
|
||||
} else {
|
||||
ElMessage.error((resp && resp.message) || '删除失败')
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function onSaved() {
|
||||
fetchCustomerList();
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
currentPage.value = 1;
|
||||
fetchCustomerList();
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
searchQuery.value = '';
|
||||
currentPage.value = 1;
|
||||
fetchCustomerList();
|
||||
}
|
||||
|
||||
function handleSizeChange() {
|
||||
fetchCustomerList();
|
||||
}
|
||||
|
||||
function handleCurrentChange() {
|
||||
fetchCustomerList();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchCustomerList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.crm-customer {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
background: #f5f7fa;
|
||||
|
||||
.customer-container {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.search-bar {
|
||||
margin-left: auto;
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
2460
pc/src/views/apps/crm/workbench/index.vue
Normal file
2460
pc/src/views/apps/crm/workbench/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -85,6 +85,18 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="IsShow" label="是否显示" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.IsShow"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="handleShowChange(scope.row)"
|
||||
:disabled="!scope.row.Id"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="280" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
@ -185,6 +197,14 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="是否显示" prop="IsShow">
|
||||
<el-switch
|
||||
v-model="currentMenu.IsShow"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="权限标识" prop="Permission">
|
||||
<el-input
|
||||
v-model="currentMenu.Permission"
|
||||
@ -223,6 +243,7 @@ interface Menu {
|
||||
ExternalUrl: string;
|
||||
MenuType: 1 | 2 | 3; // 1:普通菜单 2:分组菜单 3:按钮菜单
|
||||
Permission: string;
|
||||
IsShow: 0 | 1;
|
||||
CreateTime: string;
|
||||
UpdateTime: string;
|
||||
children?: Menu[];
|
||||
@ -255,6 +276,7 @@ const currentMenu = ref<Partial<Menu>>({
|
||||
ExternalUrl: "",
|
||||
MenuType: 1,
|
||||
Permission: "",
|
||||
IsShow: 1,
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
@ -303,14 +325,15 @@ const fetchMenus = async () => {
|
||||
ParentId: item.parentId,
|
||||
Icon: item.icon,
|
||||
Order: item.order,
|
||||
Status: 1,
|
||||
Status: item.status ?? 1,
|
||||
ComponentPath: item.componentPath || "",
|
||||
IsExternal: item.isExternal || 0,
|
||||
ExternalUrl: item.externalUrl || "",
|
||||
MenuType: item.menuType,
|
||||
Permission: item.permission || "",
|
||||
CreateTime: "",
|
||||
UpdateTime: "",
|
||||
MenuType: item.menuType,
|
||||
Permission: item.permission || "",
|
||||
IsShow: item.isShow ?? 1,
|
||||
CreateTime: "",
|
||||
UpdateTime: "",
|
||||
}));
|
||||
|
||||
// 按照参数Order的大小从小到大排序
|
||||
@ -447,25 +470,19 @@ const handleStatusChange = async (menu: Menu) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 添加顶级菜单
|
||||
const handleAddMenu = () => {
|
||||
dialogTitle.value = "添加菜单";
|
||||
currentMenu.value = {
|
||||
Id: 0,
|
||||
Name: "",
|
||||
Path: "",
|
||||
ParentId: 0,
|
||||
Icon: "",
|
||||
Order: 0,
|
||||
Status: 1,
|
||||
ComponentPath: "",
|
||||
IsExternal: 0,
|
||||
ExternalUrl: "",
|
||||
MenuType: 1,
|
||||
Permission: "",
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
// 处理是否显示变更
|
||||
const handleShowChange = async (menu: Menu) => {
|
||||
try {
|
||||
const result = await updateMenu(menu.Id, { IsShow: menu.IsShow } as any)
|
||||
if (!result.success) {
|
||||
ElMessage.error(result.message)
|
||||
menu.IsShow = menu.IsShow === 1 ? 0 : 1
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('更新是否显示失败: ' + (error as Error).message)
|
||||
menu.IsShow = menu.IsShow === 1 ? 0 : 1
|
||||
}
|
||||
}
|
||||
|
||||
// 添加子菜单
|
||||
const handleAddSubMenu = (parentMenu: Menu) => {
|
||||
|
||||
90
server/controllers/contact.go
Normal file
90
server/controllers/contact.go
Normal file
@ -0,0 +1,90 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"server/models"
|
||||
"server/services"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
type ContactController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// List GET /api/crm/contact/list?tenant_id=&related_type=&related_id=
|
||||
func (c *ContactController) List() {
|
||||
tenantId := c.GetString("tenant_id")
|
||||
relatedType, _ := strconv.Atoi(c.GetString("related_type"))
|
||||
relatedId := c.GetString("related_id")
|
||||
if tenantId == "" || relatedType == 0 || relatedId == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "缺少必要参数"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
list, err := services.ListContacts(tenantId, relatedType, relatedId)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": err.Error()}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok", "data": list}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// Add POST /api/crm/contact/add
|
||||
func (c *ContactController) Add() {
|
||||
var m models.Contact
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &m); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "请求参数格式错误"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if err := services.CreateContact(&m); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": err.Error()}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok", "data": m}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// Edit POST /api/crm/contact/edit
|
||||
func (c *ContactController) Edit() {
|
||||
var m models.Contact
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &m); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "请求参数格式错误"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if m.Id == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "id不能为空"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if err := services.UpdateContact(&m); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": err.Error()}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok"}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete POST /api/crm/contact/delete
|
||||
func (c *ContactController) Delete() {
|
||||
var body struct {
|
||||
Id int64 `json:"id"`
|
||||
TenantId string `json:"tenant_id"`
|
||||
}
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &body); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "请求参数格式错误"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if err := services.DeleteContact(body.Id, body.TenantId); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": err.Error()}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok"}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
155
server/controllers/customer.go
Normal file
155
server/controllers/customer.go
Normal file
@ -0,0 +1,155 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"server/models"
|
||||
"server/services"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
type CustomerController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// List GET /api/crm/customer/list
|
||||
func (c *CustomerController) List() {
|
||||
tenantId := c.GetString("tenantId")
|
||||
keyword := c.GetString("keyword")
|
||||
status := c.GetString("status")
|
||||
page, _ := strconv.Atoi(c.GetString("page"))
|
||||
pageSize, _ := strconv.Atoi(c.GetString("pageSize"))
|
||||
list, total, err := services.ListCustomers(tenantId, keyword, status, page, pageSize)
|
||||
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": err.Error()}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok", "data": map[string]interface{}{"list": list, "total": total}}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// Detail GET /api/crm/customer/detail?id=...
|
||||
func (c *CustomerController) Detail() {
|
||||
id := c.GetString("id")
|
||||
if id == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "id不能为空"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
m, err := services.GetCustomer(id)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": err.Error()}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok", "data": m}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// Add POST /api/crm/customer/add
|
||||
func (c *CustomerController) Add() {
|
||||
var m models.Customer
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &m); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "请求参数格式错误"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if err := services.CreateCustomer(&m); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": err.Error()}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok", "data": m}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// Edit POST /api/crm/customer/edit body: {id, ...}
|
||||
func (c *CustomerController) Edit() {
|
||||
var body map[string]interface{}
|
||||
_ = json.Unmarshal(c.Ctx.Input.RequestBody, &body)
|
||||
id, _ := body["id"].(string)
|
||||
if id == "" {
|
||||
// 也允许前端直接在JSON中传 id 字段,下面会再从结构体取
|
||||
}
|
||||
var m models.Customer
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &m); err != nil {
|
||||
// 回退:从通用map中提取已知字段,避免类型不匹配导致失败
|
||||
if body == nil {
|
||||
_ = json.Unmarshal(c.Ctx.Input.RequestBody, &body)
|
||||
}
|
||||
toStr := func(v interface{}) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return ""
|
||||
case string:
|
||||
return t
|
||||
case json.Number:
|
||||
return t.String()
|
||||
default:
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
}
|
||||
m = models.Customer{
|
||||
Id: toStr(body["id"]),
|
||||
TenantId: toStr(body["tenant_id"]),
|
||||
CustomerName: toStr(body["customer_name"]),
|
||||
CustomerType: toStr(body["customer_type"]),
|
||||
ContactPerson: toStr(body["contact_person"]),
|
||||
ContactPhone: toStr(body["contact_phone"]),
|
||||
ContactEmail: toStr(body["contact_email"]),
|
||||
CustomerLevel: toStr(body["customer_level"]),
|
||||
Industry: toStr(body["industry"]),
|
||||
Address: toStr(body["address"]),
|
||||
Status: toStr(body["status"]),
|
||||
Remark: toStr(body["remark"]),
|
||||
}
|
||||
if m.Id == "" {
|
||||
if id == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "id不能为空"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
m.Id = id
|
||||
}
|
||||
}
|
||||
if m.Id == "" {
|
||||
if id == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "id不能为空"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
m.Id = id
|
||||
}
|
||||
if err := services.UpdateCustomer(&m); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": err.Error()}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok"}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// Delete POST /api/crm/customer/delete body: {id, tenantId}
|
||||
func (c *CustomerController) Delete() {
|
||||
var body struct {
|
||||
Id string `json:"id"`
|
||||
TenantId string `json:"tenantId"`
|
||||
}
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &body); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "请求参数格式错误"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if body.Id == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "id不能为空"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if err := services.SoftDeleteCustomer(body.Id); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": err.Error()}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok"}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
@ -123,9 +123,9 @@ func (c *MenuController) UpdateMenu() {
|
||||
return
|
||||
}
|
||||
|
||||
// 直接使用 Beego 的 BindJSON 方法
|
||||
var menu models.Menu
|
||||
if err := c.BindJSON(&menu); err != nil {
|
||||
// 解析请求体为通用map,进行部分字段更新,避免未提供字段被置零
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &body); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "请求参数错误",
|
||||
@ -135,11 +135,40 @@ func (c *MenuController) UpdateMenu() {
|
||||
return
|
||||
}
|
||||
|
||||
// 设置 ID
|
||||
menu.Id = id
|
||||
// 允许更新的字段映射:前端Key -> 数据库列名
|
||||
allowed := map[string]string{
|
||||
"Name": "name",
|
||||
"Path": "path",
|
||||
"ParentId": "parent_id",
|
||||
"Icon": "icon",
|
||||
"Order": "order",
|
||||
"Status": "status",
|
||||
"ComponentPath": "component_path",
|
||||
"IsExternal": "is_external",
|
||||
"ExternalUrl": "external_url",
|
||||
"MenuType": "menu_type",
|
||||
"Permission": "permission",
|
||||
"IsShow": "is_show",
|
||||
}
|
||||
|
||||
// 更新菜单
|
||||
if err := models.UpdateMenu(&menu); err != nil {
|
||||
params := orm.Params{}
|
||||
for k, col := range allowed {
|
||||
if v, ok := body[k]; ok {
|
||||
params[col] = v
|
||||
}
|
||||
}
|
||||
|
||||
if len(params) == 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "无可更新的字段",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
o := orm.NewOrm()
|
||||
if _, err := o.QueryTable("yz_menus").Filter("id", id).Update(params); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "更新菜单失败",
|
||||
@ -149,7 +178,6 @@ func (c *MenuController) UpdateMenu() {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "更新菜单成功",
|
||||
// "data": menu,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
110
server/controllers/supplier.go
Normal file
110
server/controllers/supplier.go
Normal file
@ -0,0 +1,110 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"server/models"
|
||||
"server/services"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
type SupplierController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *SupplierController) List() {
|
||||
tenantId := c.GetString("tenantId")
|
||||
keyword := c.GetString("keyword")
|
||||
status := c.GetString("status")
|
||||
page, _ := strconv.Atoi(c.GetString("page"))
|
||||
pageSize, _ := strconv.Atoi(c.GetString("pageSize"))
|
||||
list, total, err := services.ListSuppliers(tenantId, keyword, status, page, pageSize)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": err.Error()}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok", "data": map[string]interface{}{"list": list, "total": total}}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *SupplierController) Detail() {
|
||||
id := c.GetString("id")
|
||||
if id == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "id不能为空"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
m, err := services.GetSupplier(id)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": err.Error()}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok", "data": m}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *SupplierController) Add() {
|
||||
var m models.Supplier
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &m); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "请求参数格式错误"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if err := services.CreateSupplier(&m); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": err.Error()}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok", "data": m}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *SupplierController) Edit() {
|
||||
var body map[string]interface{}
|
||||
_ = json.Unmarshal(c.Ctx.Input.RequestBody, &body)
|
||||
id, _ := body["id"].(string)
|
||||
var m models.Supplier
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &m); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "请求参数格式错误"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if m.Id == "" {
|
||||
if id == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "id不能为空"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
m.Id = id
|
||||
}
|
||||
if err := services.UpdateSupplier(&m); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": err.Error()}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok"}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *SupplierController) Delete() {
|
||||
var body struct {
|
||||
Id string `json:"id"`
|
||||
TenantId string `json:"tenantId"`
|
||||
}
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &body); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "请求参数格式错误"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if body.Id == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": "id不能为空"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if err := services.SoftDeleteSupplier(body.Id); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 1, "message": err.Error()}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{"code": 0, "message": "ok"}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
24
server/database/yz_tenant_crm_contact.sql
Normal file
24
server/database/yz_tenant_crm_contact.sql
Normal file
@ -0,0 +1,24 @@
|
||||
CREATE TABLE yz_tenant_crm_contact (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
|
||||
tenant_id BIGINT NOT NULL COMMENT '租户ID(关联租户表,隔离数据)',
|
||||
related_type TINYINT NOT NULL COMMENT '关联类型:1=客户,2=供应商(区分联系人归属)',
|
||||
related_id BIGINT NOT NULL COMMENT '关联ID(关联yz_tenant_crm_customer.id或yz_tenant_crm_supplier.id)',
|
||||
contact_name VARCHAR(50) NOT NULL COMMENT '联系人姓名',
|
||||
gender TINYINT DEFAULT 0 COMMENT '性别:0=未知,1=男,2=女',
|
||||
mobile VARCHAR(20) COMMENT '手机号(唯一索引,避免重复)',
|
||||
phone VARCHAR(20) COMMENT '固定电话',
|
||||
email VARCHAR(100) COMMENT '邮箱',
|
||||
position VARCHAR(50) COMMENT '职位(如:项目经理、采购负责人)',
|
||||
department VARCHAR(50) COMMENT '所属部门',
|
||||
is_primary TINYINT DEFAULT 0 COMMENT '是否主联系人:0=否,1=是(一个客户/供应商可设一个主联系人)',
|
||||
remark VARCHAR(500) COMMENT '备注(如:关键决策人、对接优先级等)',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
create_by BIGINT COMMENT '创建人ID(关联用户表)',
|
||||
update_by BIGINT COMMENT '更新人ID(关联用户表)',
|
||||
is_deleted TINYINT DEFAULT 0 COMMENT '逻辑删除:0=正常,1=删除',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_tenant_mobile (tenant_id, mobile) COMMENT '同一租户内手机号唯一',
|
||||
KEY idx_tenant_related (tenant_id, related_type, related_id) COMMENT '查询租户下某客户/供应商的所有联系人',
|
||||
KEY idx_contact_name (tenant_id, contact_name) COMMENT '按姓名模糊查询联系人'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户CRM联系人表';
|
||||
26
server/database/yz_tenant_crm_customer.sql
Normal file
26
server/database/yz_tenant_crm_customer.sql
Normal file
@ -0,0 +1,26 @@
|
||||
-- 客户管理表:yz_tenant_crm_customer(支持租户隔离、软删除、时间追踪)
|
||||
CREATE TABLE `yz_tenant_crm_customer` (
|
||||
`id` varchar(36) NOT NULL COMMENT 'ID',
|
||||
`tenant_id` varchar(64) NOT NULL COMMENT '租户ID',
|
||||
`customer_name` varchar(100) NOT NULL COMMENT '客户名称(企业/个人名称)',
|
||||
`customer_type` varchar(20) NOT NULL COMMENT '客户类型',
|
||||
`contact_person` varchar(50) NOT NULL COMMENT '联系人姓名',
|
||||
`contact_phone` varchar(20) NOT NULL COMMENT '联系人电话',
|
||||
`contact_email` varchar(100) DEFAULT '' COMMENT '联系人邮箱',
|
||||
`customer_level` varchar(20) DEFAULT '3' COMMENT '客户等级(1-核心客户/2-重要客户/3-普通客户/4-潜在客户)',
|
||||
`industry` varchar(50) DEFAULT '' COMMENT '所属行业',
|
||||
`address` varchar(255) DEFAULT '' COMMENT '客户地址',
|
||||
`register_time` date DEFAULT NULL COMMENT '客户注册/合作起始日期',
|
||||
`expire_time` date DEFAULT NULL COMMENT '合作到期日期',
|
||||
`status` varchar(20) NOT NULL DEFAULT '1' COMMENT '客户状态(0-禁用/1-正常/2-冻结/3-已注销)',
|
||||
`remark` text COMMENT '客户备注',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_id` (`tenant_id`) COMMENT '租户ID索引,优化多租户隔离查询',
|
||||
KEY `idx_customer_name` (`customer_name`) COMMENT '客户名称索引,优化按名称模糊查询',
|
||||
KEY `idx_contact_phone` (`contact_phone`) COMMENT '联系人电话索引,优化按电话精准查询',
|
||||
KEY `idx_status` (`status`) COMMENT '客户状态索引,优化按状态筛选(如:查询正常客户)',
|
||||
KEY `idx_register_time` (`register_time`) COMMENT '注册时间索引,优化按合作时间范围查询'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户管理表(支持租户隔离、软删除、客户全生命周期追踪)';
|
||||
26
server/database/yz_tenant_crm_supplier.sql
Normal file
26
server/database/yz_tenant_crm_supplier.sql
Normal file
@ -0,0 +1,26 @@
|
||||
-- 客户管理表:yz_tenant_crm_supplier(支持租户隔离、软删除、时间追踪)
|
||||
CREATE TABLE `yz_tenant_crm_supplier` (
|
||||
`id` varchar(36) NOT NULL COMMENT 'ID',
|
||||
`tenant_id` varchar(64) NOT NULL COMMENT '租户ID',
|
||||
`supplier_name` varchar(100) NOT NULL COMMENT '供应商名称(企业/个人名称)',
|
||||
`supplier_type` varchar(20) NOT NULL COMMENT '供应商类型',
|
||||
`contact_person` varchar(50) NOT NULL COMMENT '联系人姓名',
|
||||
`contact_phone` varchar(20) NOT NULL COMMENT '联系人电话',
|
||||
`contact_email` varchar(100) DEFAULT '' COMMENT '联系人邮箱',
|
||||
`supplier_level` varchar(20) DEFAULT '3' COMMENT '供应商等级(1-核心供应商/2-重要供应商/3-普通供应商/4-潜在供应商)',
|
||||
`industry` varchar(50) DEFAULT '' COMMENT '所属行业',
|
||||
`address` varchar(255) DEFAULT '' COMMENT '供应商地址',
|
||||
`register_time` date DEFAULT NULL COMMENT '供应商注册/合作起始日期',
|
||||
`expire_time` date DEFAULT NULL COMMENT '合作到期日期',
|
||||
`status` varchar(20) NOT NULL DEFAULT '1' COMMENT '供应商状态(0-禁用/1-正常/2-冻结/3-已注销)',
|
||||
`remark` text COMMENT '供应商备注',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`delete_time` datetime DEFAULT NULL COMMENT '删除时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_id` (`tenant_id`) COMMENT '租户ID索引,优化多租户隔离查询',
|
||||
KEY `idx_supplier_name` (`supplier_name`) COMMENT '供应商名称索引,优化按名称模糊查询',
|
||||
KEY `idx_contact_phone` (`contact_phone`) COMMENT '联系人电话索引,优化按电话精准查询',
|
||||
KEY `idx_status` (`status`) COMMENT '供应商状态索引,优化按状态筛选',
|
||||
KEY `idx_register_time` (`register_time`) COMMENT '注册时间索引,优化按合作时间范围查询'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商管理表';
|
||||
37
server/models/contact.go
Normal file
37
server/models/contact.go
Normal file
@ -0,0 +1,37 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// Contact 联系人模型(对应表 yz_tenant_crm_contact)
|
||||
type Contact struct {
|
||||
Id int64 `orm:"pk;auto" json:"id"`
|
||||
TenantId string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
RelatedType int `orm:"column(related_type)" json:"related_type"` // 1=客户 2=供应商
|
||||
RelatedId string `orm:"column(related_id);size(64)" json:"related_id"`
|
||||
ContactName string `orm:"column(contact_name);size(50)" json:"contact_name"`
|
||||
Gender int8 `orm:"column(gender);null" json:"gender"`
|
||||
Mobile string `orm:"column(mobile);size(20);null" json:"mobile"`
|
||||
Phone string `orm:"column(phone);size(20);null" json:"phone"`
|
||||
Email string `orm:"column(email);size(100);null" json:"email"`
|
||||
Position string `orm:"column(position);size(50);null" json:"position"`
|
||||
Department string `orm:"column(department);size(50);null" json:"department"`
|
||||
IsPrimary int8 `orm:"column(is_primary);null" json:"is_primary"`
|
||||
Remark string `orm:"column(remark);size(500);null" json:"remark"`
|
||||
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);type(datetime);auto_now" json:"update_time"`
|
||||
CreateBy int64 `orm:"column(create_by);null" json:"create_by"`
|
||||
UpdateBy int64 `orm:"column(update_by);null" json:"update_by"`
|
||||
IsDeleted int8 `orm:"column(is_deleted);null" json:"is_deleted"`
|
||||
}
|
||||
|
||||
func (t *Contact) TableName() string {
|
||||
return "yz_tenant_crm_contact"
|
||||
}
|
||||
|
||||
func init() {
|
||||
orm.RegisterModel(new(Contact))
|
||||
}
|
||||
36
server/models/customer.go
Normal file
36
server/models/customer.go
Normal file
@ -0,0 +1,36 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// Customer 客户模型(对应表 yz_tenant_crm_customer)
|
||||
type Customer struct {
|
||||
Id string `orm:"pk;size(36)" json:"id"`
|
||||
TenantId string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
CustomerName string `orm:"column(customer_name);size(100)" json:"customer_name"`
|
||||
CustomerType string `orm:"column(customer_type);size(20)" json:"customer_type"`
|
||||
ContactPerson string `orm:"column(contact_person);size(50)" json:"contact_person"`
|
||||
ContactPhone string `orm:"column(contact_phone);size(20)" json:"contact_phone"`
|
||||
ContactEmail string `orm:"column(contact_email);size(100);null" json:"contact_email"`
|
||||
CustomerLevel string `orm:"column(customer_level);size(20);null" json:"customer_level"`
|
||||
Industry string `orm:"column(industry);size(50);null" json:"industry"`
|
||||
Address string `orm:"column(address);size(255);null" json:"address"`
|
||||
RegisterTime *time.Time `orm:"column(register_time);null;type(date)" json:"register_time"`
|
||||
ExpireTime *time.Time `orm:"column(expire_time);null;type(date)" json:"expire_time"`
|
||||
Status string `orm:"column(status);size(20)" json:"status"`
|
||||
Remark string `orm:"column(remark);type(text);null" json:"remark"`
|
||||
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);type(datetime);auto_now" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (t *Customer) TableName() string {
|
||||
return "yz_tenant_crm_customer"
|
||||
}
|
||||
|
||||
func init() {
|
||||
orm.RegisterModel(new(Customer))
|
||||
}
|
||||
@ -21,6 +21,7 @@ type Menu struct {
|
||||
ExternalUrl string `orm:"size(1000);null"`
|
||||
MenuType int8 `orm:"default(1)"`
|
||||
Permission string `orm:"size(200);null"`
|
||||
IsShow int8 `orm:"default(1)"`
|
||||
CreateTime time.Time `orm:"auto_now_add;type(datetime)"`
|
||||
UpdateTime time.Time `orm:"auto_now;type(datetime)"`
|
||||
DeleteTime *time.Time `orm:"null;type(datetime)"`
|
||||
@ -53,6 +54,8 @@ func GetAllMenus() ([]map[string]interface{}, error) {
|
||||
"isExternal": m.IsExternal,
|
||||
"externalUrl": m.ExternalUrl,
|
||||
"menuType": m.MenuType,
|
||||
"permission": m.Permission,
|
||||
"isShow": m.IsShow,
|
||||
}
|
||||
result = append(result, item)
|
||||
}
|
||||
@ -129,11 +132,12 @@ func GetTenantMenus(roleId int) ([]map[string]interface{}, error) {
|
||||
args[len(menuIds)] = 1 // menu_type=1 表示页面菜单
|
||||
|
||||
// 3. 查询菜单(只返回menu_type=1的页面菜单,且未删除的)
|
||||
query := "SELECT id, name, path, parent_id, icon, `order`, status, component_path, is_external, external_url, menu_type, permission " +
|
||||
query := "SELECT id, name, path, parent_id, icon, `order`, status, component_path, is_external, external_url, menu_type, permission, is_show " +
|
||||
"FROM yz_menus " +
|
||||
"WHERE id IN (" + strings.Join(placeholders, ",") + ") " +
|
||||
"AND delete_time IS NULL " +
|
||||
"AND menu_type = ? " +
|
||||
"AND is_show = 1 " +
|
||||
"ORDER BY `order`, id"
|
||||
|
||||
var menus []*Menu
|
||||
@ -184,11 +188,12 @@ func GetTenantMenus(roleId int) ([]map[string]interface{}, error) {
|
||||
}
|
||||
parentArgs[len(parentIdList)] = 1 // menu_type=1
|
||||
|
||||
parentQuery := "SELECT id, name, path, parent_id, icon, `order`, status, component_path, is_external, external_url, menu_type, permission " +
|
||||
parentQuery := "SELECT id, name, path, parent_id, icon, `order`, status, component_path, is_external, external_url, menu_type, permission, is_show " +
|
||||
"FROM yz_menus " +
|
||||
"WHERE id IN (" + strings.Join(parentPlaceholders, ",") + ") " +
|
||||
"AND delete_time IS NULL " +
|
||||
"AND menu_type = ? " +
|
||||
"AND is_show = 1 " +
|
||||
"ORDER BY `order`, id"
|
||||
|
||||
var parentMenus []*Menu
|
||||
@ -224,6 +229,7 @@ func GetTenantMenus(roleId int) ([]map[string]interface{}, error) {
|
||||
"externalUrl": m.ExternalUrl,
|
||||
"menuType": m.MenuType,
|
||||
"permission": m.Permission,
|
||||
"isShow": m.IsShow,
|
||||
}
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
36
server/models/supplier.go
Normal file
36
server/models/supplier.go
Normal file
@ -0,0 +1,36 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// Supplier 供应商模型(对应表 yz_tenant_crm_supplier)
|
||||
type Supplier struct {
|
||||
Id string `orm:"pk;size(36)" json:"id"`
|
||||
TenantId string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
SupplierName string `orm:"column(supplier_name);size(100)" json:"supplier_name"`
|
||||
SupplierType string `orm:"column(supplier_type);size(20)" json:"supplier_type"`
|
||||
ContactPerson string `orm:"column(contact_person);size(50)" json:"contact_person"`
|
||||
ContactPhone string `orm:"column(contact_phone);size(20)" json:"contact_phone"`
|
||||
ContactEmail string `orm:"column(contact_email);size(100);null" json:"contact_email"`
|
||||
SupplierLevel string `orm:"column(supplier_level);size(20);null" json:"supplier_level"`
|
||||
Industry string `orm:"column(industry);size(50);null" json:"industry"`
|
||||
Address string `orm:"column(address);size(255);null" json:"address"`
|
||||
RegisterTime *time.Time `orm:"column(register_time);null;type(date)" json:"register_time"`
|
||||
ExpireTime *time.Time `orm:"column(expire_time);null;type(date)" json:"expire_time"`
|
||||
Status string `orm:"column(status);size(20)" json:"status"`
|
||||
Remark string `orm:"column(remark);type(text);null" json:"remark"`
|
||||
CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);type(datetime);auto_now" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (t *Supplier) TableName() string {
|
||||
return "yz_tenant_crm_supplier"
|
||||
}
|
||||
|
||||
func init() {
|
||||
orm.RegisterModel(new(Supplier))
|
||||
}
|
||||
@ -314,10 +314,30 @@ func init() {
|
||||
// OA基础数据合并接口(一次性获取部门、职位、角色)
|
||||
beego.Router("/api/oa/base-data/:tenantId", &controllers.OAController{}, "get:GetOABaseData")
|
||||
|
||||
// OA任务管理路由
|
||||
beego.Router("/api/oa/tasks", &controllers.TaskController{}, "get:GetTasks;post:CreateTask")
|
||||
beego.Router("/api/oa/tasks/:id", &controllers.TaskController{}, "get:GetTaskById;put:UpdateTask;delete:DeleteTask")
|
||||
beego.Router("/api/oa/tasks/todo", &controllers.TaskController{}, "get:GetTodoTasks")
|
||||
// OA任务管理路由
|
||||
beego.Router("/api/oa/tasks", &controllers.TaskController{}, "get:GetTasks;post:CreateTask")
|
||||
beego.Router("/api/oa/tasks/:id", &controllers.TaskController{}, "get:GetTaskById;put:UpdateTask;delete:DeleteTask")
|
||||
beego.Router("/api/oa/tasks/todo", &controllers.TaskController{}, "get:GetTodoTasks")
|
||||
|
||||
// CRM 客户路由
|
||||
beego.Router("/api/crm/customer/list", &controllers.CustomerController{}, "get:List")
|
||||
beego.Router("/api/crm/customer/detail", &controllers.CustomerController{}, "get:Detail")
|
||||
beego.Router("/api/crm/customer/add", &controllers.CustomerController{}, "post:Add")
|
||||
beego.Router("/api/crm/customer/edit", &controllers.CustomerController{}, "post:Edit")
|
||||
beego.Router("/api/crm/customer/delete", &controllers.CustomerController{}, "post:Delete")
|
||||
|
||||
// CRM 供应商路由
|
||||
beego.Router("/api/crm/supplier/list", &controllers.SupplierController{}, "get:List")
|
||||
beego.Router("/api/crm/supplier/detail", &controllers.SupplierController{}, "get:Detail")
|
||||
beego.Router("/api/crm/supplier/add", &controllers.SupplierController{}, "post:Add")
|
||||
beego.Router("/api/crm/supplier/edit", &controllers.SupplierController{}, "post:Edit")
|
||||
beego.Router("/api/crm/supplier/delete", &controllers.SupplierController{}, "post:Delete")
|
||||
|
||||
// CRM 联系人路由
|
||||
beego.Router("/api/crm/contact/list", &controllers.ContactController{}, "get:List")
|
||||
beego.Router("/api/crm/contact/add", &controllers.ContactController{}, "post:Add")
|
||||
beego.Router("/api/crm/contact/edit", &controllers.ContactController{}, "post:Edit")
|
||||
beego.Router("/api/crm/contact/delete", &controllers.ContactController{}, "post:Delete")
|
||||
|
||||
// 权限管理路由
|
||||
beego.Router("/api/permissions/menus", &controllers.PermissionController{}, "get:GetAllMenuPermissions")
|
||||
|
||||
64
server/services/contact.go
Normal file
64
server/services/contact.go
Normal file
@ -0,0 +1,64 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
"server/models"
|
||||
)
|
||||
|
||||
func ListContacts(tenantId string, relatedType int, relatedId string) ([]models.Contact, error) {
|
||||
o := orm.NewOrm()
|
||||
var list []models.Contact
|
||||
qs := o.QueryTable(new(models.Contact)).Filter("tenant_id", tenantId).Filter("related_type", relatedType).Filter("related_id", relatedId).Filter("is_deleted", 0)
|
||||
_, err := qs.All(&list)
|
||||
return list, err
|
||||
}
|
||||
|
||||
func CreateContact(m *models.Contact) error {
|
||||
if m.TenantId == "" || m.RelatedType == 0 || m.RelatedId == "" || m.ContactName == "" {
|
||||
return errors.New("缺少必填参数")
|
||||
}
|
||||
o := orm.NewOrm()
|
||||
// 若设为主联系人,则先清理同归属其他主联系人
|
||||
if m.IsPrimary == 1 {
|
||||
_, _ = o.QueryTable(new(models.Contact)).Filter("tenant_id", m.TenantId).Filter("related_type", m.RelatedType).Filter("related_id", m.RelatedId).Filter("is_deleted", 0).Update(orm.Params{"is_primary": 0})
|
||||
}
|
||||
_, err := o.Insert(m)
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateContact(m *models.Contact) error {
|
||||
if m.Id == 0 {
|
||||
return errors.New("id不能为空")
|
||||
}
|
||||
o := orm.NewOrm()
|
||||
// 若设为主联系人,则先清理同归属其他主联系人
|
||||
if m.IsPrimary == 1 {
|
||||
// 需要获取原始记录以得到 tenant/related
|
||||
var origin models.Contact
|
||||
origin.Id = m.Id
|
||||
if err := o.Read(&origin); err == nil {
|
||||
_, _ = o.QueryTable(new(models.Contact)).Filter("tenant_id", origin.TenantId).Filter("related_type", origin.RelatedType).Filter("related_id", origin.RelatedId).Filter("id__ne", m.Id).Filter("is_deleted", 0).Update(orm.Params{"is_primary": 0})
|
||||
}
|
||||
}
|
||||
_, err := o.Update(m, "contact_name", "gender", "mobile", "phone", "email", "position", "department", "is_primary", "remark")
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteContact(id int64, tenantId string) error {
|
||||
if id == 0 {
|
||||
return errors.New("id不能为空")
|
||||
}
|
||||
o := orm.NewOrm()
|
||||
m := models.Contact{Id: id}
|
||||
if err := o.Read(&m); err != nil {
|
||||
return err
|
||||
}
|
||||
if m.TenantId != tenantId {
|
||||
return errors.New("租户不匹配")
|
||||
}
|
||||
m.IsDeleted = 1
|
||||
_, err := o.Update(&m, "is_primary", "is_deleted")
|
||||
return err
|
||||
}
|
||||
79
server/services/customer.go
Normal file
79
server/services/customer.go
Normal file
@ -0,0 +1,79 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// ListCustomers 获取客户列表(可选租户过滤、关键词搜索、状态筛选、分页)
|
||||
func ListCustomers(tenantId, keyword, status string, page, pageSize int) (list []*models.Customer, total int64, err error) {
|
||||
o := orm.NewOrm()
|
||||
qs := o.QueryTable(new(models.Customer)).Filter("delete_time__isnull", true)
|
||||
if tenantId != "" {
|
||||
qs = qs.Filter("tenant_id", tenantId)
|
||||
}
|
||||
if keyword != "" {
|
||||
cond := orm.NewCondition()
|
||||
cond1 := cond.Or("customer_name__icontains", keyword).
|
||||
Or("contact_person__icontains", keyword).
|
||||
Or("contact_phone__icontains", keyword)
|
||||
qs = qs.SetCond(cond1)
|
||||
}
|
||||
if status != "" {
|
||||
qs = qs.Filter("status", status)
|
||||
}
|
||||
|
||||
total, err = qs.Count()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
_, err = qs.OrderBy("-create_time").Limit(pageSize, offset).All(&list)
|
||||
return
|
||||
}
|
||||
|
||||
// GetCustomer 通过ID获取客户
|
||||
func GetCustomer(id string) (*models.Customer, error) {
|
||||
o := orm.NewOrm()
|
||||
m := models.Customer{Id: id}
|
||||
if err := o.Read(&m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
// CreateCustomer 新增客户
|
||||
func CreateCustomer(m *models.Customer) error {
|
||||
o := orm.NewOrm()
|
||||
_, err := o.Insert(m)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateCustomer 更新客户(全量更新)
|
||||
func UpdateCustomer(m *models.Customer, cols ...string) error {
|
||||
o := orm.NewOrm()
|
||||
if len(cols) == 0 {
|
||||
_, err := o.Update(m)
|
||||
return err
|
||||
}
|
||||
_, err := o.Update(m, cols...)
|
||||
return err
|
||||
}
|
||||
|
||||
// SoftDeleteCustomer 软删除客户
|
||||
func SoftDeleteCustomer(id string) error {
|
||||
o := orm.NewOrm()
|
||||
now := time.Now()
|
||||
m := models.Customer{Id: id, DeleteTime: &now}
|
||||
_, err := o.Update(&m, "delete_time")
|
||||
return err
|
||||
}
|
||||
79
server/services/supplier.go
Normal file
79
server/services/supplier.go
Normal file
@ -0,0 +1,79 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// ListSuppliers 获取供应商列表(可选租户过滤、关键词搜索、状态筛选、分页)
|
||||
func ListSuppliers(tenantId, keyword, status string, page, pageSize int) (list []*models.Supplier, total int64, err error) {
|
||||
o := orm.NewOrm()
|
||||
qs := o.QueryTable(new(models.Supplier)).Filter("delete_time__isnull", true)
|
||||
if tenantId != "" {
|
||||
qs = qs.Filter("tenant_id", tenantId)
|
||||
}
|
||||
if keyword != "" {
|
||||
cond := orm.NewCondition()
|
||||
cond1 := cond.Or("supplier_name__icontains", keyword).
|
||||
Or("contact_person__icontains", keyword).
|
||||
Or("contact_phone__icontains", keyword)
|
||||
qs = qs.SetCond(cond1)
|
||||
}
|
||||
if status != "" {
|
||||
qs = qs.Filter("status", status)
|
||||
}
|
||||
|
||||
total, err = qs.Count()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
_, err = qs.OrderBy("-create_time").Limit(pageSize, offset).All(&list)
|
||||
return
|
||||
}
|
||||
|
||||
// GetSupplier 通过ID获取供应商
|
||||
func GetSupplier(id string) (*models.Supplier, error) {
|
||||
o := orm.NewOrm()
|
||||
m := models.Supplier{Id: id}
|
||||
if err := o.Read(&m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
// CreateSupplier 新增供应商
|
||||
func CreateSupplier(m *models.Supplier) error {
|
||||
o := orm.NewOrm()
|
||||
_, err := o.Insert(m)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateSupplier 更新供应商(全量更新)
|
||||
func UpdateSupplier(m *models.Supplier, cols ...string) error {
|
||||
o := orm.NewOrm()
|
||||
if len(cols) == 0 {
|
||||
_, err := o.Update(m)
|
||||
return err
|
||||
}
|
||||
_, err := o.Update(m, cols...)
|
||||
return err
|
||||
}
|
||||
|
||||
// SoftDeleteSupplier 软删除供应商
|
||||
func SoftDeleteSupplier(id string) error {
|
||||
o := orm.NewOrm()
|
||||
now := time.Now()
|
||||
m := models.Supplier{Id: id, DeleteTime: &now}
|
||||
_, err := o.Update(&m, "delete_time")
|
||||
return err
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user