一些页面实现

This commit is contained in:
sqlicong
2025-08-04 20:16:16 +08:00
parent e91b7767ca
commit d687d7fe44
14 changed files with 707 additions and 19 deletions

View File

@@ -4,7 +4,7 @@ NODE_ENV=production
VITE_DEV=false
# 请求路径
VITE_BASE_URL='http://localhost:48080'
VITE_BASE_URL='https://www.cdsrh.top'
# 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务
VITE_UPLOAD_TYPE=server
@@ -22,7 +22,7 @@ VITE_DROP_CONSOLE=true
VITE_SOURCEMAP=false
# 打包路径
VITE_BASE_PATH=/
VITE_BASE_PATH=/kfc/
# 输出路径
VITE_OUT_DIR=dist-prod

View File

@@ -14,7 +14,13 @@
content="芋道管理系统 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
/>
<title>%VITE_APP_TITLE%</title>
<script type="text/javascript" src="https://webapi.amap.com/maps?v=2.0&key=6fe46798cd1e0f55d95ffc3a993e29ac&plugin=AMap.Geolocation"></script>
<script>
// 定义SDK加载完成的回调
window.onAmapLoaded = function() {}
</script>
</head>
<body>
<div id="app">
<style>

6
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "yudao-ui-admin-vue3",
"version": "2.5.0-snapshot",
"version": "2.6.1-snapshot",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "yudao-ui-admin-vue3",
"version": "2.5.0-snapshot",
"version": "2.6.1-snapshot",
"license": "MIT",
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
@@ -20,7 +20,7 @@
"@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^3.0.4",
"animate.css": "^4.1.1",
"axios": "^1.6.8",
"axios": "1.9.0",
"benz-amr-recorder": "^1.1.5",
"bpmn-js-token-simulation": "^0.36.0",
"camunda-bpmn-moddle": "^7.0.1",

View File

@@ -47,4 +47,11 @@ export const AgentOrderApi = {
exportAgentOrder: async (params) => {
return await request.download({ url: `/kfc/agent-order/export-excel`, params })
},
submitOrder: async (data:any) => {
return await request.post({
url: `/kfc/agent-order/submit`, data
})
}
}

View File

@@ -55,4 +55,16 @@ export const ProductApi = {
exportProduct: async (params) => {
return await request.download({ url: `/kfc/product/export-excel`, params })
},
//查询kfc商品分类
getKFCProductCategory: async()=>{
return await request.get({ url: `/kfc/product/get/outProduct/sort` })
},
//查询kfc商品表单
getKFCProductForm: async(id:string)=>{
return await request.get({ url: `/kfc/product/get/outProduct/form?id=${id}`})
},
//查询kfc商品规格详情
getKFCProductDetail: async(linkId:string)=>{
return await request.get({ url: `/kfc/product/get/outProduct/detail?linkId=${linkId}` })
}
}

View File

@@ -0,0 +1,22 @@
import request from '@/config/axios'
/** 门店基本信息信息 */
export interface KfcStore {
storename:string;//门店名称
address:string;//门店地址
distance:string;//门店描述
tel:string;//联系电话
}
// 门店信息 API
export const StroeApi = {
// 查询门店基本信息列表
getStoreList: async (lon: number,lat:number) => {
return await request.get({ url: `/store/list?lon=${lon}&lat=${lat}`})
},
getStoreListByCity: async(city:string)=>{
return await request.get({ url: `/store/city-list?city=${city}`})
},
}

32
src/types/amap.d.ts vendored Normal file
View File

@@ -0,0 +1,32 @@
interface Window {
AMap: {
// 声明AMap的核心方法和类
plugin: (
pluginName: string | string[],
callback: () => void
) => void;
Geolocation: new (options: AMapGeolocationOptions) => AMapGeolocation;
};
onAmapLoaded: () => void;
}
// 定位配置选项类型
interface AMapGeolocationOptions {
enableHighAccuracy?: boolean;
timeout?: number;
convert?: boolean;
buttonPosition?: string;
buttonOffset?: any;
zoomToAccuracy?: boolean;
}
// 定位实例的方法类型
interface AMapGeolocation {
getCityInfo: (callback: (status: string, result: any) => void) => void;
getCurrentPosition: (callback: (status: string, result: any) => void) => void;
destroy: () => void;
}
// 确保TypeScript将此文件视为模块
export {};

View File

