feat: 移动端工作中心新增历史拜访+历史纪要
- 工作中心新增两个卡片入口:历史拜访 / 历史纪要 - VisitsList.vue: 拜访历史记录列表,点击进入编辑 - NotesList.vue: 纪要历史记录列表,点击进入编辑 - 复用已有编辑表单(VisitForm/DailyNoteForm edit mode) - 网格改为3列布局容纳6个卡片 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,8 @@ const router = createRouter({
|
|||||||
{ path: 'key-visit/:id/edit', name: 'KeyVisitEdit', component: () => import('@/views/mobile/KeyVisitForm.vue') },
|
{ path: 'key-visit/:id/edit', name: 'KeyVisitEdit', component: () => import('@/views/mobile/KeyVisitForm.vue') },
|
||||||
{ path: 'note/new', name: 'DailyNoteForm', component: () => import('@/views/mobile/DailyNoteForm.vue') },
|
{ path: 'note/new', name: 'DailyNoteForm', component: () => import('@/views/mobile/DailyNoteForm.vue') },
|
||||||
{ path: 'note/:id/edit', name: 'DailyNoteEdit', component: () => import('@/views/mobile/DailyNoteForm.vue') },
|
{ path: 'note/:id/edit', name: 'DailyNoteEdit', component: () => import('@/views/mobile/DailyNoteForm.vue') },
|
||||||
|
{ path: 'visits', name: 'VisitsList', component: () => import('@/views/mobile/VisitsList.vue') },
|
||||||
|
{ path: 'notes', name: 'NotesList', component: () => import('@/views/mobile/NotesList.vue') },
|
||||||
{ path: 'plans', name: 'PlansList', component: () => import('@/views/mobile/PlansList.vue') },
|
{ path: 'plans', name: 'PlansList', component: () => import('@/views/mobile/PlansList.vue') },
|
||||||
{ path: 'mini-biz', name: 'MiniBizList', component: () => import('@/views/mobile/MiniBizList.vue') },
|
{ path: 'mini-biz', name: 'MiniBizList', component: () => import('@/views/mobile/MiniBizList.vue') },
|
||||||
{ path: 'key-visits', name: 'KeyVisitsList', component: () => import('@/views/mobile/KeyVisitsList.vue') },
|
{ path: 'key-visits', name: 'KeyVisitsList', component: () => import('@/views/mobile/KeyVisitsList.vue') },
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import api from '@/api/index'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const loading = ref(false)
|
||||||
|
const items = ref<any[]>([])
|
||||||
|
|
||||||
|
onMounted(loadItems)
|
||||||
|
|
||||||
|
const catColors: Record<string, string> = {
|
||||||
|
'行政事务': '#1C3738', '合同整理': '#4A6741', '发票处理': '#C4934A',
|
||||||
|
'内部会议': '#B8472E', '培训学习': '#5B7FA5', '其他': '#7B7568',
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadItems() {
|
||||||
|
loading.value = true
|
||||||
|
try { const res = await api.get('/daily-notes/'); items.value = Array.isArray(res.data) ? res.data : (res.data.items || []) }
|
||||||
|
catch (_) {}
|
||||||
|
finally { loading.value = false }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="list-page">
|
||||||
|
<header class="form-editorial-header">
|
||||||
|
<button type="button" class="form-back-btn" @click="router.back()">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
|
||||||
|
</button>
|
||||||
|
<div class="form-title-group"><h2 class="form-title">历史纪要</h2><span class="form-subtitle">NOTE HISTORY</span></div>
|
||||||
|
<div class="form-rule"></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div v-loading="loading" class="cards">
|
||||||
|
<div v-if="items.length === 0 && !loading" class="empty-state"><div class="empty-glyph">—</div><p class="empty-text">暂无纪要记录</p></div>
|
||||||
|
<article v-for="item in items" :key="item.id" class="card" @click="router.push(`/m/note/${item.id}/edit`)">
|
||||||
|
<div class="card-accent" :style="{ background: catColors[item.category] || '#7B7568' }"></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-cat" :style="{ color: catColors[item.category] || '#7B7568' }">{{ item.category }}</span>
|
||||||
|
<span class="card-date">{{ item.note_date }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">{{ item.content || '暂无内容' }}</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<span v-if="item.time_range" class="card-time">{{ item.time_range }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.list-page { max-width: 100%; padding-bottom: 20px; }
|
||||||
|
.form-editorial-header { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 20px; flex-wrap: wrap; }
|
||||||
|
.form-back-btn { background: var(--surface); border: 1px solid var(--warm-border); padding: 8px 10px; cursor: pointer; color: var(--warm-gray); display: flex; align-items: center; transition: all var(--transition); flex-shrink: 0; margin-top: 2px; }
|
||||||
|
.form-back-btn:hover { color: var(--ink); border-color: var(--ink); }
|
||||||
|
.form-title-group { display: flex; flex-direction: column; gap: 0; flex: 1; }
|
||||||
|
.form-title { margin: 0; font-family: var(--font-heading); font-size: 24px; font-weight: 400; color: var(--ink); letter-spacing: 0.06em; line-height: 1.3; }
|
||||||
|
.form-subtitle { font-family: var(--font-mono); font-size: 9px; color: var(--gold); letter-spacing: 0.2em; }
|
||||||
|
.form-rule { width: 100%; height: 2px; background: var(--warm-border); margin-top: 6px; position: relative; }
|
||||||
|
.form-rule::after { content: ''; position: absolute; left: 0; top: 0; width: 32px; height: 2px; background: var(--gold); }
|
||||||
|
.cards { display: flex; flex-direction: column; gap: 10px; }
|
||||||
|
.card { background: var(--surface); border: 1px solid var(--warm-border); display: flex; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; }
|
||||||
|
.card:active { transform: scale(0.99); }
|
||||||
|
.card:hover { box-shadow: 0 4px 12px rgba(28,55,56,0.06); }
|
||||||
|
.card-accent { width: 4px; flex-shrink: 0; }
|
||||||
|
.card-body { flex: 1; padding: 14px 16px; min-width: 0; }
|
||||||
|
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
||||||
|
.card-cat { font-family: var(--font-heading); font-size: 13px; }
|
||||||
|
.card-date { font-family: var(--font-mono); font-size: 12px; color: var(--warm-gray); }
|
||||||
|
.card-content { font-family: var(--font-body); font-size: 14px; color: var(--ink); margin-bottom: 6px; line-height: 1.5; }
|
||||||
|
.card-footer { display: flex; align-items: center; }
|
||||||
|
.card-time { font-size: 12px; color: var(--warm-gray); }
|
||||||
|
.empty-state { text-align: center; padding: 48px 0; }
|
||||||
|
.empty-glyph { font-family: var(--font-heading); font-size: 40px; color: var(--gold); opacity: 0.4; margin-bottom: 8px; }
|
||||||
|
.empty-text { font-family: var(--font-body); font-size: 15px; color: var(--warm-gray); margin: 0; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import api from '@/api/index'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const loading = ref(false)
|
||||||
|
const items = ref<any[]>([])
|
||||||
|
|
||||||
|
onMounted(loadItems)
|
||||||
|
|
||||||
|
async function loadItems() {
|
||||||
|
loading.value = true
|
||||||
|
try { const res = await api.get('/visits/'); items.value = Array.isArray(res.data) ? res.data : (res.data.items || []) }
|
||||||
|
catch (_) {}
|
||||||
|
finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const methodColors: Record<string, string> = { '上门': '#4A6741', '电话': '#5B7FA5', '微信': '#22c55e', '出差': '#C4934A' }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="list-page">
|
||||||
|
<header class="form-editorial-header">
|
||||||
|
<button type="button" class="form-back-btn" @click="router.back()">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
|
||||||
|
</button>
|
||||||
|
<div class="form-title-group"><h2 class="form-title">历史拜访</h2><span class="form-subtitle">VISIT HISTORY</span></div>
|
||||||
|
<div class="form-rule"></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div v-loading="loading" class="cards">
|
||||||
|
<div v-if="items.length === 0 && !loading" class="empty-state"><div class="empty-glyph">—</div><p class="empty-text">暂无拜访记录</p></div>
|
||||||
|
<article v-for="item in items" :key="item.id" class="card" @click="router.push(`/m/visit/${item.id}/edit`)">
|
||||||
|
<div class="card-accent" :style="{ background: methodColors[item.visit_method] || '#7B7568' }"></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-method" :style="{ color: methodColors[item.visit_method] || '#7B7568' }">{{ item.visit_method }}</span>
|
||||||
|
<span class="card-date">{{ item.visit_date }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">{{ item.communication_content || '暂无沟通内容' }}</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<span class="card-customer">{{ item.customer_name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.list-page { max-width: 100%; padding-bottom: 20px; }
|
||||||
|
.form-editorial-header { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 20px; flex-wrap: wrap; }
|
||||||
|
.form-back-btn { background: var(--surface); border: 1px solid var(--warm-border); padding: 8px 10px; cursor: pointer; color: var(--warm-gray); display: flex; align-items: center; transition: all var(--transition); flex-shrink: 0; margin-top: 2px; }
|
||||||
|
.form-back-btn:hover { color: var(--ink); border-color: var(--ink); }
|
||||||
|
.form-title-group { display: flex; flex-direction: column; gap: 0; flex: 1; }
|
||||||
|
.form-title { margin: 0; font-family: var(--font-heading); font-size: 24px; font-weight: 400; color: var(--ink); letter-spacing: 0.06em; line-height: 1.3; }
|
||||||
|
.form-subtitle { font-family: var(--font-mono); font-size: 9px; color: var(--gold); letter-spacing: 0.2em; }
|
||||||
|
.form-rule { width: 100%; height: 2px; background: var(--warm-border); margin-top: 6px; position: relative; }
|
||||||
|
.form-rule::after { content: ''; position: absolute; left: 0; top: 0; width: 32px; height: 2px; background: var(--gold); }
|
||||||
|
.cards { display: flex; flex-direction: column; gap: 10px; }
|
||||||
|
.card { background: var(--surface); border: 1px solid var(--warm-border); display: flex; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; }
|
||||||
|
.card:active { transform: scale(0.99); }
|
||||||
|
.card:hover { box-shadow: 0 4px 12px rgba(28,55,56,0.06); }
|
||||||
|
.card-accent { width: 4px; flex-shrink: 0; }
|
||||||
|
.card-body { flex: 1; padding: 14px 16px; min-width: 0; }
|
||||||
|
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
||||||
|
.card-method { font-family: var(--font-heading); font-size: 13px; }
|
||||||
|
.card-date { font-family: var(--font-mono); font-size: 12px; color: var(--warm-gray); }
|
||||||
|
.card-content { font-family: var(--font-body); font-size: 14px; color: var(--ink); margin-bottom: 6px; line-height: 1.5; }
|
||||||
|
.card-footer { display: flex; align-items: center; justify-content: space-between; }
|
||||||
|
.card-customer { font-size: 12px; color: var(--warm-gray); }
|
||||||
|
.empty-state { text-align: center; padding: 48px 0; }
|
||||||
|
.empty-glyph { font-family: var(--font-heading); font-size: 40px; color: var(--gold); opacity: 0.4; margin-bottom: 8px; }
|
||||||
|
.empty-text { font-family: var(--font-body); font-size: 15px; color: var(--warm-gray); margin: 0; }
|
||||||
|
</style>
|
||||||
@@ -4,16 +4,20 @@ import { useRouter } from 'vue-router'
|
|||||||
import api from '@/api/index'
|
import api from '@/api/index'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const counts = ref({ plans: 0, miniBiz: 0, keyVisits: 0, leaves: 0 })
|
const counts = ref({ visits: 0, notes: 0, plans: 0, miniBiz: 0, keyVisits: 0, leaves: 0 })
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const [plans, mini, key, leaves] = await Promise.all([
|
const [visits, notes, plans, mini, key, leaves] = await Promise.all([
|
||||||
|
api.get('/visits/', { params: { page_size: 1 } }),
|
||||||
|
api.get('/daily-notes/', { params: { page_size: 1 } }),
|
||||||
api.get('/work-plans/'),
|
api.get('/work-plans/'),
|
||||||
api.get('/mini-business/'),
|
api.get('/mini-business/'),
|
||||||
api.get('/key-visits/'),
|
api.get('/key-visits/'),
|
||||||
api.get('/leaves/', { params: { page_size: 1 } }),
|
api.get('/leaves/', { params: { page_size: 1 } }),
|
||||||
])
|
])
|
||||||
|
counts.value.visits = Array.isArray(visits.data) ? visits.data.length : (visits.data.total || (visits.data.items || []).length)
|
||||||
|
counts.value.notes = Array.isArray(notes.data) ? notes.data.length : (notes.data.total || (notes.data.items || []).length)
|
||||||
counts.value.plans = Array.isArray(plans.data) ? plans.data.length : (plans.data.items || []).length
|
counts.value.plans = Array.isArray(plans.data) ? plans.data.length : (plans.data.items || []).length
|
||||||
counts.value.miniBiz = Array.isArray(mini.data) ? mini.data.length : (mini.data.items || []).length
|
counts.value.miniBiz = Array.isArray(mini.data) ? mini.data.length : (mini.data.items || []).length
|
||||||
counts.value.keyVisits = Array.isArray(key.data) ? key.data.length : (key.data.items || []).length
|
counts.value.keyVisits = Array.isArray(key.data) ? key.data.length : (key.data.items || []).length
|
||||||
@@ -22,6 +26,8 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const modules = [
|
const modules = [
|
||||||
|
{ path: '/m/visits', label: '历史拜访', icon: '📋', count: () => counts.value.visits, color: '#4A6741' },
|
||||||
|
{ path: '/m/notes', label: '历史纪要', icon: '📝', count: () => counts.value.notes, color: '#1C3738' },
|
||||||
{ path: '/m/plans', label: '工作计划', icon: '📅', count: () => counts.value.plans, color: '#4A6741' },
|
{ path: '/m/plans', label: '工作计划', icon: '📅', count: () => counts.value.plans, color: '#4A6741' },
|
||||||
{ path: '/m/mini-biz', label: '商机跟单', icon: '💰', count: () => counts.value.miniBiz, color: '#C4934A' },
|
{ path: '/m/mini-biz', label: '商机跟单', icon: '💰', count: () => counts.value.miniBiz, color: '#C4934A' },
|
||||||
{ path: '/m/key-visits', label: '要客拜访', icon: '⭐', count: () => counts.value.keyVisits, color: '#5B7FA5' },
|
{ path: '/m/key-visits', label: '要客拜访', icon: '⭐', count: () => counts.value.keyVisits, color: '#5B7FA5' },
|
||||||
@@ -64,7 +70,7 @@ const modules = [
|
|||||||
.form-rule { width: 100%; height: 2px; background: var(--warm-border); margin-top: 6px; position: relative; }
|
.form-rule { width: 100%; height: 2px; background: var(--warm-border); margin-top: 6px; position: relative; }
|
||||||
.form-rule::after { content: ''; position: absolute; left: 0; top: 0; width: 32px; height: 2px; background: var(--gold); }
|
.form-rule::after { content: ''; position: absolute; left: 0; top: 0; width: 32px; height: 2px; background: var(--gold); }
|
||||||
|
|
||||||
.module-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
.module-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; }
|
||||||
.module-card {
|
.module-card {
|
||||||
background: var(--surface); border: 1px solid var(--warm-border);
|
background: var(--surface); border: 1px solid var(--warm-border);
|
||||||
padding: 20px 16px; display: flex; flex-direction: column; align-items: center; gap: 6px;
|
padding: 20px 16px; display: flex; flex-direction: column; align-items: center; gap: 6px;
|
||||||
|
|||||||
Reference in New Issue
Block a user