diff --git a/.gitignore b/.gitignore
index bfeaeff..0bce61e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,6 @@ unpackage/
node_modules/
.DS_Store
.playwright-mcp/
+
+# xuchao: 本地 agent 说明文档,不入库(与 CLAUDE.md 均保留在本地)
+AGENTS.md
diff --git a/App.vue b/App.vue
index 2f80017..c79112e 100644
--- a/App.vue
+++ b/App.vue
@@ -23,4 +23,23 @@
Arial, Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
}
+
+ /* xuchao: 修复默认导航栏在微信 WebView 内顶部超宽空白(参加/使用指南、协议等页)——
+ uni-app 内置 H5 导航栏高度为 calc(44px + env(safe-area-inset-top)),页面模板带
+ viewport-fit=cover 时,部分 Android 微信 WebView 会误报状态栏高度(而 H5 视口本
+ 就位于微信原生标题栏下方、并未被遮挡),导致导航栏被垫高。强制回退固定 44px。
+ 标准浏览器/ iOS 中该 env 值为 0,此覆盖无副作用 */
+ uni-page-head .uni-page-head {
+ height: 44px !important;
+ padding-top: 7px !important;
+ }
+ uni-page-head .uni-page-head ~ .uni-placeholder {
+ height: 44px !important;
+ }
+ uni-page-head[uni-page-head-type='default'] ~ uni-page-wrapper {
+ height: calc(100% - 44px) !important;
+ }
+ .uni-app--showtabbar uni-page-head[uni-page-head-type='default'] ~ uni-page-wrapper {
+ height: calc(100% - 44px) !important;
+ }
diff --git a/common/account.js b/common/account.js
new file mode 100644
index 0000000..b394963
--- /dev/null
+++ b/common/account.js
@@ -0,0 +1,15 @@
+// xuchao: 账号级本地缓存集中管理。权益页首屏会读这些缓存做兜底展示,
+// 换账号/退出时漏清就会把上一个账号的中奖状态、卡号、到期时间当成当前账号的数据展示。
+const ACCOUNT_KEYS = ['token', 'phone', 'isAuth', 'hasWin', 'cardNumber', 'expiresTime',
+ 'rightsCount', 'totalValueFen', 'authFlowStarted', 'authName', 'authIdNo']
+
+export default {
+ // 清空当前账号的本地会话与权益缓存(定位 lng/lat 属于设备信息,不在此列)
+ clearAccountCache() {
+ ACCOUNT_KEYS.forEach(key => uni.removeStorageSync(key))
+ },
+ // 账号标识(手机号)
+ setAccountPhone(phone) {
+ uni.setStorageSync('phone', phone)
+ }
+}
diff --git a/common/config.js b/common/config.js
new file mode 100644
index 0000000..cbe0328
--- /dev/null
+++ b/common/config.js
@@ -0,0 +1,12 @@
+// xuchao: 全站集中配置项 —— 后端基址/客服电话等只在这里维护一处
+const config = {
+ // 后端 API 基址。当前指向 UAT 测试环境(见 AGENTS.md 部署信息),切生产改这一行即可
+ baseUrl: 'https://dscp-uat.hn12301.net',
+ // 对外统一客服电话,与《用户协议》《隐私协议》公示号码保持一致
+ servicePhone: '0731-88912301',
+ // xuchao: open-api/hhService/login 强制校验固定 password 字段(后端为 H5 开的旁路,
+ // 前端去掉即登录失败,故集中存放并在此说明;待后端改造 H5 免密登录后删除。
+ loginFixedPassword: '1232321'
+}
+
+export default config
diff --git a/common/district.js b/common/district.js
new file mode 100644
index 0000000..f353d3c
--- /dev/null
+++ b/common/district.js
@@ -0,0 +1,10 @@
+// xuchao: 卡片角标用的区县简称。后端 districtName 返回的是 region.name 行政区划全称
+// (如「芷江侗族自治县」「怀化高新技术产业开发区」),列表缩略图只有 75px,必须缩短显示
+export function shortDistrict(name) {
+ if (!name) return ''
+ return name
+ .replace('高新技术产业开发区', '高新区')
+ .replace(/(苗族|侗族|瑶族|回族|土家族|各族)*自治县$/, '')
+ .replace(/(苗族|侗族|瑶族|回族|土家族|各族)*自治州$/, '')
+ .replace(/([^市])县$/, '$1')
+}
diff --git a/common/http.api.js b/common/http.api.js
index 1d02815..fd4fe31 100644
--- a/common/http.api.js
+++ b/common/http.api.js
@@ -1,87 +1,89 @@
-// 如果没有通过拦截器配置域名的话,可以在这里写上完整的URL(加上域名部分)
-let LoginUrl = 'open-api/pingXiang/login';
-let SendVerifyCodeUrl = 'open-api/pingXiang/sendLoginVerifyCode';
-let VenueListUrl = 'mp-api/hhrs/venue/list';
-let VenueDetailUrl = 'mp-api/hhrs/venue/detail';
-let MerchantListUrl = 'mp-api/hhrs/merchant/list';
-let MerchantDetailUrl = 'mp-api/hhrs/merchant/detail';
-let RightsInfoUrl = 'mp-api/hhrs/rights/info';
-let RightsDetailUrl = 'mp-api/hhrsRights/detail';
-let RightsAuthUrl = 'mp-api/hhrs/rights/auth';
-let EligibilityUrl = 'mp-api/hhrsRights/eligibility';
-let RightsBookUrl = 'mp-api/hhrs/rights/book';
-let InitFaceVerifyUrl = 'open-api/aliyun/face/initFaceVerify';
-let DescribeFaceVerifyUrl = 'open-api/aliyun/face/describeFaceVerify';
-let RightsListUrl = 'mp-api/hhrsRights/list';
-let AppointmentListUrl = 'mp-api/hhrsRights/appointment/list';
-let CheckListUrl = 'mp-api/hhrsRights/check/list';
-let StoreListUrl = 'mp-api/hhrsStore/list';
-let StoreDetailUrl = 'mp-api/hhrsStore/detail';
-let WinnerDatesUrl = 'mp-api/hhrsWinner/dates';
-let WinnerListUrl = 'mp-api/hhrsWinner/list';
-let AppointUrl = 'zhy-api/cardBag/appoint';
-let TokenDecodeUrl = 'open-api/hhService/tokenDecode';
-
+// xuchao: 怀化人社 H5 接口清单,与 docs/hhrs-api-doc.md 一一对应
+// 基址由 common/http.interceptor.js 统一注入(common/config.js.baseUrl)
+// 登录 / 云闪付入口
+// xuchao: hhrs 专属登录端点(旧地址 open-api/pingXiang/* 由后端兼容层转发,已弃用)
+const LoginUrl = 'open-api/hhService/login'
+const SendVerifyCodeUrl = 'open-api/hhService/sendLoginVerifyCode'
+const TokenDecodeUrl = 'open-api/hhService/tokenDecode'
+// 门店(文旅场馆 typeId=1 / 商户折扣 typeId=2)
+const StoreListUrl = 'mp-api/hhrsStore/list'
+const StoreDetailUrl = 'mp-api/hhrsStore/detail'
+// 权益包
+const RightsListUrl = 'mp-api/hhrsRights/list'
+const RightsDetailUrl = 'mp-api/hhrsRights/detail'
+const EligibilityUrl = 'mp-api/hhrsRights/eligibility'
+const AppointmentListUrl = 'mp-api/hhrsRights/appointment/list'
+const CheckListUrl = 'mp-api/hhrsRights/check/list'
+// 实名人脸认证(阿里云)
+const InitFaceVerifyUrl = 'open-api/aliyun/face/initFaceVerify'
+const DescribeFaceVerifyUrl = 'open-api/aliyun/face/describeFaceVerify'
+// 中奖公示
+const WinnerDatesUrl = 'mp-api/hhrsWinner/dates'
+const WinnerListUrl = 'mp-api/hhrsWinner/list'
+// 预约下单(智慧游卡包)
+const AppointUrl = 'zhy-api/cardBag/appoint'
+// xuchao: 怀化石油汽油券(下单/预支付/最新订单查询,后端 HhVoucherController)
+const VoucherCreateUrl = 'open-api/hhService/voucher/create'
+const VoucherPrepayUrl = 'open-api/hhService/voucher/prepay'
+const VoucherLatestUrl = 'open-api/hhService/voucher/latest'
+const VoucherCheckCodeUrl = 'open-api/hhService/voucher/checkCode'
// 此处第二个参数vm,就是我们在页面使用的this
const install = (Vue, vm) => {
- let checkerLogin = (params = {}) => vm.$u.post(LoginUrl, params);
- let sendLoginVerifyCode = (params = {}) => vm.$u.post(SendVerifyCodeUrl, params);
+ // 登录与凭证
+ const checkerLogin = (params = {}) => vm.$u.post(LoginUrl, params)
+ const sendLoginVerifyCode = (params = {}) => vm.$u.post(SendVerifyCodeUrl, params)
// xuchao: 云闪付入口 token 解码
- let tokenDecode = (params = {}) => vm.$u.post(TokenDecodeUrl, params);
+ const tokenDecode = (params = {}) => vm.$u.post(TokenDecodeUrl, params)
- // 文旅场馆
- let getVenueList = (params = {}) => vm.$u.get(VenueListUrl, params);
- let getVenueDetail = (params = {}) => vm.$u.post(VenueDetailUrl, params);
-
- // 商户折扣
- let getMerchantList = (params = {}) => vm.$u.get(MerchantListUrl, params);
- let getMerchantDetail = (params = {}) => vm.$u.post(MerchantDetailUrl, params);
-
- // 中奖名单
- let getWinnerDates = (params = {}) => vm.$u.get(WinnerDatesUrl, params);
- let getWinnerList = (params = {}) => vm.$u.post(WinnerListUrl, params);
- let doAppoint = (params = {}) => vm.$u.post(AppointUrl, params);
+ // 门店列表/详情
+ const getStoreList = (params = {}) => vm.$u.post(StoreListUrl, params)
+ const getStoreDetail = (params = {}) => vm.$u.post(StoreDetailUrl, params)
// 权益包
- let getRightsInfo = (params = {}) => vm.$u.get(RightsInfoUrl, params);
- let getRightsDetail = (params = {}) => vm.$u.post(RightsDetailUrl, params);
- let doRightsAuth = (params = {}) => vm.$u.post(RightsAuthUrl, params);
- let doRightsBook = (params = {}) => vm.$u.post(RightsBookUrl, params);
- let getEligibility = (params = {}) => vm.$u.post(EligibilityUrl, params);
- let initFaceVerify = (params = {}) => vm.$u.post(InitFaceVerifyUrl, params);
- let describeFaceVerify = (params = {}) => vm.$u.post(DescribeFaceVerifyUrl, params);
- let getRightsList = (params = {}) => vm.$u.post(RightsListUrl, params);
- let getAppointmentList = (params = {}) => vm.$u.post(AppointmentListUrl, params);
- let getCheckList = (params = {}) => vm.$u.post(CheckListUrl, params);
- let getStoreList = (params = {}) => vm.$u.post(StoreListUrl, params);
- let getStoreDetail = (params = {}) => vm.$u.post(StoreDetailUrl, params);
+ const getRightsList = (params = {}) => vm.$u.post(RightsListUrl, params)
+ const getRightsDetail = (params = {}) => vm.$u.post(RightsDetailUrl, params)
+ // xuchao: 注意 identityNo 会触发后端“被动认证”回写,仅在认证流程中传
+ const getEligibility = (params = {}) => vm.$u.post(EligibilityUrl, params)
+ const getAppointmentList = (params = {}) => vm.$u.post(AppointmentListUrl, params)
+ const getCheckList = (params = {}) => vm.$u.post(CheckListUrl, params)
+ const doAppoint = (params = {}) => vm.$u.post(AppointUrl, params)
+
+ // 人脸核身
+ const initFaceVerify = (params = {}) => vm.$u.post(InitFaceVerifyUrl, params)
+ const describeFaceVerify = (params = {}) => vm.$u.post(DescribeFaceVerifyUrl, params)
+
+ // 中奖名单
+ const getWinnerDates = (params = {}) => vm.$u.get(WinnerDatesUrl, params)
+ const getWinnerList = (params = {}) => vm.$u.post(WinnerListUrl, params)
+
+ // xuchao: 石油汽油券
+ const voucherCreate = (params = {}) => vm.$u.post(VoucherCreateUrl, params)
+ const voucherPrepay = (params = {}) => vm.$u.post(VoucherPrepayUrl, params)
+ const voucherLatest = (params = {}) => vm.$u.post(VoucherLatestUrl, params)
+ const voucherCheckCode = (params = {}) => vm.$u.post(VoucherCheckCodeUrl, params)
vm.$u.api = {
checkerLogin,
sendLoginVerifyCode,
tokenDecode,
- getVenueList,
- getVenueDetail,
- getMerchantList,
- getMerchantDetail,
- getWinnerList,
- getRightsInfo,
- getRightsDetail,
- doRightsAuth,
- doRightsBook,
- getEligibility,
- initFaceVerify,
- describeFaceVerify,
- getRightsList,
- getAppointmentList,
- getCheckList,
getStoreList,
getStoreDetail,
+ getRightsList,
+ getRightsDetail,
+ getEligibility,
+ getAppointmentList,
+ getCheckList,
+ doAppoint,
+ initFaceVerify,
+ describeFaceVerify,
getWinnerDates,
getWinnerList,
- doAppoint,
- };
+ voucherCreate,
+ voucherPrepay,
+ voucherLatest,
+ voucherCheckCode
+ }
}
export default {
diff --git a/common/http.interceptor.js b/common/http.interceptor.js
index 0f824c3..4d8e988 100644
--- a/common/http.interceptor.js
+++ b/common/http.interceptor.js
@@ -1,8 +1,11 @@
+import config from '@/common/config.js'
+
// 这里的vm,就是我们在vue文件里面的this,所以我们能在这里获取vuex的变量,比如存放在里面的token
// 同时,我们也可以在此使用getApp().globalData,如果你把token放在getApp().globalData的话,也是可以使用的
const install = (Vue, vm) => {
Vue.prototype.$u.http.setConfig({
- baseUrl: 'https://dscp-uat.hn12301.net',
+ // xuchao: 基址统一取 common/config.js,避免多处硬编码
+ baseUrl: config.baseUrl,
// loadingText: '努力加载中~',
// loadingTime: 18800,
// 如果将此值设置为true,拦截回调中将会返回服务端返回的所有数据response,而不是response.data
@@ -41,10 +44,17 @@ const install = (Vue, vm) => {
// 如果把originalData设置为了true,这里return回什么,this.$u.post的then回调中就会得到什么
return res;
- }else if(res.errcode==-99){
- uni.navigateTo({
- url:'/pages/index/login'
- })
+ }else if(res.errcode==-99 || res.errcode==30002){
+ // xuchao: 未登录/会话失效(-99 自定义、30002 框架 SESSION_AUTH_FAIL)—— 已在登录页则不再入栈
+ const stack = getCurrentPages() || []
+ const top = stack.length ? stack[stack.length - 1].route : ''
+ if (top !== 'pages/index/login') {
+ uni.navigateTo({
+ url:'/pages/index/login'
+ })
+ }
+ // xuchao: 必须把 res 交回调用方,否则页面 .then(res => res.errcode) 会因 undefined 抛错
+ return res
}else {
// uni.showToast({
diff --git a/common/petrolVoucher.js b/common/petrolVoucher.js
new file mode 100644
index 0000000..dc13b84
--- /dev/null
+++ b/common/petrolVoucher.js
@@ -0,0 +1,66 @@
+// xuchao: 怀化石油汽油减免券——前端常量与工具(全写死,只服务这一个门店,决策 1/10/15)
+// 价格真相=后端配置 hh.voucher.price-fen 经 latest 下发;此处 200.00 仅为接口未返回时的展示兜底
+
+export const PETROL_SHOP_ID = '851219003520709'
+export const VOUCHER_PRICE_TEXT = '200.00'
+
+export const VOUCHER_NAME = '怀化石油汽油电子券'
+
+export const VOUCHER_GUIDE_TEXT = '请在「云闪付 App」里的「湖南社保卡」小程序首页进入「怀化社保卡」活动中完成购买'
+export const VOUCHER_CANCEL_TEXT = '支付已取消,请重新支付'
+
+// 券详情页「使用规则 / 使用流程 / 重要须知」文案,来自石油侧材料(决策 14)
+export const VOUCHER_USE_RULES = [
+ '请加油站收银员以“中石油汽油券”通道扫码核销',
+ '汽油券单日限用 1 张,多张可叠加使用',
+ '自购买日起 90 天内有效',
+ '券一经兑换不可退换'
+]
+export const VOUCHER_USE_FLOW = [
+ '加油后至收银台出示二维码核销',
+ '支付差额可选:油卡 / 现金等'
+]
+
+// xuchao: 云闪付 SDK 前置预热注入。HBuilderX 的 H5 构建不吃项目根 index.html(静态标签会被丢),
+// 只能在 main.js 最早期(document-start、App 挂载前)动态注入,逼近 xghmall 的 head 静态加载时机,
+// 让云闪付原生控件桥在首屏就注册,避免到确认订单页才注入导致 upsdk.pay 唤不起。
+// 与 orderConfirm 页的 loadUpSdk 共用同一 script[data-upsdk] 标记,全局只注入一次。
+export function injectUpSdk() {
+ if (typeof document === 'undefined' || typeof window === 'undefined') return
+ if (window.upsdk || document.querySelector('script[data-upsdk]')) return
+ const s = document.createElement('script')
+ s.src = 'https://open.95516.com/s/open/js/upsdk.js'
+ s.setAttribute('data-upsdk', '1')
+ document.head.appendChild(s)
+}
+
+
+// xuchao: open-api/hhService/voucher/* 为双层壳:外层 errcode,内层 data.code 才是业务码
+// (同 login.vue 的 isBizOk 判定,提到公共层复用)
+export function bizOk(res) {
+ if (!res || res.errcode !== 0) return false
+ const d = res.data
+ if (!d || d.code === undefined || d.code === null) {
+ return !d || (d.status === undefined && d.error === undefined)
+ }
+ return d.code === 200
+}
+
+export function bizMsg(res) {
+ return (res && res.data && res.data.msg) || (res && res.errmsg) || ''
+}
+
+export function bizData(res) {
+ return (res && res.data && res.data.data) || {}
+}
+
+// statusCode 含义与后端 HhVoucherService.toView 对齐:-1无单 0待支付 1发券中 2已发券 3已退款 4退款处理中 5已关闭
+export const VOUCHER_STATUS_TEXT = {
+ '-1': '',
+ 0: '待支付',
+ 1: '发券中',
+ 2: '有效',
+ 3: '已退款',
+ 4: '退款处理中',
+ 5: '已关闭'
+}
diff --git a/components/TabBar/TabBar.vue b/components/TabBar/TabBar.vue
deleted file mode 100644
index 8dc7d3d..0000000
--- a/components/TabBar/TabBar.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-
-