This commit is contained in:
zjf committed 2026-10-10 18:07:35 +08:00
1 parent 7670ae8329
commit 426f1fb515
15 files changed
+1590 -366

No files matched your search

+6 -1
View File
@@ -9,7 +9,9 @@ ENV = 'dev'
# ADMIN 服务地址
# VITE_ADMIN_PROXY_PATH = https://www.zianxin.com
VITE_ADMIN_PROXY_PATH = https://rj4jv5rwqs.by.takin.cc
# VITE_ADMIN_PROXY_PATH = https://654173oc0eq0.vicp.fun
VITE_ADMIN_PROXY_PATH = http://192.168.31.130:9999
#VITE_ADMIN_PROXY_PATH = https://rj4jv5rwqs.by.takin.cc
# 图片访问基础地址
VITE_IMAGE_BASE_PATH = https://www.zianxin.com/api
@@ -17,3 +19,6 @@ VITE_IMAGE_BASE_PATH = https://www.zianxin.com/api
# 代码生成服务地址
VITE_GEN_PROXY_PATH = http://47.96.250.9:5003
# 腾讯位置服务 Key(PC 地图选点)
VITE_TENCENT_MAP_KEY=EF4BZ-BT537-77HXI-HP6W3-NPTX7-HJFJW
+11 -2
View File
@@ -15,8 +15,14 @@ VITE_ADMIN_PROXY_PATH = http://localhost:9999/
# VITE_ADMIN_PROXY_PATH = http://192.168.101.196:9999 # 邓-本地
# VITE_ADMIN_PROXY_PATH = http://59.202.170.152:81/api # 正式环境线上
# 食堂配送:骑手角色编码(用于楼栋/骑手池可选用户列表,与 /admin/user/getUserListByRoleCodeAndDept 的 code 一致)
# VITE_CANTEEN_RIDER_ROLE_CODE = CANTEEN_RIDER
# ========== 内网穿透 / 微信 PC 扫码调试 ==========
# 日常开发请用 http://localhost:8888 ,不要用穿透域名跑 npm run dev(大文件会 ERR_CONTENT_LENGTH_MISMATCH)
# 微信回调须用穿透域名时:
# 1. 三方账号 redirectUrl 填 https://654173oc0eq0.vicp.fun
# 2. 花生壳映射到本机 8888
# 3. 执行 npm run preview:tunnel(build 后 preview,不要用 dev)
# VITE_TUNNEL_MODE = true
# VITE_TUNNEL_HOST = 654173oc0eq0.vicp.fun
# 图片访问基础地址
# VITE_IMAGE_BASE_PATH = http://123.157.250.162:8089/api/ # 测试线上
@@ -31,3 +37,6 @@ VITE_GEN_PROXY_PATH = http://192.168.110.156:5003
# 饭卡打印:本机打印助手 mock 开关(true=无助手/打印机也能演示打印全流程)
VITE_PLUGIN_MOCK = false
# 腾讯位置服务 Key(酒店等业务地图选点;与 .env 同源时可省略,此处便于本地覆盖)
VITE_TENCENT_MAP_KEY=EF4BZ-BT537-77HXI-HP6W3-NPTX7-HJFJW
+1 -1
View File
@@ -21,4 +21,4 @@ VITE_IMAGE_BASE_PATH = https://www.zianxin.com/api
VITE_GEN_PROXY_PATH = http://123.156.230.15:5003
# 腾讯位置服务 Key(PC 地图选点)
VITE_TENCENT_MAP_KEY=CGABZ-3MH66-6VGST-MEMS3-K6U3V-DGBKA
VITE_TENCENT_MAP_KEY=EF4BZ-BT537-77HXI-HP6W3-NPTX7-HJFJW
+18
View File
@@ -16,6 +16,24 @@ export function fetchList(query?: Object) {
})
}
/** App/联动:下级区域 */
export function fetchChildren(query?: Object) {
return request({
url: '/admin/sysArea/app/children',
method: 'get',
params: query
})
}
/** App:按名称查区域 */
export function searchArea(query?: Object) {
return request({
url: '/admin/sysArea/app/search',
method: 'get',
params: query
})
}
export function addObj(obj?: Object) {
return request({
url: '/admin/sysArea',
+4 -3
View File
@@ -56,11 +56,12 @@ const optionProps: CascaderProps = {
value: 'adcode',
lazy: props.lazy,
lazyLoad: async (node: Node, resolve: Resolve) => {
// 初始化数据
// 初始化数据;pid 用 adcode(树节点 id 也是 adcode),不要用库表主键
const {level} = node
if (level < props.type) {
const {data} = await fetchTree({pid: node.data.id || 100000})
resolve(data)
const parentAdcode = node?.data?.adcode ?? node?.data?.id ?? node?.value ?? 100000
const {data} = await fetchTree({pid: parentAdcode})
resolve(data || [])
} else {
resolve([])
}
+51 -43
View File
@@ -53,9 +53,35 @@ const imageBaseUrl = import.meta.env.VITE_IMAGE_BASE_PATH
const dialogVisible = ref(false)
const fileList = ref<any>([])
const dialogImageUrl = ref("")
/** 上传成功写回 v-model 时跳过 watch,避免多选并发上传时冲掉未完成的文件 */
const syncingFromUpload = ref(false)
// 获取 el-form 组件上下文
const formContext = inject(formContextKey, void 0)
const fileUrlOf = (item: any) => {
const raw = item?.response?.data?.url ?? item?.fileUrl ?? ""
return typeof raw === "string" ? raw : ""
}
const toDisplayUrl = (path: string) => {
if (!path) return ""
return path.startsWith("http") ? path : `${imageBaseUrl || ""}${path}`
}
const collectUrls = (files: any[]) =>
(files || [])
.map((item) => fileUrlOf(item))
.filter(Boolean)
.join(",")
const setModelUrls = (urls: string) => {
syncingFromUpload.value = true
imageUrls.value = urls
nextTick(() => {
syncingFromUpload.value = false
})
}
// 自定义上传方法
const handleHttpUpload = async (options: UploadRequestOptions) => {
let formData = new FormData()
@@ -72,6 +98,7 @@ const handleHttpUpload = async (options: UploadRequestOptions) => {
if (res.code == 0) {
return res
}
options.onError(new Error(res.msg || "上传失败") as any)
} catch (error) {
options.onError(error as any)
}
@@ -102,22 +129,15 @@ const beforeUpload: UploadProps["beforeUpload"] = (rawFile) => {
}
/**
* @description 图片上传成功
* @description 图片上传成功(多选时会多次回调,不能据此重建 fileList)
* */
const uploadSuccess = (response: any, uploadFile: UploadFile, uploadFiles: any) => {
console.log("success", uploadFiles)
const urls = uploadFiles
.map((item: any) => {
// 处理新上传和已有的图片URL
return item.response ? item.response.data.url : item.fileUrl
})
.filter(Boolean)
.join(",")
imageUrls.value = urls
console.log("fileList.value", fileList.value)
console.log("urls", urls)
const url = response?.data?.url
if (url) {
;(uploadFile as any).fileUrl = url
uploadFile.url = toDisplayUrl(url)
}
setModelUrls(collectUrls(uploadFiles))
ElNotification({
title: "温馨提示",
@@ -128,19 +148,13 @@ const uploadSuccess = (response: any, uploadFile: UploadFile, uploadFiles: any)
// 预览
const handlePictureCardPreview = (uploadFile: any) => {
dialogImageUrl.value = uploadFile.url
dialogImageUrl.value = uploadFile.url || toDisplayUrl(fileUrlOf(uploadFile))
dialogVisible.value = true
}
// 移除
const handleRemove = (uploadFile: any, uploadFiles: any[]) => {
const urls = uploadFiles
.map((item) => {
return item.response ? item.response.data.url : item.fileUrl
})
.filter(Boolean)
.join(",")
imageUrls.value = urls
const handleRemove = (_uploadFile: any, uploadFiles: any[]) => {
setModelUrls(collectUrls(uploadFiles))
}
/**
@@ -162,6 +176,8 @@ const self_disabled = computed(() => {
watch(
() => imageUrls.value,
(newValue) => {
// 上传过程中写回 model 时不重建列表,否则多选只会留下已成功的那一张
if (syncingFromUpload.value) return
if (!newValue || Object.prototype.toString.call(newValue) !== "[object String]") {
fileList.value = []
return
@@ -170,17 +186,14 @@ watch(
if (!newValue.match(/x-oss-process/g)) {
fileList.value = newValue
.split(",")
.map((item: string) => {
const url = item.startsWith("http") ? item : imageBaseUrl + item
return {
name: item.split("/").pop() || "",
url: url,
fileUrl: item,
}
})
.map((item: string) => item.trim())
.filter(Boolean)
.map((item: string) => ({
name: item.split("/").pop() || "",
url: toDisplayUrl(item),
fileUrl: item,
}))
.filter((item) => item.url)
console.log("fileList.value", fileList.value)
} else {
fileList.value = newValue
.split("?")
@@ -188,17 +201,12 @@ watch(
(item) =>
item.endsWith(".jpg") || item.endsWith(".png") || item.endsWith(".gif") || item.endsWith(".jpeg") || item.endsWith(".webp"),
)
.map((item: string) => {
const url = item.startsWith("http") ? item : imageBaseUrl + item
return {
name: item.split("/").pop() || "",
url: url,
fileUrl: item,
}
})
.map((item: string) => ({
name: item.split("/").pop() || "",
url: toDisplayUrl(item),
fileUrl: item,
}))
.filter((item) => item.url)
console.log("fileList.value", fileList.value)
}
},
{ immediate: true },
File diff suppressed because it is too large. Load diff
+6 -2
View File
@@ -53,13 +53,16 @@ const props = withDefaults(
clearable?: boolean
showCoordinates?: boolean
returnFormat?: 'object' | 'coordinates' | 'address'
/** 无坐标时,用地图打开前的地址/城市文案做初始定位(如「嘉兴市南湖区」) */
preferAddress?: string
}>(),
{
placeholder: "请点击选择地图位置",
disabled: false,
clearable: true,
showCoordinates: true,
returnFormat: 'object'
returnFormat: 'object',
preferAddress: "",
}
)
@@ -86,7 +89,8 @@ const handleClear = () => {
// 打开地图选择器
const handleMapClick = () => {
if (props.disabled) return
mapDialogRef.value?.open(state.selectedLocation, state.selectAddress)
const hint = (props.preferAddress || state.selectAddress || "").trim()
mapDialogRef.value?.open(state.selectedLocation, state.selectAddress, hint)
}
// 处理位置确认
+77 -15
View File
@@ -65,6 +65,37 @@ export function jsonp(url: string, options: JsonpOptions = {}): Promise<any> {
})
}
/** 开发环境走 Vite /qqmap 代理,绕过 localhost WebService 域名白名单 110 */
const useQqMapDevProxy = import.meta.env.DEV
/**
* 请求腾讯 WebService:开发用同源 fetch+代理,生产用 JSONP(Referer=真实域名)
* @param pathWithQuery 如 /ws/place/v1/suggestion/?keyword=xx&key=yy
*/
async function requestTencentWs(pathWithQuery: string, timeout = 10000): Promise<any> {
if (useQqMapDevProxy) {
const path = pathWithQuery.startsWith("/") ? pathWithQuery : `/${pathWithQuery}`
// 代理侧用 JSON,不要 jsonp
const qs = path.includes("?")
? path.replace(/([?&])output=jsonp\b/i, "$1output=json").replace(/\?$/, "")
: path
const hasOutput = /[?&]output=/.test(qs)
const url = `/qqmap${qs}${hasOutput ? "" : (qs.includes("?") ? "&" : "?") + "output=json"}`
const ctrl = new AbortController()
const timer = setTimeout(() => ctrl.abort(), timeout)
try {
const res = await fetch(url, { signal: ctrl.signal })
if (!res.ok) throw new Error(`地图接口 HTTP ${res.status}`)
return await res.json()
} finally {
clearTimeout(timer)
}
}
const abs = `https://apis.map.qq.com${pathWithQuery.startsWith("/") ? pathWithQuery : `/${pathWithQuery}`}`
const withJsonp = /[?&]output=/.test(abs) ? abs : `${abs}${abs.includes("?") ? "&" : "?"}output=jsonp`
return jsonp(withJsonp.replace(/output=json\b/i, "output=jsonp"), { timeout })
}
/**
* 腾讯地图 API JSONP 请求封装
*/
@@ -82,48 +113,79 @@ export class TencentMapAPI {
* @returns Promise
*/
async reverseGeocode(lat: number, lng: number): Promise<any> {
const url = `https://apis.map.qq.com/ws/geocoder/v1/?location=${lat},${lng}&key=${this.apiKey}&get_poi=1&output=jsonp`
try {
const data = await jsonp(url)
return data
return await requestTencentWs(
`/ws/geocoder/v1/?location=${lat},${lng}&key=${this.apiKey}&get_poi=1&output=jsonp`,
)
} catch (error) {
console.error("逆地址解析失败:", error)
throw error
}
}
/** IP 粗定位(无浏览器定位权限时的兜底) */
async ipLocation(): Promise<any> {
try {
return await requestTencentWs(`/ws/location/v1/ip?key=${this.apiKey}&output=jsonp`, 8000)
} catch (error) {
console.error("IP定位失败:", error)
throw error
}
}
/**
* 地址搜索
* @param keyword 搜索关键词
* @param boundary 搜索范围
* @returns Promise
*/
async searchLocation(keyword: string, boundary: string = "region(全国,0)"): Promise<any> {
const url = `https://apis.map.qq.com/ws/place/v1/search?boundary=${encodeURIComponent(boundary)}&keyword=${encodeURIComponent(
keyword,
)}&page_size=10&page_index=1&key=${this.apiKey}&output=jsonp`
async searchLocation(keyword: string, boundary: string = "region(全国,0)", pageSize = 20): Promise<any> {
try {
const data = await jsonp(url)
return data
return await requestTencentWs(
`/ws/place/v1/search?boundary=${encodeURIComponent(boundary)}&keyword=${encodeURIComponent(
keyword,
)}&page_size=${pageSize}&page_index=1&key=${this.apiKey}&output=jsonp`,
)
} catch (error) {
console.error("地址搜索失败:", error)
throw error
}
}
/**
* 关键词输入提示(模糊联想)
* @see https://lbs.qq.com/service/webService/webServiceGuide/webServiceSuggestion
*/
async suggestLocation(keyword: string, region = "全国"): Promise<any> {
const params = [
`keyword=${encodeURIComponent(keyword)}`,
`region=${encodeURIComponent(region || "全国")}`,
"region_fix=0",
// policy=0 更偏地点列表;1 偏检索词补全,可选条数往往更少
"policy=0",
"page_index=1",
"page_size=20",
`key=${this.apiKey}`,
"output=jsonp",
]
try {
return await requestTencentWs(`/ws/place/v1/suggestion/?${params.join("&")}`, 8000)
} catch (error) {
console.error("地址联想失败:", error)
throw error
}
}
/**
* 地址解析(正向地理编码)
* @param address 地址
* @returns Promise
*/
async geocode(address: string): Promise<any> {
const url = `https://apis.map.qq.com/ws/geocoder/v1/?address=${encodeURIComponent(address)}&key=${this.apiKey}&output=jsonp`
try {
const data = await jsonp(url)
return data
return await requestTencentWs(
`/ws/geocoder/v1/?address=${encodeURIComponent(address)}&key=${this.apiKey}&output=jsonp`,
)
} catch (error) {
console.error("地址解析失败:", error)
throw error
+76 -14
View File
@@ -5,7 +5,21 @@
<el-row>
<el-col :span="24" class="mb20" v-if="!form.id">
<el-form-item :label="t('area.pid')" prop="pid">
<china-area class="w-full" :placeholder="t('area.inputPidByTip')" :plus="true" @change="handleChange"/>
<div v-if="presetParentName" class="mb-2 text-sm text-gray-500">
已预填上级:<el-tag size="small">{{ presetParentName }}</el-tag>
<span class="ml-2">编码 {{ form.pid }},也可重新选择</span>
</div>
<china-area
class="w-full"
:placeholder="t('area.inputPidByTip')"
:plus="true"
@change="handleChange"
/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20" v-else>
<el-form-item label="上级">
<span>{{ form.parentName || form.pid || "-" }}</span>
</el-form-item>
</el-col>
@@ -94,10 +108,13 @@ const area_type_dict = [
{value: '4', label: '街道'}
]
const presetParentName = ref("")
// 提交表单数据
const form = reactive({
id: '',
pid: 100000,
pid: 100000 as number | string,
parentName: '',
name: '',
letter: '',
adcode: 0,
@@ -109,6 +126,12 @@ const form = reactive({
cityCode: '',
});
const suggestChildType = (parentType?: string) => {
const n = Number(parentType)
if (!Number.isFinite(n)) return "2"
return String(Math.min(n + 1, 4))
}
// 定义校验规则
const dataRules = ref({
name: [
@@ -123,30 +146,52 @@ const dataRules = ref({
],
adcode: [
{ validator: rule.overLength, trigger: 'blur' },
{required: true, message: '编码不能为空', trigger: 'blur'},
{ required: true, message: '编码不能为空', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateExist(rule, value, callback, form.id !== '');
if (value == null || value === '' || Number(value) <= 0) {
callback(new Error('请填写正确区划编码(如嘉兴市 330400),不能为 0'))
return
}
validateExist(rule, value, callback, form.id !== '')
},
trigger: 'blur',
},
]
});
// 打开弹窗
const openDialog = (id: string) => {
// 打开弹窗;parent 用于「新增下级」预填
const openDialog = (
id?: string | number,
parent?: { pid: number | string; parentName?: string; parentType?: string },
adcode?: string | number,
) => {
visible.value = true
form.id = ''
presetParentName.value = ""
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
form.pid = 100000
form.areaType = "2"
form.areaStatus = "1"
form.hot = "0"
form.areaSort = 0
if (parent?.pid != null) {
form.pid = parent.pid
presetParentName.value = parent.parentName || String(parent.pid)
form.areaType = suggestChildType(parent.parentType)
}
});
// 获取sysArea信息
if (id) {
form.id = id
getsysAreaData(id)
form.id = String(id)
getsysAreaData({ id })
return
}
if (adcode) {
getsysAreaData({ adcode })
}
};
@@ -158,7 +203,16 @@ const onSubmit = async () => {
try {
loading.value = true;
form.id ? await putObj(form) : await addObj(form);
const payload: any = { ...form }
delete payload.parentName
delete payload.hasChildren
if (!payload.id) delete payload.id
payload.pid = payload.pid === "" || payload.pid == null ? 100000 : Number(payload.pid)
// 移动端 App 接口依赖有效区划;保存时默认置为有效
if (payload.areaStatus == null || payload.areaStatus === "") {
payload.areaStatus = "1"
}
form.id ? await putObj(payload) : await addObj(payload);
useMessage().success(form.id ? t('common.editSuccessText') : t('common.addSuccessText'));
visible.value = false;
emit('refresh');
@@ -171,11 +225,19 @@ const onSubmit = async () => {
// 初始化表单数据
const getsysAreaData = (id: string) => {
// 获取数据
const getsysAreaData = (query: { id?: string | number; adcode?: string | number }) => {
loading.value = true
getObj({id: id}).then((res: any) => {
Object.assign(form, res.data)
getObj(query).then(async (res: any) => {
Object.assign(form, res.data || {})
if (form.id != null) form.id = String(form.id)
if (form.pid) {
try {
const p: any = await getObj({ adcode: form.pid })
form.parentName = p?.data?.name || form.parentName || ""
} catch {
/* ignore */
}
}
}).finally(() => {
loading.value = false
})
+1
View File
@@ -9,6 +9,7 @@ export default {
areaSort:'排序值',
hot:'热门',
areaStatus:'有效',
parentName: '上级',
inputAdCodeByTip: '请选择编码',
inputPidByTip: '请选择父级地区',
inputNameByTip: '请输入地区名称',
+413 -101
View File
@@ -1,105 +1,184 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<SearchPanel
ref="queryRef"
:model="state.queryForm"
:loading="Boolean(state.loading)"
:search-text="$t('common.queryBtn')"
:reset-text="$t('common.resetBtn')"
@search="getDataList"
@reset="resetQuery"
<div class="layout-padding area-page">
<div class="layout-padding-auto layout-padding-view area-layout">
<!-- 左侧:行政区划树 -->
<div class="area-tree-pane">
<div class="area-tree-pane__head">
<span class="area-tree-pane__title">行政区划</span>
<div class="area-tree-pane__actions">
<el-button type="primary" size="small" @click="openAdd()">新增</el-button>
<el-button type="primary" plain size="small" :disabled="!currentNode" @click="openEditCurrent()">修改</el-button>
<el-button link type="primary" @click="reloadTree">刷新</el-button>
</div>
</div>
<el-input
v-model="treeFilter"
clearable
placeholder="过滤树节点"
class="mb-2"
@input="onTreeFilter"
/>
<el-tree
:key="treeKey"
ref="treeRef"
class="area-tree"
node-key="id"
lazy
highlight-current
:indent="16"
:load="loadTreeNode"
:props="treeProps"
:expand-on-click-node="false"
:filter-node-method="filterTreeNode"
@node-click="onTreeClick"
>
<el-form-item :label="t('area.pid')" prop="adcode">
<china-area :type="3" :placeholder="t('area.inputPidByTip')" v-model="pid" :plus="true" @change="handleChange" />
</el-form-item>
<el-form-item :label="t('area.name')" prop="name">
<el-input :placeholder="t('area.inputNameByTip')" v-model="state.queryForm.name" />
</el-form-item>
</SearchPanel>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()" v-auth="'sys_sysArea_add'">
{{ $t("common.addBtn") }}
</el-button>
<el-button plain :disabled="multiple" icon="Delete" type="primary" v-auth="'sys_sysArea_del'" @click="handleDelete(selectObjs)">
{{ $t("common.delBtn") }}
</el-button>
<template #default="{ data }">
<span class="area-tree-node">
<el-tag size="small" effect="plain" :type="areaTypeTag(data.areaType)" class="mr-1">
{{ areaTypeLabel(data.areaType) }}
</el-tag>
<span class="area-tree-node__name" :title="data.name || data.label">{{ data.name || data.label }}</span>
<el-button class="area-tree-node__edit" link type="primary" @click.stop="openEdit(data)">修改</el-button>
</span>
</template>
</el-tree>
</div>
<!-- 右侧:当前节点下的子级列表 -->
<div class="area-table-pane">
<div class="area-breadcrumb mb-3">
<span class="text-gray-500 mr-2">当前:</span>
<el-breadcrumb separator="/">
<el-breadcrumb-item>
<a href="javascript:;" @click="goRoot">中国</a>
</el-breadcrumb-item>
<el-breadcrumb-item v-for="(p, i) in breadcrumb" :key="p.adcode">
<a v-if="i < breadcrumb.length - 1" href="javascript:;" @click="goBreadcrumb(i)">{{ p.name }}</a>
<span v-else>{{ p.name }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
<el-tag v-if="currentNode" size="small" class="ml-2" :type="areaTypeTag(currentNode.areaType)">
{{ areaTypeLabel(currentNode.areaType) }} · {{ currentNode.adcode }}
</el-tag>
<el-button v-if="currentNode" class="ml-2" type="primary" link @click="openEditCurrent()">修改当前</el-button>
</div>
<div class="area-toolbar mb8">
<el-button type="primary" @click="openAdd()">新增</el-button>
<el-button type="primary" plain :disabled="!currentNode" @click="openEditCurrent()">修改当前</el-button>
<el-button type="primary" plain @click="openAddChild()">在当前下新增</el-button>
<el-button plain :disabled="multiple" type="danger" @click="handleDelete(selectObjs)">删除</el-button>
<right-toolbar
v-model:showSearch="showSearch"
:export="'sys_sysArea_export'"
@exportExcel="exportExcel"
class="ml10 mr20"
style="float: right"
class="ml10"
style="margin-left: auto"
@queryTable="getDataList"
></right-toolbar>
/>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
@selection-change="selectionChangHandle"
@sort-change="sortChangeHandle"
>
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="#" width="40" />
<el-table-column prop="name" :label="t('area.name')" show-overflow-tooltip />
<el-table-column prop="adcode" :label="t('area.adcode')" show-overflow-tooltip />
<el-table-column prop="areaType" :label="t('area.areaType')" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="area_type_dict" :value="scope.row.areaType"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="hot" :label="t('area.hot')" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="yes_no_type" :value="scope.row.hot"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="areaStatus" :label="t('area.areaStatus')" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="yes_no_type" :value="scope.row.areaStatus"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="areaSort" :label="t('area.areaSort')" width="100" sortable="custom" show-overflow-tooltip />
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button icon="edit-pen" text type="primary" v-auth="'sys_sysArea_edit'" @click="formDialogRef.openDialog(scope.row.id)"
>{{ $t("common.editBtn") }}
</el-button>
<el-button icon="delete" text type="primary" v-auth="'sys_sysArea_del'" @click="handleDelete([scope.row.id])"
>{{ $t("common.delBtn") }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
<el-row v-show="showSearch">
<SearchPanel
ref="queryRef"
:model="state.queryForm"
:loading="Boolean(state.loading)"
:search-text="$t('common.queryBtn')"
:reset-text="$t('common.resetBtn')"
@search="getDataList"
@reset="resetQuery"
>
<el-form-item :label="t('area.name')" prop="name">
<el-input :placeholder="t('area.inputNameByTip')" v-model="state.queryForm.name" clearable />
</el-form-item>
<el-form-item label="类型" prop="areaType">
<el-select v-model="state.queryForm.areaType" clearable placeholder="全部类型" style="width: 140px">
<el-option v-for="item in area_type_dict" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</SearchPanel>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
border
row-key="id"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
@selection-change="selectionChangHandle"
@sort-change="sortChangeHandle"
@row-dblclick="openEdit"
>
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="name" :label="t('area.name')" min-width="160" show-overflow-tooltip>
<template #default="scope">
<div class="name-cell">
<span class="name-cell__text">{{ scope.row.name }}</span>
<el-tag v-if="scope.row.hasChildren" size="small" type="info" effect="plain">有下级</el-tag>
</div>
</template>
</el-table-column>
<el-table-column prop="parentName" label="上级" min-width="120" show-overflow-tooltip>
<template #default="scope">
<span class="text-gray-500">{{ scope.row.parentName || (scope.row.pid === 100000 ? "中国" : "-") }}</span>
</template>
</el-table-column>
<el-table-column prop="adcode" :label="t('area.adcode')" width="120" show-overflow-tooltip />
<el-table-column prop="areaType" :label="t('area.areaType')" width="100" align="center">
<template #default="scope">
<el-tag size="small" :type="areaTypeTag(scope.row.areaType)">{{ areaTypeLabel(scope.row.areaType) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="hot" :label="t('area.hot')" width="80" align="center">
<template #default="scope">
<dict-tag :options="yes_no_type" :value="scope.row.hot" />
</template>
</el-table-column>
<el-table-column prop="areaStatus" :label="t('area.areaStatus')" width="80" align="center">
<template #default="scope">
<dict-tag :options="yes_no_type" :value="scope.row.areaStatus" />
</template>
</el-table-column>
<el-table-column prop="areaSort" :label="t('area.areaSort')" width="90" sortable="custom" />
<el-table-column :label="$t('common.action')" width="280" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="openEdit(scope.row)">修改</el-button>
<el-button type="primary" link @click="openAddChild(scope.row)">新增下级</el-button>
<el-button
v-if="scope.row.hasChildren"
type="primary"
link
@click="enterChildren(scope.row)"
>
下级
</el-button>
<el-button type="danger" link @click="handleDelete([scope.row.id])">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
</div>
<!-- 编辑、新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
<form-dialog ref="formDialogRef" @refresh="onFormRefresh" />
</div>
</template>
<script setup lang="ts" name="systemSysArea">
import { BasicTableProps, useTable } from "/@/hooks/table"
import { delObjs, fetchList } from "/@/api/admin/sysArea"
import { delObjs, fetchList, fetchChildren } from "/@/api/admin/sysArea"
import { useMessage, useMessageBox } from "/@/hooks/message"
import { useDict } from "/@/hooks/dict"
import { useI18n } from "vue-i18n"
const { t } = useI18n()
// 省市区查询组件
const ChinaArea = defineAsyncComponent(() => import("/@/components/ChinaArea/index.vue"))
// 引入组件
const FormDialog = defineAsyncComponent(() => import("./form.vue"))
// 定义查询字典
const { yes_no_type } = useDict("yes_no_type")
const ROOT_ADCODE = 100000
const area_type_dict = [
{ value: "0", label: "国家" },
{ value: "1", label: "省份" },
@@ -107,57 +186,188 @@ const area_type_dict = [
{ value: "3", label: "县区" },
{ value: "4", label: "街道" },
]
// 定义变量内容
const areaTypeLabel = (v: string | number) => area_type_dict.find((i) => i.value === String(v))?.label || "-"
const areaTypeTag = (v: string | number) => {
const map: Record<string, string> = { "0": "danger", "1": "", "2": "success", "3": "warning", "4": "info" }
return (map[String(v)] || "info") as any
}
const formDialogRef = ref()
// 搜索变量
const queryRef = ref()
const treeRef = ref()
const showSearch = ref(true)
// 多选变量
const selectObjs = ref([]) as any
const multiple = ref(true)
const pid = ref()
const treeFilter = ref("")
const treeKey = ref(0)
type Crumb = { adcode: number; name: string; areaType?: string; dbId?: string | number }
const breadcrumb = ref<Crumb[]>([])
const currentNode = ref<Crumb | null>(null)
const treeProps = { label: "name", children: "children", isLeaf: "isLeaf" }
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
adcode: "",
adcode: ROOT_ADCODE,
name: "",
areaType: "",
},
ascs: ["adcode"],
pageList: fetchList,
})
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state)
// 清空搜索条件
const normalizeTreeNode = (raw: any) => {
const adcode = Number(raw.adcode ?? raw.id)
const name = raw.name || raw.label || String(adcode)
const areaType = String(raw.areaType ?? "")
const hasChildren = raw.hasChildren === true || raw.hasChildren === "true"
// 街道为末级;其余以是否有子级为准,不能因空 children 误判成叶子(否则展开箭头没有、下级看不见)
const isLeaf = areaType === "4" || !hasChildren
return {
id: adcode,
dbId: raw.id,
adcode,
pid: raw.pid,
name,
label: name,
areaType,
hot: raw.hot,
hasChildren,
isLeaf,
}
}
const loadTreeNode = async (node: any, resolve: (data: any[]) => void) => {
try {
const pid = node.level === 0 ? ROOT_ADCODE : Number(node.data?.adcode ?? node.data?.id)
if (!pid) {
resolve([])
return
}
const res: any = await fetchChildren({ pid })
resolve((res?.data || []).map(normalizeTreeNode))
} catch {
resolve([])
}
}
const filterTreeNode = (value: string, data: any) => {
if (!value) return true
return String(data.name || data.label || "").includes(value)
}
const onTreeFilter = (val: string) => {
treeRef.value?.filter(val)
}
const reloadTree = () => {
treeKey.value += 1
// 强制重建:通过 key 不够时清空并重新点根
breadcrumb.value = []
currentNode.value = null
state.queryForm.adcode = ROOT_ADCODE
getDataList()
nextTick(() => {
// el-tree lazy 无直接 reload,用替换 ref 外层 key
})
}
const onTreeClick = (data: any) => {
const adcode = Number(data.adcode ?? data.id)
const name = data.name || data.label
currentNode.value = { adcode, name, areaType: data.areaType, dbId: data.dbId }
// 简化面包屑:点击树节点时用当前节点作为末级(完整路径依赖用户逐级点选)
const idx = breadcrumb.value.findIndex((b) => b.adcode === adcode)
if (idx >= 0) {
breadcrumb.value = breadcrumb.value.slice(0, idx + 1)
} else {
breadcrumb.value = [...breadcrumb.value.filter((b) => b.adcode !== adcode), { adcode, name, areaType: data.areaType }]
}
state.queryForm.adcode = adcode
getDataList()
}
const goRoot = () => {
breadcrumb.value = []
currentNode.value = null
state.queryForm.adcode = ROOT_ADCODE
treeRef.value?.setCurrentKey?.(null)
getDataList()
}
const goBreadcrumb = (index: number) => {
breadcrumb.value = breadcrumb.value.slice(0, index + 1)
const last = breadcrumb.value[breadcrumb.value.length - 1]
currentNode.value = last
state.queryForm.adcode = last.adcode
getDataList()
}
const enterChildren = (row: any) => {
const adcode = Number(row.adcode)
const crumb = { adcode, name: row.name, areaType: row.areaType, dbId: row.id ?? row.dbId }
currentNode.value = crumb
if (!breadcrumb.value.some((b) => b.adcode === adcode)) {
breadcrumb.value = [...breadcrumb.value, crumb]
}
state.queryForm.adcode = adcode
getDataList()
}
const openAdd = () => {
formDialogRef.value?.openDialog()
}
const openEdit = (row: any) => {
const pk = row?.dbId ?? row?.id
const adcode = row?.adcode
formDialogRef.value?.openDialog(pk, undefined, adcode)
}
const openEditCurrent = () => {
if (!currentNode.value) return
formDialogRef.value?.openDialog(currentNode.value.dbId, undefined, currentNode.value.adcode)
}
const openAddChild = (row?: any) => {
const parentAdcode = row?.adcode ?? currentNode.value?.adcode ?? ROOT_ADCODE
const parentName = row?.name ?? currentNode.value?.name ?? "中国"
const parentType = row?.areaType ?? currentNode.value?.areaType ?? "0"
formDialogRef.value?.openDialog(undefined, { pid: parentAdcode, parentName, parentType })
}
const onFormRefresh = () => {
getDataList()
treeKey.value += 1
}
const resetQuery = () => {
// 清空搜索条件
queryRef.value?.resetFields()
pid.value = ""
// 清空多选
state.queryForm.name = ""
state.queryForm.areaType = ""
selectObjs.value = []
getDataList()
}
// 导出excel
const exportExcel = () => {
downBlobFile("/admin/sysArea/export", Object.assign(state.queryForm, { ids: selectObjs }), "行政区域.xlsx")
downBlobFile("/admin/sysArea/export", Object.assign({}, state.queryForm, { ids: selectObjs }), "行政区域.xlsx")
}
// 多选事件
const selectionChangHandle = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({ id }) => id)
multiple.value = !objs.length
}
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t("common.delConfirmText"))
} catch {
return
}
try {
await delObjs(ids)
getDataList()
@@ -166,10 +376,112 @@ const handleDelete = async (ids: string[]) => {
useMessage().error(err.msg)
}
}
// 地区查询
const handleChange = (data: string) => {
let dataArray = data.split(",")
state.queryForm.adcode = dataArray[dataArray.length - 1]
}
</script>
<style scoped lang="scss">
.area-page {
height: 100%;
overflow: hidden;
}
.area-page :deep(.layout-padding-auto) {
height: 100%;
min-height: 0;
}
.area-layout {
display: flex;
gap: 12px;
height: 100%;
min-height: 0;
overflow: hidden;
}
.area-tree-pane {
width: 300px;
flex-shrink: 0;
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
padding: 12px;
background: var(--el-bg-color);
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
overflow: hidden;
}
.area-tree-pane__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
flex-shrink: 0;
gap: 8px;
}
.area-tree-pane__actions {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.area-toolbar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
position: sticky;
top: 0;
z-index: 2;
padding: 8px 0;
background: var(--el-bg-color);
}
.area-tree-pane__title {
font-weight: 600;
font-size: 14px;
}
.area-tree {
flex: 1;
min-height: 0;
overflow: auto;
}
.area-tree-node {
display: inline-flex;
align-items: center;
gap: 4px;
max-width: 240px;
width: 100%;
}
.area-tree-node__name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
}
.area-tree-node__edit {
flex-shrink: 0;
padding: 0 4px;
opacity: 0.85;
}
.area-table-pane {
flex: 1;
min-width: 0;
min-height: 0;
height: 100%;
overflow: auto;
}
.area-breadcrumb {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px;
padding: 8px 12px;
background: var(--el-fill-color-light);
border-radius: 6px;
}
.name-cell {
display: flex;
align-items: center;
gap: 8px;
}
.name-cell__text {
font-weight: 600;
}
</style>
+13
View File
@@ -24,6 +24,14 @@
</el-descriptions-item>
</template>
<el-descriptions-item label="详细地址" :span="2">{{ hotel.address || "-" }}</el-descriptions-item>
<el-descriptions-item label="位置区域">
{{ hotel.district || "-" }}
<span v-if="hotel.districtAdcode" class="text-gray-400">({{ hotel.districtAdcode }})</span>
</el-descriptions-item>
<el-descriptions-item label="商圈地标">{{ hotel.bizArea || "-" }}</el-descriptions-item>
<el-descriptions-item label="星级档次">{{ starLabel(hotel.starLevel) }}</el-descriptions-item>
<el-descriptions-item label="评分">{{ hotel.score != null ? hotel.score : "-" }}</el-descriptions-item>
<el-descriptions-item label="设施标签" :span="2">{{ hotel.facilityTags || "-" }}</el-descriptions-item>
<el-descriptions-item label="坐标">
<span v-if="hotel.longitude != null && hotel.latitude != null">
{{ hotel.longitude }}, {{ hotel.latitude }}
@@ -522,6 +530,11 @@ const orderForm = reactive({
const title = computed(() => (hotel.value?.hotelName ? `酒店详情 - ${hotel.value.hotelName}` : "酒店详情"))
const isLocal = computed(() => String(hotel.value?.sheetName || "").toLowerCase() === "local")
const starLabel = (lv: any) => {
const map: Record<number, string> = { 1: "民宿", 2: "经济", 3: "舒适", 4: "高档", 5: "五星" }
const n = Number(lv)
return map[n] || "-"
}
const localSaving = ref(false)
const roomDialogVisible = ref(false)
+188 -5
View File
@@ -69,6 +69,34 @@
<div class="form-tip">支持多张,可连续选择上传;保存为逗号分隔 URL</div>
</el-form-item>
</el-col>
<el-col :span="12" class="form-col">
<el-form-item label="位置区域">
<china-area
v-if="canEditProfile && districtPathReady"
:key="districtCascaderKey"
v-model="districtPath"
class="w-full"
:type="3"
:plus="true"
placeholder="先选省/市/区县,地图会跟到该区域"
@change="onDistrictChange"
/>
<div v-else-if="canEditProfile" class="text-gray-400 text-sm">区域加载中…</div>
<div v-else class="text-gray-500">{{ form.district || "-" }} {{ form.districtAdcode ? `(${form.districtAdcode})` : "" }}</div>
<div v-if="form.district" class="form-tip">已选:{{ form.district }}({{ form.districtAdcode || "-" }})</div>
</el-form-item>
</el-col>
<el-col :span="12" class="form-col">
<el-form-item label="商圈地标">
<el-input
v-model="form.bizArea"
class="w-full"
placeholder="如:嘉兴南站附近"
maxlength="128"
:disabled="!canEditProfile"
/>
</el-form-item>
</el-col>
<el-col :span="24" class="form-col">
<el-form-item label="地图选点">
<div class="map-lng-lat-row">
@@ -77,7 +105,8 @@
class="map-pick-part"
:disabled="!canEditProfile"
:show-coordinates="false"
placeholder="搜索或点击地图选择位置"
:prefer-address="mapPreferAddress"
placeholder="先选区域,再点这里搜索定位"
@change="onMapChange"
/>
<div class="lng-lat-inline">
@@ -96,6 +125,7 @@
/>
</div>
</div>
<div class="form-tip">支持模糊搜索小区/酒店/路名;打开地图会按「位置区域」落到对应城市</div>
</el-form-item>
</el-col>
<el-col :span="24" class="form-col">
@@ -110,6 +140,48 @@
/>
</el-form-item>
</el-col>
<el-col :span="12" class="form-col">
<el-form-item label="星级档次">
<el-select v-model="form.starLevel" class="w-full" clearable placeholder="请选择" :disabled="!canEditProfile">
<el-option :value="5" label="五星" />
<el-option :value="4" label="高档" />
<el-option :value="3" label="舒适" />
<el-option :value="2" label="经济" />
<el-option :value="1" label="民宿" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="form-col">
<el-form-item label="评分">
<el-input-number
v-model="form.score"
class="w-full"
:min="0"
:max="5"
:precision="1"
:step="0.1"
controls-position="right"
:disabled="!canEditProfile"
/>
<div class="form-tip">0.0 ~ 5.0,移动端可按评分筛选</div>
</el-form-item>
</el-col>
<el-col :span="24" class="form-col">
<el-form-item label="设施标签">
<el-select
v-model="form.facilityTagList"
class="w-full"
multiple
filterable
allow-create
default-first-option
placeholder="含早餐 / 免费取消 / 免费停车 等"
:disabled="!canEditProfile"
>
<el-option v-for="t in facilityTagOptions" :key="t" :label="t" :value="t" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="form-col">
<el-form-item label="酒店简介">
<editor
@@ -469,6 +541,9 @@ import {
removeLocalDaily,
} from "/@/api/supermarket/smHotelMid"
import MapSelect from "/@/components/map/map-select.vue"
import { getObj as getAreaObj } from "/@/api/admin/sysArea"
const ChinaArea = defineAsyncComponent(() => import("/@/components/ChinaArea/index.vue"))
const emit = defineEmits(["refresh"])
const { sm_hotel_data_source } = useDict("sm_hotel_data_source")
@@ -482,6 +557,8 @@ const subSaving = ref(false)
const formRef = ref<FormInstance>()
const activeTab = ref("base")
const facilityTagOptions = ["含早餐", "免费取消", "免费停车", "立即确认", "限时优惠", "接站服务", "室内泳池", "健身中心"]
const defaultForm = () => ({
id: undefined as string | undefined,
hotelCode: "",
@@ -491,6 +568,12 @@ const defaultForm = () => ({
coverImage: "",
carouselImages: "",
address: "",
district: "",
districtAdcode: undefined as number | undefined,
bizArea: "",
starLevel: undefined as number | undefined,
score: undefined as number | undefined,
facilityTagList: [] as string[],
longitude: "" as string | number,
latitude: "" as string | number,
intro: "",
@@ -505,7 +588,81 @@ const defaultForm = () => ({
})
const form = reactive(defaultForm())
/** 省市区级联路径,逗号分隔 adcode(必须是完整链路,单写区县码级联无法回显) */
const districtPath = ref("")
const districtPathReady = ref(true)
const districtCascaderKey = ref(0)
const mapLocation = ref<any>(null)
/** 判断是否为「经度,纬度」形态,避免选点把坐标写进详细地址 */
const looksLikeCoordinates = (text: string) =>
/^-?\d+(\.\d+)?\s*,\s*-?\d+(\.\d+)?$/.test((text || "").trim())
const pickAreaRow = (res: any) => {
const d = res?.data
if (Array.isArray(d)) return d[0] || null
return d || null
}
/** 从区县 adcode 向上拼出 省,市,区 路径,供 china-area 回显 */
const buildDistrictPathByAdcode = async (adcode: number | string) => {
const codes: string[] = []
let cur = Number(adcode)
let guard = 0
while (Number.isFinite(cur) && cur > 0 && cur !== 100000 && guard++ < 10) {
codes.unshift(String(cur))
try {
const res: any = await getAreaObj({ adcode: cur })
const row = pickAreaRow(res)
const pid = Number(row?.pid)
if (!Number.isFinite(pid) || pid === 100000 || pid === 0 || pid === cur) break
cur = pid
} catch {
break
}
}
return codes.join(",")
}
/** 地图初始定位文案:区域 + 商圈 + 详细地址,便于落到嘉兴等正确城市 */
const mapPreferAddress = computed(() => {
const parts = [form.district, form.bizArea, form.address]
.map((s) => String(s || "").trim())
.filter((s) => s && !looksLikeCoordinates(s) && s !== "已选择位置")
return parts.join("")
})
const onDistrictChange = async (val: string) => {
districtPath.value = val || ""
const parts = String(val || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean)
const last = parts[parts.length - 1]
if (!last) {
form.district = ""
form.districtAdcode = undefined
return
}
form.districtAdcode = Number(last)
// 拼完整「省市区」文案,地图打开/搜索才能落到正确城市,而不是只拿「南湖区」
try {
const names: string[] = []
for (const code of parts) {
const res: any = await getAreaObj({ adcode: code })
const name = pickAreaRow(res)?.name
if (name) names.push(name)
}
form.district = names.join("") || form.district || ""
} catch {
try {
const res: any = await getAreaObj({ adcode: last })
form.district = pickAreaRow(res)?.name || form.district || String(last)
} catch {
form.district = form.district || String(last)
}
}
}
const rooms = ref<any[]>([])
const products = ref<any[]>([])
const dailyList = ref<any[]>([])
@@ -586,10 +743,6 @@ const syncMapFromForm = () => {
}
}
/** 判断是否为「经度,纬度」形态,避免选点把坐标写进详细地址 */
const looksLikeCoordinates = (text: string) =>
/^-?\d+(\.\d+)?\s*,\s*-?\d+(\.\d+)?$/.test((text || "").trim())
const onMapChange = (val: any) => {
if (!val) {
form.longitude = ""
@@ -644,6 +797,9 @@ const openDialog = async (id?: string) => {
visible.value = true
activeTab.value = "base"
Object.assign(form, defaultForm())
districtPath.value = ""
districtPathReady.value = true
districtCascaderKey.value++
mapLocation.value = null
rooms.value = []
products.value = []
@@ -654,6 +810,7 @@ const openDialog = async (id?: string) => {
nextTick(() => formRef.value?.clearValidate())
if (!id) return
loading.value = true
districtPathReady.value = false
try {
const res = await getObj(id)
const data = res.data || {}
@@ -665,6 +822,25 @@ const openDialog = async (id?: string) => {
form.coverImage = data.coverImage || ""
form.carouselImages = data.carouselImages || ""
form.address = data.address || ""
form.district = data.district || ""
form.districtAdcode = data.districtAdcode != null ? Number(data.districtAdcode) : undefined
// 库里只存区县 adcode,回显必须还原完整省市区路径,否则级联是空的
if (form.districtAdcode != null && Number.isFinite(form.districtAdcode)) {
districtPath.value = await buildDistrictPathByAdcode(form.districtAdcode)
if (!form.district && districtPath.value) {
await onDistrictChange(districtPath.value)
}
} else {
districtPath.value = ""
}
districtCascaderKey.value++
form.bizArea = data.bizArea || ""
form.starLevel = data.starLevel != null ? Number(data.starLevel) : undefined
form.score = data.score != null && data.score !== "" ? Number(data.score) : undefined
form.facilityTagList = String(data.facilityTags || "")
.split(/[,,]/)
.map((s: string) => s.trim())
.filter(Boolean)
form.longitude = data.longitude ?? ""
form.latitude = data.latitude ?? ""
form.intro = data.intro || ""
@@ -681,6 +857,7 @@ const openDialog = async (id?: string) => {
} catch (err: any) {
useMessage().error(err.msg || "加载失败")
} finally {
districtPathReady.value = true
loading.value = false
}
}
@@ -704,6 +881,12 @@ const submitBase = async () => {
coverImage: form.coverImage || null,
carouselImages: form.carouselImages || null,
address: form.address?.trim() || null,
district: form.district?.trim() || null,
districtAdcode: form.districtAdcode ?? null,
bizArea: form.bizArea?.trim() || null,
starLevel: form.starLevel ?? null,
score: form.score ?? null,
facilityTags: (form.facilityTagList || []).filter(Boolean).join(",") || null,
longitude: form.longitude === "" || form.longitude == null ? null : form.longitude,
latitude: form.latitude === "" || form.latitude == null ? null : form.latitude,
intro: form.intro || null,
+13
View File
@@ -121,6 +121,19 @@ const viteConfig = defineConfig((mode: ConfigEnv) => {
ws: true, // 是否启用 WebSocket
changeOrigin: true,
},
// 本地调试腾讯 WebService:浏览器走同源代理,转发时带已白名单的生产域名 Referer
// (localhost 写进腾讯白名单经常仍 110,生产部署仍用浏览器直连 JSONP)
"/qqmap": {
target: "https://apis.map.qq.com",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/qqmap/, ""),
configure: (proxy) => {
proxy.on("proxyReq", (proxyReq) => {
proxyReq.setHeader("Referer", "https://zianxin.com/")
proxyReq.setHeader("Origin", "https://zianxin.com")
})
},
},
},
},
build: {