diff --git a/api/visit-report.js b/api/visit-report.js
new file mode 100644
index 0000000..b519436
--- /dev/null
+++ b/api/visit-report.js
@@ -0,0 +1,21 @@
+import post, {get, put, del} from "@/api/request";
+
+// 跑店填报(巡店列表 + 活动小结)
+export const visitReportAddReq = data => post('/yxe/yxe-visit-report', data)
+
+export const visitReportListReq = params => get('/yxe/yxe-visit-report', {params})
+
+export const visitReportPageReq = params => get('/yxe/yxe-visit-report/page', {params})
+
+export const visitReportAuditPageReq = params => get('/yxe/yxe-visit-report/audit/page', {params})
+
+export const visitReportInfoReq = id => get(`/yxe/yxe-visit-report/${id}`)
+
+export const visitReportUserListReq = params => get('/yxe/yxe-visit-report/userList', {params})
+
+export const visitReportChangeReq = data => put('/yxe/yxe-visit-report', data)
+
+// 驳回后重新提交:仅本人、仅已驳回记录可调用,状态由服务端置回待审核
+export const visitReportResubmitReq = data => put('/yxe/yxe-visit-report/resubmit', data)
+
+export const visitReportDeleteReq = id => del(`/yxe/yxe-visit-report/${id}`)
diff --git a/packageUser/self-report/create.vue b/packageUser/self-report/create.vue
index 73b3041..b6e6faf 100644
--- a/packageUser/self-report/create.vue
+++ b/packageUser/self-report/create.vue
@@ -1,6 +1,6 @@
-
+
连锁筛选
@@ -268,6 +268,15 @@ import {
clearSalesHandoff,
cloneSalesItems
} from "@/util/sales-handoff";
+import {
+ normalizeMerchant as sharedNormalizeMerchant,
+ pickFirstText as sharedPickFirstText,
+ unwrapList as sharedUnwrapList,
+ normalizeMerchantChain as sharedNormalizeMerchantChain,
+ groupMerchantListByChain as sharedGroupMerchantListByChain,
+ flattenMerchantsFromChains as sharedFlattenMerchantsFromChains,
+ filterMerchantStores as sharedFilterMerchantStores
+} from "@/util/store-picker";
export default {
components: {Login, AppNavbar},
@@ -761,123 +770,36 @@ export default {
this.currentCity = location.city_str || ''
this.currentAddress = location.full || [location.province_str, location.city_str, location.area_str].filter(Boolean).join('')
},
+ // 连锁/门店解析统一走 util/store-picker:跑店填报(visit.vue)用同一份逻辑,
+ // 两页各自维护一份迟早会出现解析口径不一致。
normalizeMerchant(item) {
- const id = item.id ?? item.merchantId ?? item.merchant_id
- const text = item.name || item.merchantName || item.merchant_name || item.title || '未命名门店'
- const lat = Number(item.lat ?? item.latitude ?? item.merchantLat ?? item.merchant_lat)
- const lng = Number(item.lng ?? item.longitude ?? item.merchantLng ?? item.merchant_lng)
- const address = [item.province, item.city, item.region, item.area, item.address].filter(Boolean).join('')
- return {
- id,
- text,
- chainId: item.chainId ?? item.chain_id ?? '',
- chainName: item.chainName || item.chain || item.chainAbb || '',
- lat: Number.isFinite(lat) ? lat : null,
- lng: Number.isFinite(lng) ? lng : null,
- province: item.province || item.province_str || '',
- city: item.city || item.city_str || '',
- address
- }
+ return sharedNormalizeMerchant(item)
+ },
+ pickFirstText(source, keys = []) {
+ return sharedPickFirstText(source, keys)
+ },
+ unwrapList(data) {
+ return sharedUnwrapList(data)
+ },
+ normalizeMerchantChain(item, index) {
+ return sharedNormalizeMerchantChain(item, index)
+ },
+ groupMerchantListByChain(list) {
+ return sharedGroupMerchantListByChain(list)
+ },
+ flattenMerchantsFromChains(chainList) {
+ return sharedFlattenMerchantsFromChains(chainList)
+ },
+ // 本页的门店搜索关键词存在 data 里(merchantStoreSearchKeyword),
+ // 与 visit.vue 由调用方传入关键词的签名不同,这里保留本页签名。
+ filterMerchantStores(merchants) {
+ return sharedFilterMerchantStores(merchants, this.merchantStoreSearchKeyword)
},
normalizeChainIdForApi(rawChainId) {
const text = String(rawChainId || '').trim()
if (!text || text.startsWith('chain_')) return ''
return text
},
- pickFirstText(source, keys = []) {
- if (!source || typeof source !== 'object') return ''
- for (let i = 0; i < keys.length; i += 1) {
- const value = source[keys[i]]
- if (value !== undefined && value !== null && String(value).trim()) {
- return String(value).trim()
- }
- }
- return ''
- },
- unwrapList(data) {
- if (Array.isArray(data)) return data
- if (!data || typeof data !== 'object') return []
- const keys = ['list', 'records', 'rows', 'items', 'data', 'result', 'chainList']
- for (let i = 0; i < keys.length; i += 1) {
- const value = data[keys[i]]
- if (Array.isArray(value)) return value
- if (value && typeof value === 'object') {
- const nested = this.unwrapList(value)
- if (nested.length) return nested
- }
- }
- return [data]
- },
- normalizeMerchantChain(item, index) {
- const chainName = this.pickFirstText(item, ['chainName', 'chain', 'chainAbb', 'name', 'title']) || `连锁${index + 1}`
- const rawChainId = item.chainId ?? item.chain_id ?? item.id ?? item.chainCode ?? item.chain_code ?? chainName
- const chainId = rawChainId === undefined || rawChainId === null || String(rawChainId).trim() === ''
- ? `chain_${index}`
- : rawChainId
- const childCandidates = ['children', 'merchantList', 'merchants', 'shopList', 'list', 'records']
- let rawChildren = []
- for (let i = 0; i < childCandidates.length; i += 1) {
- const list = item[childCandidates[i]]
- if (Array.isArray(list) && list.length) {
- rawChildren = list
- break
- }
- }
- let merchants = rawChildren
- .map(child => this.normalizeMerchant(child))
- .filter(merchant => merchant.id !== undefined && merchant.id !== null && String(merchant.id).trim() !== '')
- .map(merchant => Object.assign({}, merchant, {chainId, chainName}))
- if (!merchants.length) {
- const selfMerchant = this.normalizeMerchant(item)
- if (selfMerchant.id !== undefined && selfMerchant.id !== null && String(selfMerchant.id).trim() !== '') {
- merchants = [Object.assign({}, selfMerchant, {chainId, chainName})]
- }
- }
- return {
- id: chainId,
- text: chainName,
- merchants
- }
- },
- groupMerchantListByChain(list) {
- const chainMap = {}
- ;(Array.isArray(list) ? list : []).forEach((item, index) => {
- const merchant = this.normalizeMerchant(item)
- if (merchant.id === undefined || merchant.id === null || String(merchant.id).trim() === '') return
- const chainName = this.pickFirstText(item, ['chainName', 'chain', 'chainAbb', 'chain_name'])
- if (!chainName) return
- const rawChainId = item.chainId ?? item.chain_id ?? item.chainCode ?? item.chain_code ?? chainName
- const chainId = rawChainId === undefined || rawChainId === null || String(rawChainId).trim() === ''
- ? `chain_${index}`
- : rawChainId
- const key = String(chainId)
- if (!chainMap[key]) {
- chainMap[key] = {id: chainId, text: chainName, merchants: []}
- }
- chainMap[key].merchants.push(Object.assign({}, merchant, {chainId, chainName}))
- })
- return Object.keys(chainMap).map(key => chainMap[key])
- },
- flattenMerchantsFromChains(chainList) {
- const list = []
- ;(Array.isArray(chainList) ? chainList : []).forEach(chain => {
- const merchants = Array.isArray(chain.merchants) ? chain.merchants : []
- merchants.forEach(merchant => {
- if (!merchant || merchant.id === '' || merchant.id === null || merchant.id === undefined) return
- list.push(Object.assign({}, merchant, {
- chainId: merchant.chainId || chain.id || '',
- chainName: merchant.chainName || chain.text || ''
- }))
- })
- })
- return list
- },
- filterMerchantStores(merchants) {
- const list = Array.isArray(merchants) ? merchants : []
- const keyword = (this.merchantStoreSearchKeyword || '').trim()
- if (!keyword) return list
- return list.filter(merchant => (merchant.text || '').includes(keyword))
- },
applyMerchant(chain, merchant) {
if (!merchant) return
this.form.chainName = (chain && chain.text) || merchant.chainName || ''
diff --git a/packageUser/self-report/records.vue b/packageUser/self-report/records.vue
index d13c0b4..678351e 100644
--- a/packageUser/self-report/records.vue
+++ b/packageUser/self-report/records.vue
@@ -3,6 +3,16 @@
+
+
+ {{ tab.label }}
+
+
- {{ item.merchantName || '-' }}
- 连锁:{{ item.chainName }}
+ {{ cardTitle(item) }}
+ {{ cardSubTitle(item) }}
{{ statusText(item.status) }}
-
+
活动时间
{{ item.activityTime || '-' }}
-
-
-
-
-
+
+ 巡店门店数
+ {{ item.storeCount || 0 }}家
+
+
评分
{{ item.rating || '-' }}星
@@ -77,6 +87,35 @@
+
+
+ 暂无巡店记录
+
+
+ 巡店{{ sIdx + 1 }}
+ {{ store.visitTime || '-' }}
+
+ 门店:{{ store.merchantName || '-' }}
+ 连锁:{{ store.chainName || '-' }}
+
+ 定位:{{ store.location || '-' }}
+ ({{ store.lat }}, {{ store.lng }})
+
+
+
+
+ 暂无巡店照片
+
+
+
+
上下班打卡
@@ -139,6 +178,7 @@
暂无销量数据
+
@@ -185,12 +225,19 @@ import AppNavbar from "@/components/AppNavbar.vue";
import Login from "@/components/Login.vue";
import {dictDataListByDictReq} from "@/api/common";
import {selfReportUserListReq} from "@/api/self-report";
+import {visitReportUserListReq} from "@/api/visit-report";
import {getUserInfo} from "@/util/user";
export default {
components: {AppNavbar, Login},
data() {
return {
+ // 驻店填报 = yxe_self_report,跑店填报 = yxe_visit_report(两张表,分别拉取)
+ typeTabs: [
+ {label: '驻店填报', value: 'store'},
+ {label: '跑店填报', value: 'visit'}
+ ],
+ activeType: 'store',
statusTabs: [
{label: '全部', value: 'all'},
{label: '待审核', value: 0},
@@ -226,10 +273,10 @@ export default {
if (this.activeStatus !== 'all' && Number(item.status) !== Number(this.activeStatus)) {
return false
}
- if (this.filterForm.chainId && !this.matchesSelectedChain(item.chainIdNormalized, item.chainName, this.filterForm.chainId)) {
+ if (this.filterForm.chainId && !this.matchesItemChain(item)) {
return false
}
- if (keyword && !String(item.merchantName || '').toLowerCase().includes(keyword)) {
+ if (keyword && !this.matchesItemKeyword(item, keyword)) {
return false
}
if ((startTs || endTs) && !Number.isFinite(item.submitTimestamp)) {
@@ -291,7 +338,8 @@ export default {
async fetchList() {
this.loading = true
try {
- const {data} = await selfReportUserListReq({})
+ const req = this.activeType === 'visit' ? visitReportUserListReq : selfReportUserListReq
+ const {data} = await req({})
this.records = this.unwrapList(data)
} catch (error) {
this.records = []
@@ -321,6 +369,9 @@ export default {
}
},
normalizeRecord(item = {}) {
+ if (this.activeType === 'visit') {
+ return this.normalizeVisitRecord(item)
+ }
const createRaw = item.createTime || item.createdAt || item.date || ''
const clockPhotos = this.parseMaybeJson(item.clockPhotos, [])
const boothPhotos = this.parseMaybeJson(item.boothPhotos, [])
@@ -346,7 +397,48 @@ export default {
clockImages: this.normalizeMediaList(clockPhotos),
boothImages: this.normalizeMediaList(boothPhotos),
receiptImages: this.normalizeMediaList(receiptPhoto),
- salesRows: this.buildSalesRows(salesGroups)
+ salesRows: this.buildSalesRows(salesGroups),
+ storeCount: 0,
+ storeRows: []
+ }
+ },
+ // 跑店填报没有单门店/活动时间/打卡/销量,卡片主体是巡店列表
+ normalizeVisitRecord(item = {}) {
+ const createRaw = item.createTime || item.createdAt || ''
+ const summaryRaw = this.parseMaybeJson(item.summary, item.summary || '')
+ const summary = typeof summaryRaw === 'string' ? summaryRaw : JSON.stringify(summaryRaw || '')
+ const storeList = this.parseMaybeJson(item.storeList, [])
+ const storeRows = (Array.isArray(storeList) ? storeList : []).map(store => ({
+ merchantName: store && (store.merchantName || store.storeName) ? (store.merchantName || store.storeName) : '',
+ chainName: store && store.chainName ? store.chainName : '',
+ visitTime: store && (store.visitTime || store.time) ? (store.visitTime || store.time) : '',
+ location: store && store.location ? store.location : '',
+ lat: store && store.lat !== undefined ? store.lat : '',
+ lng: store && store.lng !== undefined ? store.lng : '',
+ photos: this.normalizeMediaList(store && store.photos)
+ }))
+ return {
+ id: item.id,
+ merchantId: '',
+ merchantName: '',
+ chainName: '-',
+ chainIdNormalized: '',
+ activityTime: '',
+ date: '',
+ status: item.status,
+ rejectReason: item.rejectReason || item.reject_reason || '',
+ reward: item.reward,
+ rating: '',
+ summary,
+ submitTime: this.formatDateTime(createRaw, createRaw || '-'),
+ submitTimestamp: this.parseDateToTimestamp(createRaw),
+ clockImages: [],
+ boothImages: [],
+ receiptImages: [],
+ salesRows: [],
+ // 门店数与主门店关键词都从列表内容推导,避免列表页再下发冗余字段
+ storeCount: Number(item.storeCount) || storeRows.length,
+ storeRows
}
},
buildSalesRows(groups) {
@@ -413,6 +505,21 @@ export default {
value
}
},
+ // 跑店记录没有单一门店/连锁,判定要落到巡店列表的每一行
+ matchesItemChain(item) {
+ const selectedId = this.filterForm.chainId
+ if (selectedId === '' || selectedId === null || selectedId === undefined) return true
+ if (this.activeType === 'visit') {
+ return (item.storeRows || []).some(store => this.matchesSelectedChain('', store.chainName, selectedId))
+ }
+ return this.matchesSelectedChain(item.chainIdNormalized, item.chainName, selectedId)
+ },
+ matchesItemKeyword(item, keyword) {
+ if (this.activeType === 'visit') {
+ return (item.storeRows || []).some(store => String(store.merchantName || '').toLowerCase().includes(keyword))
+ }
+ return String(item.merchantName || '').toLowerCase().includes(keyword)
+ },
matchesSelectedChain(chainId, chainName, selectedId) {
if (selectedId === '' || selectedId === null || selectedId === undefined) return true
const option = this.chainOptions.find(item => String(item.id) === String(selectedId))
@@ -477,9 +584,33 @@ export default {
},
handleResubmit(item) {
if (!item || item.id === undefined || item.id === null) return
- uni.navigateTo({
- url: `/packageUser/self-report/create?id=${item.id}&mode=resubmit`
- })
+ const url = this.activeType === 'visit'
+ ? `/packageUser/self-report/visit?id=${item.id}&mode=resubmit`
+ : `/packageUser/self-report/create?id=${item.id}&mode=resubmit`
+ uni.navigateTo({url})
+ },
+ cardTitle(item) {
+ if (this.activeType === 'visit') {
+ const names = (item.storeRows || [])
+ .map(store => store.merchantName)
+ .filter(Boolean)
+ if (names.length) {
+ return names.length > 2 ? `${names.slice(0, 2).join('、')} 等${names.length}家` : names.join('、')
+ }
+ return '跑店填报'
+ }
+ return item.merchantName || '-'
+ },
+ cardSubTitle(item) {
+ if (this.activeType === 'visit') return ''
+ return item.chainName && item.chainName !== '-' ? `连锁:${item.chainName}` : ''
+ },
+ changeType(value) {
+ if (this.activeType === value) return
+ this.activeType = value
+ this.expandedMap = {}
+ this.records = []
+ this.fetchList()
},
changeStatus(value) {
this.activeStatus = value
@@ -581,6 +712,14 @@ export default {
margin: 18rpx 20rpx 0;
}
+.type-tabs {
+ display: flex;
+ gap: 12rpx;
+ padding-bottom: 16rpx;
+ margin-bottom: 16rpx;
+ border-bottom: 1rpx solid #eef2f7;
+}
+
.status-tabs {
display: flex;
gap: 12rpx;
@@ -839,6 +978,48 @@ export default {
font-size: 24rpx;
}
+.store-block {
+ padding: 18rpx;
+ margin-bottom: 16rpx;
+ border-radius: 16rpx;
+ background: #f9fafb;
+ border: 1rpx solid #eef2f7;
+}
+
+.store-block:last-child {
+ margin-bottom: 0;
+}
+
+.store-block-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12rpx;
+}
+
+.store-block-title {
+ font-size: 26rpx;
+ font-weight: 700;
+ color: #111827;
+}
+
+.store-block-time {
+ font-size: 22rpx;
+ color: #9ca3af;
+}
+
+.store-block-line {
+ margin-bottom: 8rpx;
+ font-size: 24rpx;
+ color: #4b5563;
+ line-height: 1.5;
+ word-break: break-all;
+}
+
+.store-block .media-images {
+ margin-top: 10rpx;
+}
+
.sales-list {
display: flex;
flex-direction: column;
diff --git a/packageUser/self-report/select.vue b/packageUser/self-report/select.vue
new file mode 100644
index 0000000..694dcdd
--- /dev/null
+++ b/packageUser/self-report/select.vue
@@ -0,0 +1,268 @@
+
+
+
+
+
+
+
+
+ 自主填报
+ 真实记录 · 及时上报 · 共创佳绩
+
+
+
+
+
+
+ 请选择填报类型
+
+ 请选择您本次要填写的工作类型
+
+
+
+
+ {{ item.title }}
+ {{ item.desc }}
+
+
+
+
+
+
+
+
+ 确认选择
+
+ ① 请选择合适的填报类型,以便更准确地记录您的工作
+
+
+
+
+
+
diff --git a/packageUser/self-report/visit.vue b/packageUser/self-report/visit.vue
new file mode 100644
index 0000000..1e17d12
--- /dev/null
+++ b/packageUser/self-report/visit.vue
@@ -0,0 +1,1216 @@
+
+
+
+
+
+
+ 巡店列表
+
+
+ 暂无巡店记录,请点击下方「新增巡店」
+
+
+
+ 巡店{{ idx + 1 }}
+ {{ item.visitTime }}
+
+ 删除
+
+
+
+
+ 连锁筛选
+
+
+ {{ chainText(item) }}
+
+
+
+
+
+
+ 门店筛选
+
+
+ {{ merchantText(item) }}
+
+
+
+
+
+
+
+ 定位
+ 获取定位
+
+
+
+ 纬度:
+ {{ formatCoordinate(item.lat) }}
+
+
+ 经度:
+ {{ formatCoordinate(item.lng) }}
+
+
+ 详细地址:
+ {{ item.location || '暂无地址(可不填)' }}
+
+
+
+
+
+
+ 打卡拍照
+
+
+
+
+
+
+
+
+
+
+ x
+
+
+
+
+
+
+
+ 新增巡店
+
+
+
+ 活动小结
+
+
+
+
+ 驳回原因
+ {{ rejectReason }}
+
+
+
+ 保存草稿
+ {{ submitLoading ? '提交中...' : (isResubmit ? '重新提交' : '提交填报') }}
+
+
+
+
+
+
+ 取消
+ 选择连锁
+ 确定
+
+
+
+
+
+
+
+ {{ item.text }}
+
+
+
+
+
+
+
+
+
+ 取消
+ 选择门店
+ 确定
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.text }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages.json b/pages.json
index 765af2c..a4c0aed 100644
--- a/pages.json
+++ b/pages.json
@@ -164,12 +164,26 @@
"root": "packageUser",
"pages": [
{
- "path": "self-report/create",
+ "path": "self-report/select",
"style": {
"navigationBarTitleText": "自主填报",
"navigationStyle": "custom"
}
},
+ {
+ "path": "self-report/create",
+ "style": {
+ "navigationBarTitleText": "驻店填报",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "self-report/visit",
+ "style": {
+ "navigationBarTitleText": "跑店填报",
+ "navigationStyle": "custom"
+ }
+ },
{
"path": "self-report/sales",
"style": {
diff --git a/pages/audit/components/AuditVisitReportItem.vue b/pages/audit/components/AuditVisitReportItem.vue
new file mode 100644
index 0000000..d8cd946
--- /dev/null
+++ b/pages/audit/components/AuditVisitReportItem.vue
@@ -0,0 +1,185 @@
+
+
+
+
+ 门店数:{{ currentRecord.storeCount || storeRows.length || 0 }} 家
+ 提交人:{{ currentRecord.owner || item.owner }}
+ 小结:{{ currentRecord.summary || '-' }}
+ 驳回原因:{{ currentRecord.rejectReason }}
+
+ {{ detailExpanded ? '收起详情' : '展开详情' }}
+
+
+
+ 加载中...
+ 暂无巡店记录
+
+
+ 巡店{{ idx + 1 }}
+ {{ store.visitTime || '-' }}
+
+ 连锁:{{ store.chainName || '-' }}
+ 门店:{{ store.merchantName || '-' }}
+
+ 定位:{{ store.location || '-' }}
+ ({{ store.lat }}, {{ store.lng }})
+
+
+
+
+ 暂无巡店照片
+
+
+
+
+
+ 通过
+
+
+ 驳回
+
+
+
+
+
+
+
+
+
diff --git a/pages/audit/index.vue b/pages/audit/index.vue
index 3acdee9..c7848bd 100644
--- a/pages/audit/index.vue
+++ b/pages/audit/index.vue
@@ -106,6 +106,16 @@
@updated="handleSelfReportUpdated"
/>
+
+
+