You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

873 lines
23 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="container">
<!-- 顶部图片和标题 -->
<view class="header-section">
<image
class="header-image"
src="/static/imgs/indexImg.png"
mode="aspectFill"
></image>
<view class="header-overlay">
<view class="header-title">生成电梯图纸</view>
<view class="header-subtitle">
选择模板并设置参数快速生成专业电梯施工图
</view>
</view>
</view>
<!-- 选择图纸模板 -->
<view class="section">
<view class="section-title">选择图纸模板</view>
<uv-skeletons
v-if="loading"
:loading="loading"
:skeleton="[
{
type: 'flex',
num: 2,
gap: '20rpx',
children: [
{
type: 'custom',
num: 1,
gap: '20rpx',
style: 'width: 300rpx;height:200rpx;marginLeft: 10rpx;',
},
{
type: 'custom',
num: 1,
gap: '20rpx',
style: 'width: 300rpx;height:200rpx;marginLeft: 10rpx;',
},
],
},
]"
></uv-skeletons>
<view class="template-grid" v-else>
<view
class="template-item"
:class="{ active: currentTemplate.id === item.id }"
@click="selectTemplate(item)"
v-for="item in templateList"
:key="item.id"
>
<view class="tag" v-if="item.price_cents">
¥ {{ item.price_cents / 100 }}
</view>
<view class="template-name">{{ item.name }}</view>
<view class="template-desc">{{ item.description }}</view>
</view>
</view>
</view>
<!-- 设置参数 -->
<view class="section" v-if="has_access">
<view class="section-title">设置参数</view>
<uv-skeletons
v-if="params_loading"
:loading="params_loading"
:skeleton="[
{
type: 'line',
num: 3,
gap: '20rpx',
},
]"
></uv-skeletons>
<view class="form" v-else>
<!-- 按分类展示参数 -->
<view
v-for="(params, category) in groupedParams"
:key="category"
class="param-category"
>
<view class="category-title">{{ category }}</view>
<uv-form
labelAlign="left"
labelWidth="auto"
labelPosition="left"
:model="formField"
:rules="rules"
ref="form"
>
<uv-form-item
:prop="item.name"
:label="item.name + ''"
v-for="item in params"
:key="item.name"
>
<div class="required" v-if="item.required" style="color: #f12323">
*
</div>
<uv-input
@click="openPicker(item)"
@input="handleInputChange(item)"
@blur="validateNumberRange(item)"
:readonly="item.type === 'select' || item.name === '土建图号'"
:type="item.type === 'number' ? 'digit' : 'text'"
v-model="item.value"
color="#000"
border="bottom"
:placeholderStyle="{ color: '#666778' }"
:placeholder="item.type === 'number' ? `范围:${item.min} - ${item.max === 0 ? '不限' : item.max}` : (item.name === '土建图号'
? '自动生成'
: '请输入')"
></uv-input>
<!-- 数字范围提示 -->
<!-- <view
v-if="
item.type === 'number' &&
(item.min !== undefined || item.max !== undefined)
"
class="number-range-hint"
>
<text v-if="item.min !== undefined && item.max !== undefined">
范围:{{ item.min }} - {{ item.max }}
</text>
<text v-else-if="item.min !== undefined">
最小值:{{ item.min }}
</text>
<text v-else-if="item.max !== undefined">
最大值:{{ item.max }}
</text>
</view> -->
</uv-form-item>
</uv-form>
</view>
</view>
</view>
<view class="section" v-else>
<div class="status">
申请状态:{{ get_access_status(currentTemplate.access_status) }}
</div>
<uv-button
size="small"
v-if="currentTemplate.access_status !== 'pending'"
@click="openApplyModal"
text="申请权限"
color="#3F70FF"
></uv-button>
</view>
<!-- 底部按钮 -->
<view class="bottom-actions">
<uv-button
:loading="generating"
@click="modalRefOpen"
text="生成图纸"
color="#3F70FF"
></uv-button>
<uv-button @click="bugFeedback" text="bug反馈"></uv-button>
<uv-button @click="viewHistory" text="查看记录" color="#df9d5d"></uv-button>
</view>
<uv-modal
ref="modalRef"
showCancelButton
content="确定生成图纸?"
@confirm="confirmGenerate"
></uv-modal>
<uv-modal
ref="applyModalRef"
showCancelButton
title="申请权限"
@confirm="confirmApply"
@cancel="cancelApply"
>
<view class="apply-modal-content">
<view class="apply-label">申请理由:</view>
<uv-textarea
v-model="applyReason"
placeholder="请输入申请理由"
maxlength="200"
count
></uv-textarea>
</view>
</uv-modal>
<!-- 悬浮窗 -->
<view class="floating-btn" @click="openComplaintModal">
<view class="floating-text">投诉与</view>
<view class="floating-text">纠纷</view>
</view>
<!-- 客户投诉与纠纷处理机制弹窗 -->
<uv-modal
ref="complaintModalRef"
title="客户投诉与纠纷处理机制说明"
confirmText="我知道了"
@confirm="closeComplaintModal"
>
<scroll-view scroll-y class="complaint-content">
<view class="complaint-text">
<view class="p">为规范平台交易秩序,保障用户合法权益,结合我司图纸生成、电子图纸、设计方案等虚拟数字类产品的业务特性,特建立完善的客户投诉及纠纷处理机制,具体如下:</view>
<view class="h3">一、业务及退款规则说明</view>
<view class="p">我司提供的图纸生成服务为虚拟数字类产品,具有可复制、易传播、交付即生效的属性,产品一经生成并交付用户,无法进行回收、撤回或二次销售。</view>
<view class="p">因此在产品正常交付、内容符合约定、无质量问题的情况下,不支持无理由退款。相关规则已在商品详情页、订单页、支付确认环节进行显著提示,用户完成支付即视为已充分知晓并同意。</view>
<view class="h3">二、投诉受理渠道</view>
<view class="p">用户可通过以下渠道提交投诉与售后申请,确保问题及时反馈:</view>
<view class="p">客服电话:<text class="phone-link" @click="callService">13666500395</text></view>
<view class="h3">三、投诉响应时效</view>
<view class="p">收到用户投诉后工作日2小时内响应非工作日当天内响应<br/>一般投诉问题24小时内处理完毕并反馈结果<br/>复杂争议类问题48小时内给出解决方案。</view>
<view class="h3">四、客诉分类处理方案</view>
<view class="p">【支付异常类】针对重复扣款、支付成功未到账、订单异常等情况,经核实后立即原路全额退款或补发对应服务,确保用户资金不受损失。</view>
<view class="p">【产品质量类】若图纸生成错误、内容缺失、未达到服务标准,优先为用户重新生成、修改完善或补发,保障用户正常使用。</view>
<view class="p">【服务争议类】对已正常交付、无质量问题的退款申请,将依据订单记录、交付凭证、用户协议耐心解释沟通;如确属特殊情况,将在核实后按公司流程审慎处理。</view>
<view class="p">【恶意投诉类】对已使用产品后恶意申请退款的行为,将留存完整凭证,按支付平台规则合理举证,维护正常经营秩序。</view>
<view class="h3">五、退款执行规范</view>
<view class="p">确需退款的场景,一律按原支付渠道原路退回,退款时限遵循支付平台规定,不设置不合理门槛,不拖延、不拒付。</view>
<view class="h3">六、长效管理机制</view>
<view class="p">我司将持续完善售前提示、服务说明及售后流程,主动降低投诉发生率,积极配合支付平台监督管理,妥善处理各类交易纠纷,保障交易安全与用户体验。</view>
</view>
</scroll-view>
</uv-modal>
<uv-picker ref="picker" :columns="columns" @confirm="confirm"></uv-picker>
</view>
</template>
<script setup>
import { ref, onMounted, nextTick, computed } from "vue";
import {
onShow,
onLoad,
onShareAppMessage,
onShareTimeline,
onReachBottom,
onPullDownRefresh,
} from "@dcloudio/uni-app";
import { useAuthStore, useUserStore } from "@/store";
import RPC from "@/utils/request";
import { usePermission } from "@/hooks/usePermission";
import dayjs from 'dayjs'
const authStore = useAuthStore();
const userStore = useUserStore();
const loading = ref(false);
const params_loading = ref(false);
const has_access = ref(false);
const applyModalRef = ref(null);
const applyReason = ref("");
const applyLoading = ref(false);
const complaintModalRef = ref(null);
const openComplaintModal = () => {
complaintModalRef.value.open();
};
const closeComplaintModal = () => {
complaintModalRef.value.close();
};
const callService = () => {
uni.makePhoneCall({
phoneNumber: '13666500395'
});
};
// 根据category分组参数
const groupedParams = computed(() => {
if (!currentTemplate.value.params) return {};
const enabledParams = currentTemplate.value.params.filter(
(item) => item.enabled
);
const grouped = {};
enabledParams.forEach((param) => {
const category = param.category || "默认";
if (!grouped[category]) {
grouped[category] = [];
}
grouped[category].push(param);
});
return grouped;
});
const get_access_status = (access_status) => {
let str = "";
if (access_status === "none") {
str = "无权限";
} else if (access_status === "pending") {
str = "等待审批...";
} else if (access_status === "rejected") {
str = "已拒绝";
} else if (access_status === "approved") {
str = "已同意";
} else {
str = access_status;
}
return str;
};
const bugFeedback = () => {
uni.navigateTo({
url: "/pagesA/bug-feedback/bug-feedback",
});
};
const modalRefOpen = () => {
if (!has_access.value) {
uni.showToast({
title: "模板无权限使用",
icon: "error",
});
return;
}
let flag = false;
let name;
currentTemplate.value.params
?.filter((item) => item.enabled)
.forEach((item) => {
if (item.required && !item.value) {
flag = true;
name = item.name;
}
});
if (flag) {
uni.showToast({
title: `请检查【${name}】`,
icon: "none",
});
return;
}
modalRef.value.open();
};
const currentParams = ref(null);
const openPicker = (item) => {
console.log(item);
if (item.type !== "select") return;
currentParams.value = item;
columns.value = [item.options.split(",")];
picker.value.open();
};
const picker = ref(null);
const columns = ref([[]]);
const confirm = (val) => {
console.log(val);
currentParams.value.value = val.value[0];
// 如果修改的是土建图型号,需要重新生成土建图号
// if (currentParams.value.name === "土建图型号") {
// nextTick(() => {
// generateDrawingNumber();
// });
// }
};
// 打开申请权限弹框
const openApplyModal = () => {
applyReason.value = "";
applyModalRef.value.open();
};
// 确认申请权限
const confirmApply = async () => {
if (!applyReason.value.trim()) {
uni.showToast({
title: "请输入申请理由",
icon: "error",
});
return;
}
applyLoading.value = true;
try {
await RPC.post("/template_access_apply", {
template_id: currentTemplate.value.id,
reason: applyReason.value.trim(),
});
uni.showToast({
title: "申请已提交",
icon: "success",
});
applyModalRef.value.close();
applyReason.value = "";
getList();
} catch (error) {
uni.showToast({
title: "申请失败,请重试",
icon: "error",
});
} finally {
applyLoading.value = false;
}
};
// 取消申请
const cancelApply = () => {
applyReason.value = "";
};
// 处理输入变化
const handleInputChange = (item) => {
// 如果修改的是土建图型号,需要重新生成土建图号
// if (item.name === "土建图型号") {
// nextTick(() => {
// generateDrawingNumber();
// });
// }
};
// 验证数字范围
const validateNumberRange = (item) => {
if (item.type !== "number" || !item.value) return;
const value = parseFloat(item.value);
// 检查是否为有效数字
if (isNaN(value)) {
uni.showToast({
title: `${item.name}必须为数字`,
icon: "error",
});
item.value = "";
return;
}
// 检查最小值
if (item.min !== undefined && value < item.min) {
uni.showToast({
title: `${item.name}不能小于${item.min}`,
icon: "error",
});
item.value = item.min.toString();
return;
}
// 检查最大值
if (item.max !== undefined && item.max !== 0 && value > item.max) {
uni.showToast({
title: `${item.name}不能大于${item.max}`,
icon: "error",
});
item.value = item.max.toString();
return;
}
};
// 生成土建图号
const generateDrawingNumber = () => {
const params = currentTemplate.value.params;
if (!params) return;
// 找到土建图型号和土建图号字段
// const modelParam = params.find((p) => p.name === "土建图型号");
const numberParam = params.find((p) => p.name === "土建图号");
if (!numberParam) return;
// 获取当前年份和月份
// const now = new Date();
// const year = now.getFullYear().toString().slice(-2); // 取年份后两位
// const month = (now.getMonth() + 1).toString().padStart(2, "0"); // 月份补零
// 生成4位随机数字+字母
const randomPart = generateRandomCode();
// 组合生成土建图号GSE400-V25-10-0002A
numberParam.value = dayjs().format('YYYYMMDDHHmmss') + randomPart;
};
// 生成随机编码4位数字+1位字母
const generateRandomCode = () => {
const numbers = Math.floor(Math.random() * 10000)
.toString()
.padStart(4, "0");
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const letter = letters.charAt(Math.floor(Math.random() * letters.length));
return numbers;
};
const generating = ref(false);
const modalRef = ref(null);
const confirmGenerate = () => {
const params = {};
let flag = false;
let name = "";
currentTemplate.value.params
?.filter((item) => item.enabled)
.forEach((item) => {
if (item.required && !item.value) {
flag = true;
name = item.name;
}
params[item.name] = item.value;
});
if (flag) {
uni.showToast({
title: `请检查【${name}】`,
icon: "none",
});
return;
}
RPC.post("/template_create_task", {
template_id: currentTemplate.value.id,
template_data: params,
}).then(() => {
uni.showToast({
title: "图纸开始生成",
icon: "success",
});
setTimeout(() => {
uni.navigateTo({
url: "/pagesA/generate-record/generate-record",
});
}, 1000);
modalRef.value.close();
});
};
const templateList = ref([]);
const currentTemplate = ref({});
const getList = () => {
loading.value = true;
RPC.post("/template/list_with_access", {
page: 1,
limit: -1,
})
.then((res) => {
templateList.value = (res.rows || []).filter((item) => item.enabled && item.has_access);
currentTemplate.value = templateList.value[0]
? JSON.parse(JSON.stringify(templateList.value[0]))
: {};
if (currentTemplate.value.has_access) {
params_loading.value = true;
has_access.value = true;
RPC.post("/template/detail", { id: currentTemplate.value.id })
.then((res) => {
currentTemplate.value.params = res?.params;
generateDrawingNumber()
})
.finally(() => {
params_loading.value = false;
});
}
})
.finally(() => {
loading.value = false;
});
};
// 选择模板
const selectTemplate = (item) => {
currentTemplate.value = JSON.parse(JSON.stringify(item));
has_access.value = currentTemplate.value.has_access;
if (currentTemplate.value.has_access) {
params_loading.value = true;
RPC.post("/template/detail", { id: currentTemplate.value.id })
.then((res) => {
currentTemplate.value.params = res?.params;
// 如果有土建图型号,自动生成土建图号
nextTick(() => {
generateDrawingNumber();
});
})
.finally(() => {
params_loading.value = false;
});
}
};
const form = ref(null);
const formField = ref({
phone: "",
password: "",
});
const rules = ref({
// 'phone': {
// required: true,
// message: '请填写手机号',
// trigger: ['blur', 'change']
// },
// 'password': {
// required: true,
// message: '请填写验证码',
// trigger: ['blur', 'change']
// }
});
// 查看历史记录
const viewHistory = () => {
uni.navigateTo({
url: "/pagesA/generate-record/generate-record",
});
};
// 分享给朋友
onShareAppMessage((data) => {
console.log("🚀 ~ onShareAppMessage ~ data:", data);
return {
title: "电梯图纸生成平台",
path: "/pages/index/index",
};
});
// 分享到朋友圈
onShareTimeline((data) => {
console.log("🚀 ~ onShareTimeline ~ data:", data);
return {
title: "电梯图纸生成平台",
query: "a=1&b=2",
};
});
onLoad(() => {
console.log("index-onLoad");
});
onShow(async () => {
getList();
console.log("test page onShow");
const hasPermission = await usePermission();
console.log(hasPermission ? "已登录" : "跳转登录");
// 以下开始写业务逻辑...
if (!hasPermission) {
authStore.signOut();
uni.$uv.route("/pages/login/login");
}
});
onPullDownRefresh(() => {
getList();
uni.stopPullDownRefresh();
});
</script>
<style lang="scss" scoped>
.container {
font-size: 14px;
min-height: 100vh;
background-color: #f8f8f8;
padding-bottom: 52px;
/* #ifdef H5 */
padding-bottom: 102px;
/* H5环境下需要更多空间 (50px按钮 + 50px tabBar + 2px间距) */
/* #endif */
}
.header-section {
position: relative;
height: 200px;
overflow: hidden;
.header-image {
width: 100%;
height: 100%;
}
.header-overlay {
position: absolute;
top: 50%;
right: 0;
background: rgba(0, 0, 0, 0.5);
color: white;
width: 70%;
transform: translate(0, -50%);
padding: 20px 10px;
.header-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 8px;
color: #bd9e85;
}
.header-subtitle {
font-size: 12px;
}
}
}
.section {
margin: 16px;
background: white;
border-radius: 12px;
padding: 16px;
.section-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 16px;
}
.status {
margin-bottom: 20px;
}
}
.template-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
max-height: 200px;
overflow-y: auto;
.template-item {
border: 1px solid #e5e5e5;
border-radius: 8px;
padding: 16px 12px;
text-align: center;
transition: all 0.3s;
position: relative;
.tag {
position: absolute;
top: 0px;
left: 0;
padding: 2px 8px;
border-radius: 0 0 10px 0;
font-size: 14px;
color: white;
background: #00caef;
}
&.active {
border-color: #4285f4;
background-color: #f0f7ff;
}
.template-name {
font-size: 14px;
font-weight: 500;
color: #333;
margin: 8px 0 4px;
}
.template-desc {
font-size: 12px;
color: #999;
}
}
}
.bottom-actions {
z-index: 999;
position: fixed;
bottom: 0;
left: 0;
background: #fff;
height: 50px;
border-top: 1px solid #e5e5e5;
display: flex;
align-items: center;
gap: 12px;
justify-content: space-between;
padding: 0 16px;
box-sizing: border-box;
width: 100%;
/* #ifdef H5 */
bottom: 50px;
/* H5环境下给tabBar留出空间 */
/* #endif */
}
.apply-modal-content {
padding: 20px 0;
width: 100%;
.apply-label {
font-size: 14px;
color: #333;
margin-bottom: 12px;
}
}
.number-range-hint {
font-size: 12px;
color: #999;
margin-top: 4px;
padding-left: 0;
}
.param-category {
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
.category-title {
font-size: 15px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
position: relative;
&::before {
content: "";
position: absolute;
left: 0;
bottom: -1px;
width: 30px;
height: 2px;
background: #3f70ff;
}
}
}
.floating-btn {
position: fixed;
right: 16px;
bottom: 120px;
/* #ifdef H5 */
bottom: 170px;
/* #endif */
width: 56px;
height: 56px;
background: linear-gradient(135deg, #3f70ff, #5c8aff);
border-radius: 50%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
box-shadow: 0 4px 12px rgba(63, 112, 255, 0.4);
z-index: 99;
font-size: 12px;
.floating-text {
line-height: 1.2;
}
}
.complaint-content {
max-height: 60vh;
padding: 0 10px 10px;
.complaint-text {
font-size: 14px;
color: #333;
line-height: 1.6;
.h3 {
font-size: 15px;
font-weight: bold;
margin-top: 16px;
margin-bottom: 8px;
color: #111;
}
.p {
margin-bottom: 8px;
text-align: justify;
color: #666;
}
.phone-link {
color: #3f70ff;
text-decoration: underline;
display: inline-block;
padding: 0 4px;
}
}
}
</style>
<style lang="scss">
.bottom-actions {
.uv-button-wrapper {
width: 30%;
}
}
</style>