@@ -37,8 +37,22 @@
<el-input v-model="formData.triggerCondition" placeholder="请输入触发条件" />
</el-form-item>
<el-form-item label="接收用户" prop="receiveUserIds">
<el-input v-model="formData.receiveUserIds" placeholder="请输入接收用户,多个用户用','隔开'" />
</el-form-item>
<el-select
v-model="formData.receiveUserIds"
placeholder="请选择接收用户"
@click="getSimpleUserList()"
multiple
clearable
>
<el-option
v-for="dict in simpleUserList"
:key="dict.id"
:label="dict.nickname"
:value="dict.id"
/>
</el-select>
</el-form-item>
<el-form-item label="通知方式" prop="notificationWay">
<el-checkbox-group v-model="formData.notificationWay">
<el-checkbox
@@ -69,6 +83,7 @@
</template>
<script setup lang="ts">
import { getIntDictOptions, getStrDictOptions, DICT_TYPE } from '@/utils/dict'
import * as UserApi from '@/api/system/user'
import { AlarmConfigApi, AlarmConfig } from '@/api/kfc/alarm/alarmconfig'
/** 报警配置 表单 */
@@ -124,14 +139,13 @@ const submitForm = async () => {
// 校验表单
await formRef.value.validate()
const submitData = { ...formData.value }; // 复制一份数据,避免修改原表单
if (submitData.receiveUserIds) {
// 分割字符串为数组,并转换为数字类型
submitData.receiveUserIds = formData.value.receiveUserIds
.split(',')
.map(id => Number(id.trim())) // 去除空格并转为数字
.filter(id => !isNaN(id)); // 过滤无效数字
if (Array.isArray(submitData.receiveUserIds)) {
// 确保是数字数组(过滤无效值)
submitData.receiveUserIds = submitData.receiveUserIds
.map(id => Number(id))
.filter(id => !isNaN(id));
} else {
// 空值处理确保后端接收数组类型而非undefined
// 兜底:确保始终是数组类型
submitData.receiveUserIds = [];
}
// 提交请求
@@ -152,7 +166,11 @@ const submitForm = async () => {
formLoading.value = false
}
}
const simpleUserList=ref()
const getSimpleUserList =async ()=>{
simpleUserList.value = await UserApi.getSimpleUserList()
}
/** 重置表单 */
const resetForm = () => {
formData.value = {

View File

@@ -239,7 +239,7 @@ const getList = async () => {
loading.value = true
try {
const data = await AlarmConfigApi.getAlarmConfigPage(queryParams)
console.log(data)
// console.log(data)
list.value = data.list
total.value = data.total
} finally {

View File

@@ -0,0 +1,138 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="80%">
<div v-loading="formLoading" class="loading-container">
<!-- 基础信息表格 -->
<el-table :data="baseInfoTableData" border style="margin-bottom: 20px;">
<el-table-column prop="label" label="基础信息" width="180" align="center" />
<el-table-column prop="value" label="详情" align="left" />
</el-table>
<!-- 餐品分类表格 -->
<el-table
:data="categoryTableData"
border
style="margin-bottom: 10px;"
row-key="round"
>
<el-table-column prop="roundNameCn" label="分类名称" width="150" />
<el-table-column prop="round" label="排序" width="80" align="center" />
<el-table-column label="餐品列表" align="left">
<template #default="scope">
<el-table
:data="scope.row.condimentItemList"
border
size="small"
style="width: 100%;"
row-key="linkId"
>
<el-table-column prop="showNameCn" label="显示名称" width="180" />
<el-table-column prop="nameCn" label="商品名称" width="180" />
<el-table-column
prop="price"
label="价格"
width="100"
align="right"
:formatter="(row) => row.price === 0 ? '0' : (row.price / 100).toFixed(2)"
/>
<el-table-column prop="quantity" label="数量" width="80" align="center" />
<el-table-column prop="linkId" label="商品ID" width="150" />
<el-table-column prop="imageUrl" label="图片URL">
<template #default="itemScope">
<el-input
v-model="itemScope.row.imageUrl"
disabled
style="width: 300px;"
/>
</template>
</el-table-column>
</el-table>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ProductApi } from '@/api/kfc/product'
/** 商品详情表格展示组件 */
defineOptions({ name: 'ProductDetailTable' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 状态管理
const dialogVisible = ref(false)
const dialogTitle = ref('商品详情')
const formLoading = ref(false)
// 原始数据
const formData = ref({
linkId: '',
disabledStatus: 0,
saleFlag: 'Y',
condimentRoundList: [] as Array<{
roundNameCn: string
round: number
condimentItemList: Array<{
linkId: string
imageUrl: string
showNameCn: string
nameCn: string
price: number
quantity: number
mealdealId: number
itemType: number
}>
}>
})
// 基础信息表格数据(转换为键值对格式)
const baseInfoTableData = computed(() => [
{ label: '关联ID', value: formData.value.linkId },
{
label: '禁用状态',
value: formData.value.disabledStatus === 0 ? '启用' : '禁用'
},
{
label: '销售状态',
value: formData.value.saleFlag === 'Y' ? '在售' : '下架'
}
])
// 分类表格数据(直接使用原始数组)
const categoryTableData = computed(() => formData.value.condimentRoundList)
/** 打开弹窗并加载数据 */
const open = async (linkId: string) => {
dialogVisible.value = true
formLoading.value = true
try {
// 调用接口获取数据(假设接口返回格式为 {code:0, data: {disabledStatus, ...}}
const response = await ProductApi.getKFCProductDetail(linkId)
// if (response.code === 0) {
formData.value = { ...formData.value, ...response}
// } else {
// message.error(`加载失败:${response.msg || '未知错误'}`)
// }
} catch (error) {
message.error(t('common.loadFail'))
console.error('数据加载失败:', error)
} finally {
formLoading.value = false
}
}
defineExpose({ open })
</script>
<style scoped>
.loading-container {
min-height: 300px;
padding: 10px;
}
</style>

View File

@@ -36,7 +36,7 @@
<el-form-item label="商品价格" prop="price">
<el-input v-model="formData.price" placeholder="请输入商品价格" />
</el-form-item>
<el-form-item label="中文显示名称" prop="showNameCn">
<el-form-item label="名称" prop="showNameCn">
<el-input v-model="formData.showNameCn" placeholder="请输入中文显示名称" />
</el-form-item>
<el-form-item label="单品标记" prop="singleFlag">

View File

@@ -223,8 +223,15 @@
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="120px">
<el-table-column label="操作" align="center" min-width="180px">
<template #default="scope">
<el-button
link
type="primary"
@click="openKFCForm(scope.row.linkId)"
>
详情
</el-button>
<el-button
link
type="primary"
@@ -255,6 +262,7 @@
<!-- 表单弹窗添加/修改 -->
<ProductForm ref="formRef" @success="getList" />
<KFCProductForm ref="KFCformRef" />
</template>
<script setup lang="ts">
@@ -264,6 +272,7 @@ import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { ProductApi, Product } from '@/api/kfc/product'
import ProductForm from './ProductForm.vue'
import KFCProductForm from './KFCProductForm.vue'
/** 商品基本信息 列表 */
defineOptions({ name: 'Product' })
@@ -325,6 +334,10 @@ const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
const KFCformRef=ref()
const openKFCForm=(id:number)=>{
KFCformRef.value.open(id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {

View File

@@ -0,0 +1,439 @@
<template>
<div class="store-page">
<!-- 页面标题与搜索框 -->
<div class="page-header">
<h1>附近门店</h1>
<p class="sub-title">为您展示周边可用门店信息</p>
<!-- 搜索框 -->
<div class="search-box">
<el-input
v-model="searchKeyword"
placeholder="请输入门店名称搜索..."
prefix-icon="Search"
clearable
size="medium"
/>
</div>
</div>
<!-- 门店列表 -->
<div class="store-list" v-loading="loading">
<div
v-for="(store, index) in filteredStoreList"
:key="index"
class="store-card"
>
<div class="store-info">
<h3 class="store-name">
{{ store.storename }}
<span class="distance-tag">距离{{ store.distance || '未知'}}</span>
</h3>
<div class="store-detail">
<p class="address">
<el-icon class="icon"><Location /></el-icon>
{{ store.address }}
</p>
<p class="tel">
<el-icon class="icon"><Phone /></el-icon>
{{ store.tel }}
</p>
</div>
</div>
<div class="store-actions">
<el-button type="primary" @click="handleSelectStore(store)">
选择此门店并提交订单
</el-button>
</div>
</div>
<!-- 空状态 -->
<div class="empty-state" v-if="!loading && filteredStoreList.length === 0">
<el-empty :description="getEmptyDescription()" />
<el-button
type="text"
@click="retryGetLocation"
v-if="showRetryButton"
>
重试获取位置
</el-button>
</div>
</div>
<!-- 手动输入code的弹窗 -->
<el-dialog
v-model="showCodeDialog"
title="激活码"
:close-on-click-modal="false"
>
<el-input
v-model="inputCode"
placeholder="请输入激活码"
@keyup.enter="confirmCode"
/>
<template #footer>
<el-button @click="showCodeDialog = false">取消</el-button>
<el-button type="primary" @click="confirmCode">确认</el-button>
</template>
</el-dialog>
<!-- 手动选择城市弹窗 -->
<el-dialog v-model="showCitySelectDialog" title="选择城市">
<el-input
v-model="cityName"
placeholder="请输入城市名称"
@keyup.enter="confirmCity"
/>
<template #footer>
<el-button @click="showCitySelectDialog = false">取消</el-button>
<el-button type="primary" @click="confirmCity">确认</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { StroeApi, KfcStore } from '@/api/kfc/store'
import { AgentOrderApi } from '@/api/kfc/finance/agentorder'
import { ElMessage, ElLoading } from 'element-plus'
import { Location, Phone, Search } from '@element-plus/icons-vue'
// 路由相关
const route = useRoute()
const router = useRouter()
// 状态管理
const loading = ref(false)
const storeList = ref<KfcStore[]>([])
const searchKeyword = ref('')
const userLocation = ref<{ lon: number, lat: number } | null>(null)
const cityName = ref('')
const selectedStore = ref<KfcStore | null>(null) // 选中的门店
// code相关状态
const systemCode = ref('') // 系统生成的固定code
const showCodeDialog = ref(false)
const inputCode = ref('')
const currentCode = ref('') // 最终有效的code
// 错误状态
const emptyDescription = ref('正在获取您的位置...')
const showRetryButton = ref(false)
const showCitySelectDialog = ref(false)
// 高德地图定位实例
let geolocation: any = null
// 过滤门店列表
const filteredStoreList = computed(() => {
if (!searchKeyword.value.trim()) {
return storeList.value
}
const keyword = searchKeyword.value.trim().toLowerCase()
return storeList.value.filter(store =>
store.storename?.toLowerCase().includes(keyword)
)
})
// 空状态描述
const getEmptyDescription = () => {
if (searchKeyword.value.trim()) {
return `未找到包含"${searchKeyword.value}"的门店`
}
return emptyDescription.value
}
// 页面挂载时处理code
onMounted(() => {
handleCodeLogic()
})
// 处理code逻辑
const handleCodeLogic = () => {
const urlCode = route.query.code as string
if (urlCode) {
currentCode.value = urlCode
initLocationAndStore()
return
}
if (systemCode.value) {
router.replace({ ...route, query: { ...route.query, code: systemCode.value } })
currentCode.value = systemCode.value
initLocationAndStore()
return
}
showCodeDialog.value = true
}
// 确认用户输入的code
const confirmCode = () => {
if (!inputCode.value.trim()) {
ElMessage.warning('激活码不能为空')
return
}
router.replace({ ...route, query: { ...route.query, code: inputCode.value.trim() } })
currentCode.value = inputCode.value.trim()
showCodeDialog.value = false
initLocationAndStore()
}
// 初始化定位和门店
const initLocationAndStore = () => {
console.log('当前code', currentCode.value)
if (window.AMap) {
initAmapLocation()
} else {
emptyDescription.value = '地图服务加载中...'
window.onAmapLoaded = () => initAmapLocation()
}
}
// 初始化高德定位
const initAmapLocation = () => {
window.AMap.plugin('AMap.Geolocation', () => {
geolocation = new AMap.Geolocation({
enableHighAccuracy: true,
timeout: 8000,
convert: true,
zoomToAccuracy: false,
buttonPosition: 'RB'
})
geolocation.getCityInfo((status: string, result: any) => {
if (status === 'complete' && result.city) {
cityName.value = result.city
}
})
geolocation.getCurrentPosition((status: string, result: any) => {
handleAmapResult(status, result)
})
})
}
// 处理定位结果
const handleAmapResult = (status: string, result: any) => {
loading.value = true
if (status === 'complete' && result.position) {
userLocation.value = {
lon: result.position.lng,
lat: result.position.lat
}
fetchStoreList()
} else {
handleLocationError(result)
}
}
// 定位失败处理
const handleLocationError = (result: any) => {
loading.value = false
showRetryButton.value = true
switch (result.info) {
case 'PERMISSION_DENIED':
emptyDescription.value = '您拒绝了定位授权,请在设置中开启'
break
case 'POSITION_UNAVAILABLE':
emptyDescription.value = '无法获取位置信息'
break
case 'TIMEOUT':
emptyDescription.value = '定位超时'
break
default:
emptyDescription.value = `定位失败:${result.info || '未知错误'}`
}
if (cityName.value) {
showCitySelectDialog.value = true
}
}
// 获取门店列表
const fetchStoreList = async () => {
if (!userLocation.value) return
try {
const response = await StroeApi.getStoreList(
userLocation.value.lon,
userLocation.value.lat
)
storeList.value = response || []
emptyDescription.value = storeList.value.length === 0
? '未查询到附近门店'
: ''
} catch (error) {
emptyDescription.value = '获取门店失败,请重试'
showRetryButton.value = true
} finally {
loading.value = false
}
}
// 重试定位
const retryGetLocation = () => {
storeList.value = []
emptyDescription.value = '正在重新获取位置...'
showRetryButton.value = false
initAmapLocation()
}
// 确认城市
const confirmCity = () => {
if (!cityName.value) {
ElMessage.warning('请输入城市名称')
return
}
showCitySelectDialog.value = false
loading.value = true
fetchStoreByCity(cityName.value)
}
// 按城市获取门店
const fetchStoreByCity = async (city: string) => {
try {
const response = await StroeApi.getStoreListByCity(city)
storeList.value = response || []
emptyDescription.value = storeList.value.length === 0
? `未查询到${city}的门店`
: ''
} catch (error) {
emptyDescription.value = '获取门店失败,请重试'
} finally {
loading.value = false
}
}
// 选择门店并提交订单
const handleSelectStore = async (store: KfcStore) => {
selectedStore.value = store
// 校验code是否存在理论上此时code必存在做双重保险
if (!currentCode.value) {
ElMessage.error('缺少必要参数code请刷新页面重试')
return
}
// 调用提交订单接口
await submitOrder()
}
// 提交订单接口核心传入code和门店参数
const submitOrder = async () => {
if (!selectedStore.value) return
const loadingInstance = ElLoading.service({
lock: true,
text: '提交订单中...',
background: 'rgba(0, 0, 0, 0.7)'
})
try {
// 构造订单参数包含code和门店信息
const orderParams = {
orderNo: currentCode.value, // 传入code
storeName: selectedStore.value.storename, // 门店名称
}
// 调用订单接口
const result = await AgentOrderApi.submitOrder(orderParams)
console.log(result.mealCode)
if (result.agentOrderNo) {
ElMessage.success(`订单提交成功!订单号:${result.agentOrderNo}`)
alert("下单成功,取餐码:"+result.mealCode)
// 可跳转至订单详情页
// router.push(`/order/detail?orderNo=${result.orderNo}`)
} else {
ElMessage.error(`提交失败:${result.message || '未知错误'}`)
}
} catch (error) {
console.error('订单提交失败', error)
ElMessage.error('网络异常,订单提交失败,请重试')
} finally {
loadingInstance.close()
}
}
// 页面卸载时销毁定位实例
onUnmounted(() => {
if (geolocation) {
geolocation.destroy()
}
})
</script>
<style scoped>
.store-page {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.page-header {
margin-bottom: 30px;
}
/* 搜索框样式 */
.search-box {
margin-top: 15px;
max-width: 500px;
}
.store-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
margin-top: 20px;
max-height: calc(100vh - 220px); /* 预留搜索框高度 */
overflow-y: auto;
padding-right: 10px;
}
.store-card {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 20px;
transition: all 0.3s ease;
}
.store-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.store-name {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
display: flex;
justify-content: space-between;
}
.distance-tag {
font-size: 14px;
color: #409eff;
background-color: #ecf5ff;
padding: 2px 8px;
border-radius: 12px;
}
.store-detail p {
display: flex;
align-items: center;
margin-bottom: 10px;
color: #666;
}
.icon {
margin-right: 8px;
color: #888;
}
.empty-state {
grid-column: 1 / -1;
padding: 60px 0;
text-align: center;
}
</style>

View File

@@ -31,13 +31,14 @@
// "vite-plugin-svg-icons/client"
],
"outDir": "target", // 请保留这个属性防止tsconfig.json文件报错
"typeRoots": ["./node_modules/@types/", "./types"]
"typeRoots": ["./node_modules/@types/", "./types","./src/types"]
},
"include": [
"src",
"types/**/*.d.ts",
"src/types/auto-imports.d.ts",
"src/types/auto-components.d.ts"
"src/types/auto-components.d.ts",
"src/types/**/*.d.ts"
],
"exclude": ["dist", "target", "node_modules"]
}