| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076 |
- <template>
- <div class="p-2">
- <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
- <div v-show="showSearch" class="mb-[10px]">
- <el-card shadow="hover">
- <el-form ref="queryFormRef" :model="queryParams" :inline="true">
- <el-form-item label="门店名称" prop="name">
- <el-input v-model="queryParams.name" placeholder="请输入门店名称" clearable @keyup.enter="handleQuery" />
- </el-form-item>
- <el-form-item label="详细地址" prop="address">
- <el-input v-model="queryParams.address" placeholder="请输入详细地址" clearable @keyup.enter="handleQuery" />
- </el-form-item>
- <el-form-item>
- <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
- <el-button icon="Refresh" @click="resetQuery">重置</el-button>
- </el-form-item>
- </el-form>
- </el-card>
- </div>
- </transition>
- <el-card shadow="never">
- <template #header>
- <el-row :gutter="10" class="mb8">
- <el-col :span="1.5">
- <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['physical:store:add']">新增</el-button>
- </el-col>
- <el-col :span="1.5">
- <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['physical:store:edit']"
- >修改</el-button
- >
- </el-col>
- <el-col :span="1.5">
- <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['physical:store:remove']"
- >删除</el-button
- >
- </el-col>
- <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
- </el-row>
- </template>
- <el-table v-loading="loading" border :data="storeList" @selection-change="handleSelectionChange">
- <el-table-column type="selection" width="55" align="center" />
- <el-table-column label="门店 ID" align="center" prop="id" v-if="true" />
- <el-table-column label="主页图标" align="center" prop="iconUrl" width="120">
- <template #default="scope">
- <img v-if="scope.row.iconUrl" :src="scope.row.iconUrl" alt="" style="width: 40px; height: 40px; object-fit: cover; border-radius: 4px" />
- <span v-else>无</span>
- </template>
- </el-table-column>
- <el-table-column label="昵称" align="center" prop="name" />
- <el-table-column label="服务类型" align="center" prop="storeTypeName" />
- <el-table-column label="详细地址" align="center" prop="address" />
- <el-table-column label="是否授权" align="center" prop="isAuthorization">
- <template #default="scope">
- <el-tag :type="scope.row.isAuthorization ? 'success' : 'info'">
- {{ scope.row.isAuthorization ? '已授权' : '未授权' }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column label="兑换开始时间" align="center" prop="businessStartTime" width="180">
- <template #default="scope">
- <span>{{ parseTime(scope.row.businessStartTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
- </template>
- </el-table-column>
- <el-table-column label="兑换结束时间" align="center" prop="businessEndTime" width="180">
- <template #default="scope">
- <span>{{ parseTime(scope.row.businessEndTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
- </template>
- </el-table-column>
- <el-table-column label="状态" align="center" prop="status">
- <template #default="scope">
- <el-tag :type="scope.row.status === 1 ? 'success' : 'info'">
- {{ scope.row.status === 1 ? '开业中' : '闭店中' }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column label="创建时间" align="center" prop="createdAt" width="180"></el-table-column>
- <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
- <template #default="scope">
- <el-tooltip content="修改" placement="top">
- <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['physical:store:edit']"></el-button>
- </el-tooltip>
- <el-tooltip content="删除" placement="top">
- <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['physical:store:remove']"></el-button>
- </el-tooltip>
- <el-tooltip :content="scope.row.isAuthorization ? '关闭授权' : '点击授权'" placement="top">
- <el-button
- link
- :type="scope.row.isAuthorization ? 'warning' : 'success'"
- @click="handleAuthorization(scope.row)"
- style="text-decoration: underline"
- >
- {{ scope.row.isAuthorization ? '关闭授权' : '点击授权' }}
- </el-button>
- </el-tooltip>
- </template>
- </el-table-column>
- </el-table>
- <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
- </el-card>
- <!-- 添加或修改门店信息对话框 -->
- <el-dialog :title="dialog.title" v-model="dialog.visible" width="600px" append-to-body>
- <el-form ref="storeFormRef" :model="form" :rules="rules" label-width="120px">
- <el-form-item label="昵称" prop="name">
- <el-input v-model="form.name" placeholder="请输入昵称" />
- </el-form-item>
- <el-form-item label="详细地址" prop="address">
- <el-input v-model="form.address" type="textarea" placeholder="请输入内容" />
- <el-button type="primary" @click="openMapSelector" style="margin-top: 8px">选择地图位置</el-button>
- </el-form-item>
- <el-form-item label="联系方式" prop="phone">
- <el-input v-model="form.phone" type="textarea" placeholder="请输入内容" />
- </el-form-item>
- <el-form-item label="服务类型" prop="storeTypeId">
- <el-select v-model="form.storeTypeId" placeholder="请选择服务类型" style="width: 100%">
- <el-option v-for="item in tagOptions" :key="item.id" :label="item.serviceName" :value="item.id" />
- </el-select>
- </el-form-item>
- <el-form-item label="主页图标" prop="iconUrl">
- <div class="upload-container">
- <el-upload
- class="upload-icon"
- action="#"
- :on-change="handleIconChange"
- :on-remove="handleIconRemove"
- :file-list="fileList"
- :auto-upload="false"
- :limit="1"
- accept="image/*"
- >
- <template #trigger>
- <el-button type="primary">点击选择图标</el-button>
- </template>
- <template #default>
- <div class="preview-area" @click="handlePreviewClick">
- <img
- v-if="iconPreviewUrl || form.iconUrl"
- :src="iconPreviewUrl || form.iconUrl"
- alt="预览图"
- style="max-width: 100px; max-height: 100px; margin-top: 10px; cursor: pointer"
- />
- <div v-else style="margin-top: 10px; color: #999">无</div>
- </div>
- </template>
- <template #tip>
- <div class="el-upload__tip">
- <span v-if="fileList.length > 0">当前已选文件:{{ fileList[0].name }}</span>
- </div>
- </template>
- </el-upload>
- </div>
- </el-form-item>
- <el-form-item label="主标题" prop="mainTitle">
- <el-input v-model="form.mainTitle" type="textarea" placeholder="请输入内容" />
- </el-form-item>
- <el-form-item label="门店简介" prop="storeDescription">
- <el-input v-model="form.storeDescription" type="textarea" placeholder="请输入内容" />
- </el-form-item>
- <el-form-item label="门店appId" prop="previewDurationSeconds">
- <div>
- <el-input v-model="form.storeAppIds" placeholder="请输入内容" style="width: 100%" />
- <div style="margin-top: 4px; font-size: 12px; color: #999">
- <el-icon style="vertical-align: middle"><InfoFilled /></el-icon>
- 温馨提示:多个 appId 请用英文逗号分隔,appId为用户ID
- </div>
- </div>
- </el-form-item>
- <el-form-item label="门店宣传图" prop="storePromotionImageOsId">
- <imageUpload v-model="form.storePromotionImageOsId" :limit="5" />
- </el-form-item>
- <el-form-item label="开始兑换时间" prop="businessStartTime">
- <el-date-picker
- clearable
- v-model="form.businessStartTime"
- type="datetime"
- value-format="YYYY-MM-DD HH:mm:ss"
- placeholder="请选择开始兑换时间"
- >
- </el-date-picker>
- </el-form-item>
- <el-form-item label="结束兑换时间" prop="businessEndTime">
- <el-date-picker
- clearable
- v-model="form.businessEndTime"
- type="datetime"
- value-format="YYYY-MM-DD HH:mm:ss"
- placeholder="请选择结束兑换时间"
- >
- </el-date-picker>
- </el-form-item>
- <el-form-item label="状态" prop="status">
- <el-radio-group v-model="form.status">
- <el-radio :label="1">开业中</el-radio>
- <el-radio :label="2">闭店中</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer">
- <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
- <el-button @click="cancel">取 消</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 地图选择对话框 -->
- <el-dialog title="选择地理位置" v-model="mapDialogVisible" width="800px" append-to-body @close="handleMapClose">
- <div style="height: 500px; display: flex; flex-direction: column">
- <div style="margin-bottom: 10px; padding: 10px; background: #f5f7fa; border-radius: 4px; font-size: 12px">
- <el-icon style="vertical-align: middle; margin-right: 5px"><InfoFilled /></el-icon>
- 操作提示:拖动地图找到目标位置 → 点击地图标记位置 → 点击确认选择保存坐标
- </div>
- <div style="flex: 1">
- <div id="amapContainer" style="width: 100%; height: 100%"></div>
- </div>
- <div style="margin-top: 10px; display: flex; gap: 10px">
- <el-input v-model="selectedCoordinate" placeholder="选中的坐标" readonly style="flex: 1">
- <template #prepend>坐标:</template>
- </el-input>
- <el-button type="primary" @click="confirmMapSelection">确认选择</el-button>
- </div>
- </div>
- <template #footer>
- <div class="dialog-footer">
- <el-button @click="cancelMapSelection">取 消</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 图片预览弹窗 -->
- <el-dialog v-model="dialogVisible" title="图片预览" width="50%">
- <img :src="previewSrc || iconPreviewUrl || form.iconUrl" alt="预览图片" style="max-width: 100%; max-height: 80vh" />
- </el-dialog>
- </div>
- </template>
- <script setup name="Store" lang="ts">
- import { listStore, getStore, delStore, addStore, updateStore } from '@/api/system/physical/store';
- import { StoreVO, StoreQuery, StoreForm } from '@/api/system/physical/store/types';
- import { uploadTournament } from '@/api/system/business/tournaments';
- import { selectAllPhysicalTagsSelList } from '@/api/system/physical/tag';
- import { TagVO } from '@/api/system/physical/tag/types';
- import { ElMessage } from 'element-plus';
- import { InfoFilled } from '@element-plus/icons-vue';
- import { listByIds } from '@/api/system/oss';
- const { proxy } = getCurrentInstance() as ComponentInternalInstance;
- let mapInstance: any = null;
- let markerInstance: any = null;
- let mapApiLoaded = false;
- const storeList = ref<StoreVO[]>([]);
- const buttonLoading = ref(false);
- const loading = ref(true);
- const showSearch = ref(true);
- const ids = ref<Array<string | number>>([]);
- const single = ref(true);
- const multiple = ref(true);
- const total = ref(0);
- const queryFormRef = ref<ElFormInstance>();
- const storeFormRef = ref<ElFormInstance>();
- const dialog = reactive<DialogOption>({
- visible: false,
- title: ''
- });
- const initFormData: StoreForm = {
- id: undefined,
- storeCode: undefined,
- name: undefined,
- storeTypeId: undefined,
- zoneId: undefined,
- address: undefined,
- iconUrl: undefined,
- backgroundUrl: undefined,
- businessStartTime: undefined,
- businessEndTime: undefined,
- status: 1,
- phone: '',
- customTags: [],
- createdAt: undefined,
- updatedAt: undefined,
- deletedAt: undefined,
- latitude: undefined,
- longitude: undefined,
- storePromotionImageOsId: []
- };
- const data = reactive<PageData<StoreForm, StoreQuery>>({
- form: { ...initFormData },
- queryParams: {
- pageNum: 1,
- pageSize: 10,
- storeCode: undefined,
- name: undefined,
- storeTypeId: undefined,
- zoneId: undefined,
- address: undefined,
- iconUrl: undefined,
- backgroundUrl: undefined,
- businessStartTime: undefined,
- businessEndTime: undefined,
- status: undefined,
- createdAt: undefined,
- updatedAt: undefined,
- deletedAt: undefined,
- params: {}
- },
- rules: {
- id: [{ required: true, message: '主键 ID 不能为空', trigger: 'blur' }],
- name: [{ required: true, message: '门店名称,最多 20 字不能为空', trigger: 'blur' }],
- storeTypeId: [{ required: true, message: '门店类型 ID 不能为空', trigger: 'blur' }],
- address: [{ required: true, message: '详细地址不能为空', trigger: 'blur' }],
- status: [{ required: true, message: '状态不能为空', trigger: 'change' }],
- mainTitle: [{ required: true, message: '主标题不能为空', trigger: 'change' }],
- storeDescription: [{ required: true, message: '门店简介不能为空', trigger: 'change' }],
- phone: [
- { required: true, message: '联系方式不能为空', trigger: 'blur' },
- { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
- ],
- businessStartTime: [
- { required: true, message: '营业开始时间不能为空', trigger: 'blur' },
- {
- validator: (rule, value, callback) => {
- if (value && form.value.businessEndTime && value > form.value.businessEndTime) {
- callback(new Error('开始时间不能大于结束时间'));
- } else {
- callback();
- }
- },
- trigger: 'change'
- }
- ],
- businessEndTime: [
- { required: true, message: '营业结束时间不能为空', trigger: 'blur' },
- {
- validator: (rule, value, callback) => {
- if (value && form.value.businessStartTime && value < form.value.businessStartTime) {
- callback(new Error('结束时间不能小于开始时间'));
- } else {
- callback();
- }
- },
- trigger: 'change'
- }
- ],
- storePromotionImageOsId: [
- {
- validator: (rule, value, callback) => {
- if (!value || value.length === 0) {
- callback(new Error('请至少上传一张门店宣传图'));
- } else {
- callback();
- }
- },
- trigger: 'change'
- }
- ]
- }
- });
- const { queryParams, form, rules } = toRefs(data);
- const getList = async () => {
- loading.value = true;
- const res = await listStore(queryParams.value);
- storeList.value = res.rows;
- total.value = res.total;
- loading.value = false;
- };
- const cancel = () => {
- reset();
- dialog.visible = false;
- };
- const reset = () => {
- form.value = { ...initFormData };
- storeFormRef.value?.resetFields();
- customTags.value = [];
- fileList.value = [];
- fileList2.value = [];
- iconPreviewUrl.value = '';
- iconPreviewUrl2.value = '';
- form.value.iconUrl = '';
- form.value.backgroundUrl = '';
- };
- const handleQuery = () => {
- queryParams.value.pageNum = 1;
- getList();
- };
- const resetQuery = () => {
- queryFormRef.value?.resetFields();
- handleQuery();
- };
- const handleSelectionChange = (selection: StoreVO[]) => {
- ids.value = selection.map((item) => item.id);
- single.value = selection.length != 1;
- multiple.value = !selection.length;
- };
- const handleAdd = () => {
- reset();
- dialog.visible = true;
- dialog.title = '添加门店信息';
- fileList.value = [];
- fileList2.value = [];
- iconPreviewUrl.value = '';
- iconPreviewUrl2.value = '';
- form.value.iconUrl = '';
- form.value.backgroundUrl = '';
- customTags.value = [];
- };
- const handleUpdate = async (row?: StoreVO) => {
- reset();
- const _id = row?.id || ids.value[0];
- const res = await getStore(_id);
- Object.assign(form.value, res.data);
- if (form.value.iconUrl) {
- iconPreviewUrl.value = form.value.iconUrl;
- }
- if (form.value.backgroundUrl) {
- iconPreviewUrl2.value = form.value.backgroundUrl;
- }
- if (res.data.customTags && Array.isArray(res.data.customTags)) {
- customTags.value = [...res.data.customTags];
- } else {
- customTags.value = [];
- }
- const processImages = async (images: any[]) => {
- const imageUrls = await Promise.all(
- images.map(async (image) => {
- try {
- const res = await getIds(image.osId);
- const firstItem = Array.isArray(res) ? res[0] : res.data?.[0] || res[0];
- return {
- url: firstItem?.url || firstItem?.picUrl,
- ossId: image.osId,
- name: image.osId
- };
- } catch (error) {
- console.error('获取图片URL失败:', error);
- return { url: '', ossId: '' };
- }
- })
- );
- return imageUrls
- .filter((item) => item.url !== '')
- .map((item) => item.ossId)
- .join(',');
- };
- if (res.data.storePromotionImage && Array.isArray(res.data.storePromotionImage)) {
- form.value.storePromotionImageOsId = await processImages(res.data.storePromotionImage);
- } else {
- form.value.storePromotionImageOsId = [];
- }
- dialog.visible = true;
- dialog.title = '修改门店信息';
- };
- const getIds = async (ids: string | number) => {
- return await listByIds(ids);
- };
- const submitForm = () => {
- storeFormRef.value?.validate(async (valid: boolean) => {
- if (valid) {
- buttonLoading.value = true;
- const processOsIds = async (osIdString: any, imageType: string) => {
- if (!osIdString) return [];
- const osIds = osIdString
- .toString()
- .split(',')
- .map((id: string) => id.trim());
- const imageList = [];
- for (const osId of osIds) {
- const res = await listByIds(osId);
- const firstItem = Array.isArray(res) ? res[0] : res.data?.[0] || res[0];
- const picUrl = firstItem?.url || firstItem?.picUrl;
- imageList.push({ osId, imageUrl: picUrl, imageType });
- }
- return imageList;
- };
- form.value.storePromotionImage = await processOsIds(form.value.storePromotionImageOsId, 'STORE');
- if (form.value.id) {
- await updateStore(form.value).finally(() => (buttonLoading.value = false));
- } else {
- await addStore(form.value).finally(() => (buttonLoading.value = false));
- }
- proxy?.$modal.msgSuccess('操作成功');
- dialog.visible = false;
- await getList();
- }
- });
- };
- const handleDelete = async (row?: StoreVO) => {
- const _ids = row?.id || ids.value;
- await proxy?.$modal.confirm('是否确认删除门店信息编号为"' + _ids + '"的数据项?');
- await delStore(_ids);
- proxy?.$modal.msgSuccess('删除成功');
- await getList();
- };
- onMounted(() => {
- getList();
- loadTagOptions();
- });
- const iconPreviewUrl = ref('');
- const iconPreviewUrl2 = ref('');
- const fileList = ref([]);
- const fileList2 = ref([]);
- const dialogVisible = ref(false);
- const dialogVisible2 = ref(false);
- const previewSrc = ref('');
- const previewSrc2 = ref('');
- const handleIconChange = async (file) => {
- const index = fileList.value.findIndex((f) => f.uid === file.uid);
- fileList.value = [];
- if (file.raw) {
- iconPreviewUrl.value = URL.createObjectURL(file.raw);
- }
- if (index === -1) {
- fileList.value.push(file);
- }
- try {
- const rawFile = file.raw;
- const res = await uploadTournament(rawFile);
- if (res.code === 200) {
- fileList.value[index] = {
- ...file,
- status: 'success',
- response: res.data.url
- };
- form.value.iconUrl = fileList.value[index].response;
- iconPreviewUrl.value = fileList.value[index].response;
- ElMessage.success('上传成功');
- } else {
- throw new Error(res.msg);
- }
- } catch (error) {
- fileList.value[index] = {
- ...file,
- status: 'fail',
- error: '上传失败'
- };
- ElMessage.error('上传失败,请重试');
- }
- };
- const handleIconRemove = (file, updatedFileList) => {
- fileList.value = updatedFileList;
- iconPreviewUrl.value = '';
- form.value.iconUrl = '';
- };
- const handlePreviewClick = () => {
- const currentSrc = iconPreviewUrl.value || form.value.iconUrl;
- if (currentSrc) {
- dialogVisible.value = true;
- }
- };
- const handleIconChange2 = async (file) => {
- const index = fileList2.value.findIndex((f) => f.uid === file.uid);
- fileList2.value = [];
- if (file.raw) {
- iconPreviewUrl2.value = URL.createObjectURL(file.raw);
- }
- if (index === -1) {
- fileList2.value.push(file);
- }
- try {
- const rawFile = file.raw;
- const res = await uploadTournament(rawFile);
- if (res.code === 200) {
- fileList2.value[index] = {
- ...file,
- status: 'success',
- response: res.data.url
- };
- form.value.backgroundUrl = fileList2.value[index].response;
- iconPreviewUrl2.value = fileList2.value[index].response;
- ElMessage.success('上传成功');
- } else {
- throw new Error(res.msg);
- }
- } catch (error) {
- fileList2.value[index] = {
- ...file,
- status: 'fail',
- error: '上传失败'
- };
- ElMessage.error('上传失败,请重试');
- }
- };
- const handleIconRemove2 = (file, updatedFileList) => {
- fileList2.value = updatedFileList;
- iconPreviewUrl2.value = '';
- form.value.backgroundUrl = '';
- };
- const handlePreviewClick2 = () => {
- const currentSrc = iconPreviewUrl2.value || form.value.backgroundUrl;
- if (currentSrc) {
- dialogVisible2.value = true;
- }
- };
- const tagOptions = ref<TagVO[]>([]);
- const loadTagOptions = async () => {
- try {
- const res = await selectAllPhysicalTagsSelList();
- if (res.code === 200) {
- tagOptions.value = res.data;
- } else {
- ElMessage.error('加载门店类型失败:' + res.msg);
- }
- } catch (error) {
- console.error('请求出错:', error);
- ElMessage.error('请求失败,请检查网络');
- }
- };
- const customTags = ref<string[]>([]);
- const addTagDialogVisible = ref(false);
- const newTagName = ref('');
- const confirmAddTag = () => {
- if (!newTagName.value.trim()) {
- ElMessage.warning('标签名称不能为空');
- return;
- }
- if (customTags.value.length >= 3) {
- ElMessage.warning('最多只能添加 3 个标签');
- return;
- }
- if (customTags.value.includes(newTagName.value)) {
- ElMessage.warning('该标签已存在');
- return;
- }
- customTags.value.push(newTagName.value);
- newTagName.value = '';
- addTagDialogVisible.value = false;
- };
- const handleTagClose = (index: number) => {
- customTags.value.splice(index, 1);
- };
- watch(
- customTags,
- () => {
- if (Array.isArray(customTags.value)) {
- form.value.customTags = customTags.value;
- }
- },
- { deep: true }
- );
- const openAddTagDialog = () => {
- newTagName.value = '';
- addTagDialogVisible.value = true;
- };
- // 地图相关变量
- const mapDialogVisible = ref(false);
- const selectedCoordinate = ref('');
- // 动态加载高德地图API
- const loadAmapApi = (): Promise<void> => {
- return new Promise((resolve, reject) => {
- if (mapApiLoaded || (window as any).AMap) {
- mapApiLoaded = true;
- resolve();
- return;
- }
- const script = document.createElement('script');
- script.type = 'text/javascript';
- script.src = 'https://webapi.amap.com/maps?v=1.4.15&key=7ffb6b303bfa384026e2124df21248c0a';
- script.onload = () => {
- mapApiLoaded = true;
- resolve();
- };
- script.onerror = () => {
- reject(new Error('高德地图API加载失败'));
- };
- document.head.appendChild(script);
- });
- };
- // 初始化高德地图
- const initMap = async () => {
- try {
- await loadAmapApi();
- const AMap = (window as any).AMap;
- if (mapInstance) {
- mapInstance.destroy();
- }
- // 优先使用表单中已有的坐标
- let initCenter: number[] = [];
- let markerPos: number[] = [];
- let hasCoordinates = false;
- if (form.value.longitude !== undefined && form.value.latitude !== undefined) {
- const lng = Number(form.value.longitude);
- const lat = Number(form.value.latitude);
- if (!isNaN(lng) && !isNaN(lat)) {
- initCenter = [lng, lat];
- markerPos = [lng, lat];
- hasCoordinates = true;
- }
- }
- // 如果没有已有坐标,尝试获取浏览器定位
- if (!hasCoordinates) {
- try {
- const position = await getCurrentBrowserLocation();
- // 注意:浏览器原生定位返回的是WGS-84,需要转换为GCJ-02
- const gcj02Pos = convertWgs84ToGcj02(position.latitude, position.longitude);
- initCenter = [gcj02Pos.lng, gcj02Pos.lat];
- markerPos = [gcj02Pos.lng, gcj02Pos.lat];
- hasCoordinates = true;
- proxy?.$modal.msgSuccess('已自动定位到您当前的位置');
- } catch (error) {
- console.log('浏览器定位失败,使用默认中心点', error);
- // 使用中国中心点作为备选(GCJ-02坐标系)
- initCenter = [104.1954, 35.8617];
- markerPos = [104.1954, 35.8617];
- }
- }
- mapInstance = new AMap.Map('amapContainer', {
- center: initCenter,
- zoom: hasCoordinates ? 16 : 5,
- resizeEnable: true,
- dragEnable: true,
- zoomEnable: true
- });
- markerInstance = new AMap.Marker({
- position: markerPos,
- title: '门店位置',
- icon: new AMap.Icon({
- size: new AMap.Size(25, 34),
- image: '//webapi.amap.com/theme/v1.3/markers/n/mark_b.png',
- imageSize: new AMap.Size(25, 34)
- }),
- offset: new AMap.Pixel(-12, -34)
- });
- mapInstance.add(markerInstance);
- // 点击地图事件 - 更新坐标并获取地址
- mapInstance.on('click', async (e: any) => {
- const lng = e.lnglat.getLng();
- const lat = e.lnglat.getLat();
- markerInstance.setPosition([lng, lat]);
- selectedCoordinate.value = `纬度: ${lat.toFixed(6)}, 经度: ${lng.toFixed(6)} (GCJ-02)`;
- form.value.latitude = lat;
- form.value.longitude = lng;
- // 使用 OpenCageData API 获取中文地址(需要将 GCJ-02 转换为 WGS-84)
- await setSelectedLocationWithConversion(lat, lng);
- });
- if (hasCoordinates) {
- const lat = Number(form.value.latitude || markerPos[1]);
- const lng = Number(form.value.longitude || markerPos[0]);
- selectedCoordinate.value = `纬度: ${lat.toFixed(6)}, 经度: ${lng.toFixed(6)} (GCJ-02)`;
- // 如果编辑模式且已有地址,不需要重新获取
- if (!form.value.address) {
- // 如果是新增模式或地址为空,尝试获取地址
- await setSelectedLocationWithConversion(lat, lng);
- }
- }
- } catch (error) {
- console.error('地图初始化失败:', error);
- proxy?.$modal.msgError('地图加载失败');
- }
- };
- /**
- * 使用 OpenCageData API 获取地址(GCJ-02 → WGS-84 转换)
- */
- const setSelectedLocationWithConversion = async (gcjLat: number, gcjLon: number) => {
- const coordsText = `纬度:${gcjLat.toFixed(6)}, 经度:${gcjLon.toFixed(6)}`;
- try {
- // 关键步骤:将 GCJ-02 转换为 WGS-84
- const wgs84Coords = convertGcj02ToWgs84(gcjLat, gcjLon);
- console.log('坐标转换:', {
- '原始GCJ-02': `${gcjLat}, ${gcjLon}`,
- '转换WGS-84': `${wgs84Coords.lat}, ${wgs84Coords.lng}`
- });
- // 使用 WGS-84 坐标调用 OpenCageData API
- const response = await fetch(
- `https://api.opencagedata.com/geocode/v1/json?q=${wgs84Coords.lat}+${wgs84Coords.lng}&key=82827937f9614a6eb17c772efa63d75d&language=zh&no_annotations=1`
- );
- if (response.ok) {
- const data = await response.json();
- if (data && data.results && data.results.length > 0) {
- const address = data.results[0].formatted;
- // 清理地址格式:移除开头的邮编和逗号
- const cleanedAddress = cleanChineseAddress(address);
- // 将地址填充到表单中(注意:数据库中仍然存储 GCJ-02 坐标)
- form.value.address = cleanedAddress;
- console.log('详细地址:', cleanedAddress);
- proxy?.$modal.msgSuccess(`已获取地址:${cleanedAddress}`);
- } else {
- form.value.address = coordsText;
- proxy?.$modal.msgWarning('未能获取详细地址,请手动输入');
- }
- } else {
- form.value.address = coordsText;
- proxy?.$modal.msgWarning('地址解析服务异常,请手动输入');
- }
- } catch (error) {
- console.error('地址解析失败:', error);
- form.value.address = coordsText;
- proxy?.$modal.msgWarning('地址解析失败,请手动输入');
- }
- };
- /**
- * GCJ-02 → WGS-84(前端简易版)
- */
- const convertGcj02ToWgs84 = (gcjLat: number, gcjLon: number): { lat: number; lng: number } => {
- const PI = 3.1415926535897932384626;
- const A = 6378245.0;
- const EE = 0.00669342162296594323;
- const outOfChina = (lat: number, lon: number): boolean => {
- return lon < 72.004 || lon > 137.8347 || lat < 0.8293 || lat > 55.8271;
- };
- const transformLat = (x: number, y: number): number => {
- let ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
- ret += ((20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0) / 3.0;
- ret += ((20.0 * Math.sin(y * PI) + 40.0 * Math.sin((y / 3.0) * PI)) * 2.0) / 3.0;
- ret += ((160.0 * Math.sin((y / 12.0) * PI) + 320.0 * Math.sin((y * PI) / 30.0)) * 2.0) / 3.0;
- return ret;
- };
- const transformLon = (x: number, y: number): number => {
- let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
- ret += ((20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0) / 3.0;
- ret += ((20.0 * Math.sin(x * PI) + 40.0 * Math.sin((x / 3.0) * PI)) * 2.0) / 3.0;
- ret += ((150.0 * Math.sin((x / 12.0) * PI) + 300.0 * Math.sin((x / 30.0) * PI)) * 2.0) / 3.0;
- return ret;
- };
- if (outOfChina(gcjLat, gcjLon)) {
- return { lat: gcjLat, lng: gcjLon };
- }
- let dLat = transformLat(gcjLon - 105.0, gcjLat - 35.0);
- let dLon = transformLon(gcjLon - 105.0, gcjLat - 35.0);
- const radLat = (gcjLat / 180.0) * PI;
- let magic = Math.sin(radLat);
- magic = 1 - EE * magic * magic;
- const sqrtMagic = Math.sqrt(magic);
- dLat = (dLat * 180.0) / (((A * (1 - EE)) / (magic * sqrtMagic)) * PI);
- dLon = (dLon * 180.0) / ((A / sqrtMagic) * Math.cos(radLat) * PI);
- return {
- lat: gcjLat - dLat, // 注意:这里是减法,与 WGS→GCJ 相反
- lng: gcjLon - dLon
- };
- };
- /**
- * 清理中文地址格式
- */
- const cleanChineseAddress = (address: string): string => {
- if (!address) return address;
- // 移除开头的邮编(如 "410000 湖南省..." → "湖南省...")
- let cleaned = address.replace(/^\d{5,}\s*/, '');
- // 处理重复的省市名称(如:湖南省长沙市长沙市 → 湖南省长沙市)
- cleaned = cleaned.replace(/([\u4e00-\u9fa5]+市)\1/g, '$1');
- // 移除多余的逗号、空格
- cleaned = cleaned.replace(/[,,\s]+/g, '').replace(/\s+/g, '');
- // 移除 "中国" 前缀(OpenCageData 通常会包含)
- cleaned = cleaned.replace(/^中国/, '');
- return cleaned;
- };
- // ... existing code ...
- // WGS-84转GCJ-02(前端简易版)
- const convertWgs84ToGcj02 = (wgsLat: number, wgsLon: number): { lat: number; lng: number } => {
- const PI = 3.1415926535897932384626;
- const A = 6378245.0;
- const EE = 0.00669342162296594323;
- const outOfChina = (lat: number, lon: number): boolean => {
- return lon < 72.004 || lon > 137.8347 || lat < 0.8293 || lat > 55.8271;
- };
- const transformLat = (x: number, y: number): number => {
- let ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
- ret += ((20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0) / 3.0;
- ret += ((20.0 * Math.sin(y * PI) + 40.0 * Math.sin((y / 3.0) * PI)) * 2.0) / 3.0;
- ret += ((160.0 * Math.sin((y / 12.0) * PI) + 320.0 * Math.sin((y * PI) / 30.0)) * 2.0) / 3.0;
- return ret;
- };
- const transformLon = (x: number, y: number): number => {
- let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
- ret += ((20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0) / 3.0;
- ret += ((20.0 * Math.sin(x * PI) + 40.0 * Math.sin((x / 3.0) * PI)) * 2.0) / 3.0;
- ret += ((150.0 * Math.sin((x / 12.0) * PI) + 300.0 * Math.sin((x / 30.0) * PI)) * 2.0) / 3.0;
- return ret;
- };
- if (outOfChina(wgsLat, wgsLon)) {
- return { lat: wgsLat, lng: wgsLon };
- }
- let dLat = transformLat(wgsLon - 105.0, wgsLat - 35.0);
- let dLon = transformLon(wgsLon - 105.0, wgsLat - 35.0);
- const radLat = (wgsLat / 180.0) * PI;
- let magic = Math.sin(radLat);
- magic = 1 - EE * magic * magic;
- const sqrtMagic = Math.sqrt(magic);
- dLat = (dLat * 180.0) / (((A * (1 - EE)) / (magic * sqrtMagic)) * PI);
- dLon = (dLon * 180.0) / ((A / sqrtMagic) * Math.cos(radLat) * PI);
- return {
- lat: wgsLat + dLat,
- lng: wgsLon + dLon
- };
- };
- // 获取浏览器定位(Promise封装)
- const getCurrentBrowserLocation = (): Promise<{ latitude: number; longitude: number }> => {
- return new Promise((resolve, reject) => {
- if (!navigator.geolocation) {
- reject(new Error('浏览器不支持地理位置'));
- return;
- }
- navigator.geolocation.getCurrentPosition(
- (position) => {
- resolve({
- latitude: position.coords.latitude,
- longitude: position.coords.longitude
- });
- },
- (error) => {
- reject(error);
- },
- {
- enableHighAccuracy: true,
- timeout: 10000,
- maximumAge: 0
- }
- );
- });
- };
- // 打开地图选择器
- const openMapSelector = () => {
- mapDialogVisible.value = true;
- nextTick(() => {
- setTimeout(() => {
- initMap();
- }, 200);
- });
- };
- // 确认地图选择
- const confirmMapSelection = () => {
- if (form.value.latitude === undefined || form.value.longitude === undefined) {
- proxy?.$modal.msgWarning('请先在地图上点击选择位置');
- return;
- }
- proxy?.$modal.msgSuccess('位置选择成功');
- mapDialogVisible.value = false;
- };
- // 取消地图选择
- const cancelMapSelection = () => {
- mapDialogVisible.value = false;
- };
- // 关闭地图对话框
- const handleMapClose = () => {
- if (mapInstance) {
- mapInstance.destroy();
- mapInstance = null;
- markerInstance = null;
- }
- };
- const handleAuthorization = async (row: StoreVO) => {
- const action = row.isAuthorization ? '关闭' : '授权';
- await proxy?.$modal.confirm(`确认要${action}门店"${row.name}"吗?`);
- const updateData: StoreForm = {
- ...row,
- isAuthorization: !row.isAuthorization,
- storePromotionImage: row.storePromotionImage || []
- };
- await updateStore(updateData);
- proxy?.$modal.msgSuccess('授权成功');
- await getList();
- };
- </script>
- <style scoped>
- .upload-container {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- }
- .upload-icon {
- margin-bottom: 10px;
- }
- .preview-area {
- display: flex;
- flex-direction: column;
- align-items: center;
- }
- .tag-container {
- flex-wrap: wrap;
- }
- </style>
|