feat: 接口联调

This commit is contained in:
zc 2025-10-13 19:17:49 +08:00
parent beded95be0
commit 5f446bc97a
32 changed files with 935 additions and 102 deletions

8
components.d.ts vendored
View File

@ -10,23 +10,15 @@ declare module 'vue' {
export interface GlobalComponents {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
VanArea: typeof import('vant/es')['Area']
VanButton: typeof import('vant/es')['Button']
VanCell: typeof import('vant/es')['Cell']
VanCellGroup: typeof import('vant/es')['CellGroup']
VanCheckbox: typeof import('vant/es')['Checkbox']
VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup']
VanField: typeof import('vant/es')['Field']
VanForm: typeof import('vant/es')['Form']
VanIcon: typeof import('vant/es')['Icon']
VanNoticeBar: typeof import('vant/es')['NoticeBar']
VanPopover: typeof import('vant/es')['Popover']
VanPopup: typeof import('vant/es')['Popup']
VanRate: typeof import('vant/es')['Rate']
VanSearch: typeof import('vant/es')['Search']
VanTab: typeof import('vant/es')['Tab']
VanTabs: typeof import('vant/es')['Tabs']
VanTag: typeof import('vant/es')['Tag']
VanUploader: typeof import('vant/es')['Uploader']
}
}

View File

@ -2,6 +2,7 @@ import { globalIgnores } from 'eslint/config'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import pluginVue from 'eslint-plugin-vue'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
const path = require('path')
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
@ -16,6 +17,8 @@ export default defineConfigWithVueTs(
{
languageOptions: {
parserOptions: {
sconfigRootDir: path.resolve(__dirname),
project: './tsconfig.json',
requireConfigFile: false,
},
},

View File

@ -18,8 +18,12 @@
"dependencies": {
"@vant/area-data": "^2.1.0",
"axios": "^1.11.0",
"capture-request-log": "^0.0.4",
"js-base64": "^3.7.8",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"lz-utils-lib": "^1.0.60",
"mobile-detect": "^1.4.5",
"normalize.css": "^8.0.1",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.5.0",

8
src/api/commodity.ts Normal file
View File

@ -0,0 +1,8 @@
// 商品维度接口
const shop = {
searchCommodityList: ['/index/page/list'], // 获取商品列表接口
getCommodityDetail: ['/index/product/detail'], // 获取商品详情
getReviewList: ['/comment/list'], // 获取评价接口
}
export default shop

4
src/api/common.ts Normal file
View File

@ -0,0 +1,4 @@
// 公共接口
const common = {}
export default common

22
src/api/index.ts Normal file
View File

@ -0,0 +1,22 @@
import axios from '@/utils/axios'
import user from './user'
import shop from './commodity'
import common from './common'
const totalApiConfig = { user, shop, common }
const handleAddArrayPrototype = (obj: Recordable<[string, Recordable<any>]>) => {
Object.values(obj).forEach((arr) => {
const [url, other = {}] = arr
Object.setPrototypeOf(arr, {
post: <T>(data = {}) => axios.post<T>({ url, data, ...other }),
get: <T>(data = {}) => axios.get<T>({ url, params: data, ...other }),
})
})
}
Object.values(totalApiConfig).forEach((apiItem) => {
handleAddArrayPrototype(apiItem as any)
})
export default totalApiConfig

7
src/api/user.ts Normal file
View File

@ -0,0 +1,7 @@
// 用户维度接口
const user = {
getAddressList: ['/buyer/address/list'],
updateAddress: ['/buyer/address/insertOrUpdate'],
}
export default user

View File

@ -49,6 +49,7 @@ declare global {
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const showToast: typeof import('vant/es')['showToast']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']

View File

@ -1,9 +1,11 @@
import 'virtual:uno.css'
import 'normalize.css'
import "@/assets/index.scss"
import '@/assets/index.scss'
import '../types/global.d.ts'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import VConsole from 'vconsole'
import App from './App.vue'
@ -11,7 +13,7 @@ import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(createPinia().use(piniaPluginPersistedstate))
app.use(router)
app.mount('#app')

15
src/settings/index.ts Normal file
View File

@ -0,0 +1,15 @@
import type { AppConfigType, PrefixType } from '#/config'
export * from './log'
export const appConfig: Record<PrefixType, AppConfigType> = {
dcd: {
name: '东成贷',
prefix: 'dcd',
},
}
export const loginPaths = []
const LOCAL_PATH = 'test'
export const baseURL = import.meta.env.DEV ? `/${LOCAL_PATH}/api-interface/app` : '/api-interface/app'

12
src/settings/log.ts Normal file
View File

@ -0,0 +1,12 @@
export const logConfig = {
// 前端日志管理过滤域名
interceptDomain: ['https://tapp.dongchengdai.com', 'https://app.dongchengdai.com'],
// 前端日志过滤请求地址
filterHttpUrl: ['bl-log/web', '/user/baseInfo', '/log/behaviorLogDcd'],
// 前端日志管理接口
bllogAddUrl: 'https://bllog.yijiesudai.com/bl-log/web/log/add',
// vconsole过滤域名
vConsoleDomain: '',
// sdk过滤域名
sdkDomain: ''
}

View File

@ -1,7 +0,0 @@
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', () => {
const themeColor = '#01CF24'
return { themeColor }
})

11
src/stores/index.ts Normal file
View File

@ -0,0 +1,11 @@
import useAppStore from './modules/app'
import useNativeStore from './modules/native'
import useLoginStore from './modules/login'
const useStore = () => ({
appStore: useAppStore(),
nativeStore: useNativeStore(),
userStore: useLoginStore(),
})
export default useStore

32
src/stores/modules/app.ts Normal file
View File

@ -0,0 +1,32 @@
import { defineStore } from 'pinia'
import type { AppConfigType, EnvType, PlatType } from '#/config'
export default defineStore(
'app',
() => {
const themeColor = '#01CF24'
const env = ref('')
const platform = ref('')
const setPlatform = (val: PlatType) => {
platform.value = val
}
const setEnv = () => {
const envs: Recordable<EnvType> = {
app: 'online',
tapp: 'test',
}
const key = location.host.replace(/^([a-z]+).+/g, '$1')
env.value = envs[key] || 'dev'
}
return { themeColor, env, setPlatform, setEnv }
},
{
persist: {
key: 'appConfig',
storage: localStorage,
},
},
)

125
src/stores/modules/login.ts Normal file
View File

@ -0,0 +1,125 @@
import axios from 'axios'
import Log from 'capture-request-log'
import { handleData } from 'lz-utils-lib'
import { defineStore } from 'pinia'
import { logConfig } from '@/settings'
import { getPhoneType, log } from '@/utils/common'
// import { secretKeyAesKey } from '@/utils/secret-key'
// import type { PlatType } from '#/config'
import useAppStore from './app'
export default defineStore('login', {
state: () => {
return {
userInfo: {
uuid: '',
token: '',
mobile: '',
},
environment: '',
}
},
actions: {
async saveLoginInfo(loginData: string) {
const appStore = useAppStore()
try {
const data = handleData.tobase64.decodeObj(decodeURIComponent(loginData))
console.log('token:', data.userInfo.token)
console.log('environment:', data.environment)
console.log('appPackage:', data.appInfo.appPackage)
console.log('channel:', data.appInfo.channel)
appStore.setEnv()
this.userInfo = {
uuid: data.userInfo.uuid,
token: data.userInfo.token,
mobile: '',
}
/* let platform = ''
if (data.appInfo) {
platform = data.appInfo.appPackage.replace(/_\w+/, '')
appStore.setPlatform(platform as PlatType)
}
if (data.environment) {
this.environment = data.environment
} */
return Promise.resolve()
} catch (e) {
console.error('传入returnData有误')
}
},
clearLogin() {
this.$reset()
//testzc VueCookie.delete('loginData')
},
// 清除path直接用
clearPathSearch(keys: string[]) {
let path = location.search.slice(1)
let str = keys.reduce((data, cur) => {
data += cur + '|'
return data
}, '')
str = str.slice(0, -1)
path = path.replace(new RegExp(`((${str})=[^&]+)`, 'g'), () => '')
path = path.replace(/^[&]+/, '')
path = path.replace(/([&]+)/g, '&')
path = path ? `?${path}` : path
const time = setTimeout(() => {
clearTimeout(time)
window.history.replaceState(null, '', `${location.pathname}${path}`)
}, 1000)
},
// 上传token和git log可用微调
async handleUploadToken(token: string, mobile: string) {
const divGitInfo = (document.querySelector('#div-git-info') as HTMLElement)?.dataset.gitInfo
const oldData = { commitId: '' }
if (divGitInfo) {
divGitInfo.replace(/([^&=]+)=([^&]+)/g, (_, k, v) => (oldData[k as keyof typeof oldData] = v))
}
/* apiCommon.uploadToken.post({
mobile,
msg: JSON.stringify([`<东成贷> 联合登录url参数:${oldData.commitId}`, `token=${token}`]),
deviceType: getPhoneType(),
systemCode: 'dongchengdai',
systemCodeName: '东成贷',
projectCode: 'DCD_H5',
projectCodeName: '东成贷H5',
title: 'consoleLog',
type: 'console',
url: location.origin + location.pathname,
}) */
},
// 前端日志上报(可用,微调)
async handleCreateLog(app: any) {
const appStore = useAppStore()
let mobile: string
new Log({
window,
Vue: appStore.env !== 'dev' ? app : undefined,
filterHttpUrl: logConfig.filterHttpUrl,
interceptDomain: logConfig.interceptDomain,
sendError: async (obj: any = {}) => {
const msgOb = JSON.parse(obj.msg || '{}')
const url = msgOb.url ? msgOb.url.replace(/.+app-web/g, '') : ''
Object.assign(obj, {
// mobile,
// secretKeyAesKey: secretKeyAesKey[url],
systemCode: 'dongchengdai',
systemCodeName: '东成贷',
deviceType: getPhoneType(),
projectCode: 'DCD_H5',
projectCodeName: '东成贷H5',
})
// delete secretKeyAesKey[url]
axios.post(logConfig.bllogAddUrl, obj)
},
})
window.log = log
},
},
persist: {
key: 'login',
storage: localStorage,
},
})

View File

@ -0,0 +1,46 @@
import type { DCAndNativeNameType, DCNativeNameType } from 'lz-utils-lib'
import { DC, handleData } from 'lz-utils-lib'
import { defineStore } from 'pinia'
import api from '@/api'
import router from '@/router'
import { handleGoThirdPage } from '@/utils/common'
import { openMinProgram } from '@/utils/wx-minprogram'
export default defineStore('native', {
state: (): { appInfo: any } => {
return {
appInfo: Object.create(null) as any,
}
},
getters: {
version(state) {
return state.appInfo.version ? state.appInfo.version : ''
},
appPackage() {
return 'xcx_dc'
},
},
actions: {
jumpRouter(type) {
if (this.appPackage === 'xcx_dc') {
switch (type) {
case 'home':
openMinProgram('index')
break
case 'logout':
openMinProgram('index')
break
case 'login':
openMinProgram('login')
break
default:
}
}
},
},
persist: {
key: 'nativeConfig',
storage: localStorage,
},
})

97
src/utils/axios.ts Normal file
View File

@ -0,0 +1,97 @@
import 'vant/es/toast/style'
import axios, { AxiosError, type AxiosInstance, type AxiosRequestConfig, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios'
import { showToast } from 'vant'
import { baseURL } from '@/settings'
import useStore from '@/stores'
import { loadingData } from './page'
// 创建axios实例
const service: AxiosInstance = axios.create({
baseURL, // api 的 base_url
timeout: 150000, // 请求超时时间
})
const noEncryptWhiteList = ['/common/protocol/content', '/portal/common/protocol/content', '/bl-log/web/log/add']
const noConsoleWhiteList = ['bl-log/web/log/add']
// request拦截器
service.interceptors.request.use(
async (config: InternalAxiosRequestConfig) => {
const { userStore, appStore } = useStore()
if (!/^http/.test(config.url!)) {
config.headers!.token = userStore.userInfo.token || ''
}
if (!(config.headers!['Content-Type'] as string)?.endsWith('multipart/form-data')) {
if (config.method === 'get') {
if (config.params?.isLoading) {
config.isLoading = true
delete config.params.isLoading
loadingData.type === 'submit' && (loadingData.show = true)
}
} else {
config.data.buyerId = 2 // testzc
config.data.buyerWeixin = '我是微信号' // testzc
config.isLoading = config.data.isLoading
delete config.data.isLoading
config.isLoading && loadingData.type === 'submit' && (loadingData.show = true)
}
}
return config
},
(error: AxiosError) => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
},
)
// response 拦截器
service.interceptors.response.use(
(response: AxiosResponse<any>) => {
const { nativeStore } = useStore()
if (response.config.responseType === 'blob' || response.config.third) {
// 如果是文件流,直接过
return response
}
if (!['200'].includes(response.data.code)) {
showToast({ message: response.data.desc, duration: 3000 })
if (response.data.code === 401) {
setTimeout(() => {
nativeStore.jumpRouter('login')
}, 1000)
return
}
if (response.config.isLoading && loadingData.type === 'submit') {
loadingData.show = false
}
return Promise.reject(response.data)
}
if (response.data.code === '200' || response.config.toast === false) {
return response.data
}
if (response.config.isLoading && loadingData.type === 'submit') {
loadingData.show = false
}
setTimeout(() => {
showToast({ message: response.data.msg, duration: 3000 })
}, 100)
return Promise.reject(response.data)
},
(error: AxiosError) => {
console.log(`err${error}`) // for debug
showToast(error.message)
loadingData.type === 'submit' && (loadingData.show = false)
return Promise.reject(error)
},
)
export const request = <T, K, F>(config: AxiosRequestConfig<T>): Promise<F extends { third: boolean } ? K : { code: string | number; msg: string; data: K }> => service.request(config)
export default {
get: <K = unknown, F = string, T = object>(config: AxiosRequestConfig<T>) => request<T, K, F>({ ...config, method: 'get' }),
post: <K = unknown, F = string, T = object>(config: AxiosRequestConfig<T>) => request<T, K, F>({ ...config, method: 'post' }),
delete: <K = unknown, F = string, T = object>(config: AxiosRequestConfig<T>) => request<T, K, F>({ ...config, method: 'delete' }),
put: <K = unknown, F = string, T = object>(config: AxiosRequestConfig<T>) => request<T, K, F>({ ...config, method: 'put' }),
}

99
src/utils/common.ts Normal file
View File

@ -0,0 +1,99 @@
import { logConfig } from '@/settings'
import { map } from 'lodash-es'
import MobileDetect from 'mobile-detect'
import { getData } from 'lz-utils-lib'
import useAppStore from '@/stores/modules/app'
import router from '@/router'
// 获取手机具体型号
export const getPhoneType = () => {
try {
const deviceType = navigator.userAgent // 获取userAgent信息
const md = new MobileDetect(deviceType) // 初始化mobile-detect
let os = md.os() // 获取系统
let model = '' as any
if (os === 'iOS') {
// ios系统的处理
os = md.os() + ' ' + md.version('iPhone')
model = md.mobile()
} else if (os === 'AndroidOS') {
// Android系统的处理
os = md.os().replace('OS', '') + ' ' + md.version('Android')
const sss = deviceType.split(';')
const i = sss.findIndex((val) => val.includes('Build/'))
if (i) {
model = sss[i].substring(0, sss[i].indexOf('Build/')).trim()
}
}
return os + ' ' + model
} catch (e) {
return '未知型号'
}
}
// 判断是线上还是开发环境打印日志
export const log = (...arg: any) => {
try {
if (logConfig.interceptDomain.includes(location.origin)) {
// 线上环境
window.captureLog(...arg)
} else {
console.log(...arg)
}
} catch (e) {
console.log(...arg)
}
}
// url参数变成参数字符串
export const getQuerystring = (data: Record<string, any>) => {
return map(data, (val: any, key: string) => `${encodeURIComponent(key)}=${encodeURIComponent(typeof val === 'object' ? JSON.stringify(val) : val)}`).join('&')
}
/**
* url
* @param url: https://www.baidu.com; https://lth5.yijiesudai.com/unicom/a; /borrow/money;
*/
export const handleGoThirdPage = (url: string, name?: string, urlParams?: Record<string, any>) => {
const newParams = { ...getData.getUrlParams(url), ...(urlParams || {}) }
const query = getQuerystring(newParams)
const newUrl = url.split('?')[0]
if (newUrl.includes(`/${import.meta.env.VITE_APP_CODE}/`)) {
const regex = new RegExp(`.*(/${import.meta.env.VITE_APP_CODE})([^?]+).*`)
const path = newUrl.replace(regex, '$2')
router.push({ path, query: newParams })
} else if (newUrl.endsWith('.pdf')) {
handleGoPdfPage(newUrl, name, url.split('?')[1])
} else {
window.location.href = newUrl + '?' + query
}
}
/**
* pdf跳转
* @param url pdf链接
*/
export const handleGoPdfPage = (url: string, name: string, search: string = '') => {
const appStore = useAppStore()
/*
// 小程序:等小程序上线后再使用
if (appStore.platform === 'xcx') {
// window.useLoading({ type: 'submit', show: true })
// window.wx.miniProgram.postMessage({ data: { pdfUrl: url + '?' + search } })
window.wx.miniProgram.navigateTo({ url: `/pages/pdf/index?url=${encodeURIComponent(url)}` })
} else */
if (getPhoneType().toLocaleLowerCase().startsWith('ios')) {
window.location.href = url + '?' + search
} else {
sessionStorage.setItem('picUrlName', name)
const prefixConfig = {
'ltf.yijiesudai': '/yjsd',
'dcd-pri.oss-cn-hangzhou': '/dcd-pri',
'dcd-pri.dongchengdai.com': '/dcd-pri',
'ltyijie.oss-cn-hangzhou': '/yjsd',
}
const prefix = prefixConfig[Object.keys(prefixConfig).find((key) => url.includes(key))]
const newUrl = url.replace(/(https?:\/\/)?[^/?]+/, window.location.origin + (appStore.env === 'dev' ? '/test' : '') + prefix)
window.location.href = window.location.origin + '/dcmb/pdfjs/web/viewer.html?file=' + encodeURIComponent(newUrl + '?' + search)
}
}

63
src/utils/page.ts Normal file
View File

@ -0,0 +1,63 @@
import { closeToast, showLoadingToast, type ToastWrapperInstance } from 'vant'
interface ApiType {
api: <T, K>(data?: T) => Promise<{ code: string; msg: string; data: K }>
params?: object
}
export const useCreatePageData = (apis: ApiType[]) => {
return Promise.all(apis.map((item: ApiType) => item.api(item.params)))
}
// 提交loading
export const useSubmitLoading = (message = '提交中...') => {
showLoadingToast({ duration: 0, overlay: true, forbidClick: true, message })
}
useSubmitLoading.close = () => {
closeToast()
}
// 将日期转为数组
export const handleDateToArray = (date: Date) => {
return [date.getFullYear().toString(), (date.getMonth() + 1).toString().padStart(2, '0'), date.getDate().toString().padStart(2, '0')]
}
window.isBgWhite = ref(true)
export const isBgWhite = window.isBgWhite
// 获取loading
export const loadingData = reactive({ if: null as null | boolean, type: 'enter', show: false })
window.useLoading = (ob: { type: 'enter' | 'submit'; show: boolean } = { type: 'enter', show: true }) => {
loadingData.if ??= true
return Object.assign(loadingData, ob)
}
// 提示弹窗
export const tipDialogData = reactive({
if: null as null | boolean,
show: false,
showCloseIcon: false,
showCancelBtn: false,
cancelBtnName: '',
title: '',
html: '',
btnName: '',
callback: (() => {}) as (result: boolean) => void | Promise<any>,
})
window.useTipDialog = (
callback: (result: boolean) => void | Promise<any>,
config: {
title: string
html: string
btnName: string
showCloseIcon?: boolean
showCancelBtn?: boolean
cancelBtnName?: string
},
) => {
tipDialogData.showCancelBtn = false
Object.assign(tipDialogData, config)
tipDialogData.if ??= true
tipDialogData.show = true
tipDialogData.callback = callback
}

View File

@ -0,0 +1,36 @@
import api from '@/api'
/**
* H5打开小程序页面
* @param {string} type index首页mine我的
*/
export const openMinProgram = (type = 'index') => {
if (['index', 'mine'].includes(type)) {
window.wx.miniProgram.switchTab({ url: `/pages/${type}/index` })
} else {
window.wx.miniProgram.redirectTo({ url: `/pages/${type}/index` })
}
}
/**
* H5打开小程序页面
* @param {string} type index首页mine我的webview页面
* @param {string} h5Url H5页面地址type=webview时存在此值
*/
export const h5OpenMinProgram = (type = 'index', h5Url = '') => {
const params = {}
if (type) {
Object.assign(params, { backUrl: `/pages/${type}/index` })
}
if (h5Url && !h5Url.startsWith('https')) {
Object.assign(params, {
query: `url=${encodeURIComponent(`${window.location.origin}${h5Url}`)}`
})
}
api.getSchemeUrl.post(params).then(res => {
if (res.data) {
window.location.href = res.data as any
} else {
showToast('跳转链接有误,请联系客服!')
}
})
}

View File

@ -4,17 +4,15 @@
<div class="pt-3 flex-1 overflow-y-scroll">
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field v-model="formData.name" label="名字" placeholder="请输入收货人名称" :rules="[{ required: true, message: '请输入收货人名称' }]" />
<van-field v-model="formData.mobile" label="手机号" placeholder="请输入收货人手机号" :rules="[{ required: true, message: '请输入收货人手机号' }]" />
<van-field v-model="formData.area" is-link readonly name="area" label="地区选择" placeholder="点击选择省市区" @click="areaData.show = true" />
<van-field v-model="formData.addressDetail" rows="3" autosize label="详细地址" type="textarea" placeholder="请输入详细地址" :rules="[{ required: true, message: '请输入详细地址' }]" />
<van-field name="checkboxGroup" label="" class="py-1!">
<template #input>
<van-checkbox-group v-model="formData.default" direction="horizontal">
<van-checkbox :name="1" shape="square" class="text-xs">默认地址</van-checkbox>
</van-checkbox-group>
<van-field v-model="formData.buyerName" label="名字" placeholder="请输入收货人名称" :rules="[{ required: true, message: '请输入收货人名称' }]" />
<van-field v-model="formData.buyerPhone" label="手机号" placeholder="请输入收货人手机号" :rules="[{ required: true, message: '请输入收货人手机号' }]" />
<van-field v-model="formData.areaName" is-link readonly name="areaName" label="地区选择" placeholder="点击选择省市区" @click="areaData.show = true" />
<van-field v-model="formData.detail" rows="3" autosize label="详细地址" type="textarea" placeholder="请输入详细地址" :rules="[{ required: true, message: '请输入详细地址' }]" />
<van-cell title="设为默认地址">
<template #right-icon>
<van-switch v-model="formData.status" size=".2rem" active-value="default" inactive-value="common" :active-color="appStore.themeColor" />
</template>
</van-field>
</van-cell>
</van-cell-group>
<div class="bg-white w-full h-12 p-2 box-border">
<button class="block w-full h-full border-none theme-bg-color color-white rounded-[.04rem]" native-type="submit">确定</button>
@ -23,26 +21,62 @@
</div>
</van-popup>
<van-popup v-model:show="areaData.show" destroy-on-close position="bottom">
<van-area :area-list="areaList" :model-value="areaData.pickerValue" @confirm="onSelectArea" @cancel="areaData.show = false" />
<van-area :area-list="areaList" :model-value="areaData.areaCode" @confirm="onSelectArea" @cancel="areaData.show = false" />
</van-popup>
</template>
<script setup lang="ts">
import api from '@/api'
import useAppStore from '@/stores/modules/app'
import { areaList } from '@vant/area-data'
const model = defineModel<boolean>()
defineProps<{ isEdit: boolean }>()
const formData = reactive({ name: '', mobile: '', area: '', addressDetail: '', province: '', city: '', region: '', default: [] })
const formData = ref({ id: NaN, buyerName: '', buyerPhone: '', areaName: '', district: '', detail: '', province: '', city: '', status: 'common' })
const areaData = reactive({ show: false, pickerValue: '' })
const areaData = reactive({ show: false, areaCode: '' })
const appStore = useAppStore()
const editData = defineModel<Recordable<any>>('editData')
watch(
model,
(val) => {
console.warn('----- my data is 111: ', editData.value.id)
if (val && editData.value.id) {
const data = editData.value
formData.value = {
id: data.id,
buyerName: data.buyerName,
buyerPhone: data.buyerPhone,
areaName: data.province + ' ' + data.city + ' ' + data.district,
district: data.district,
detail: data.detail,
province: data.province,
city: data.city,
status: data.status,
}
areaData.areaCode = data.areaCode
}
},
{ immediate: true },
)
const onSelectArea = ({ selectedValues, selectedOptions }: { selectedOptions: any; selectedValues: any }) => {
areaData.pickerValue = selectedValues.length ? selectedValues[selectedValues.length - 1] : ''
areaData.areaCode = selectedValues.length ? selectedValues[selectedValues.length - 1] : ''
areaData.show = false
formData.area = selectedOptions.map((item: any) => item.text).join(' ')
formData.value.province = selectedOptions[0].text
formData.value.city = selectedOptions[1].text
formData.value.district = selectedOptions[2].text
formData.value.areaName = selectedOptions.map((item: any) => item.text).join(' ')
}
const emits = defineEmits(['confirm'])
const onSubmit = () => {
model.value = false
const params = { ...formData.value, contry: '中国', areaCode: areaData.areaCode }
delete params.areaName
api.user.updateAddress.post(params).then(() => {
model.value = false
emits('confirm')
})
}
</script>

View File

@ -1,49 +1,86 @@
<template>
<van-popup id="address-list" class="flex flex-col" v-model:show="model" position="bottom" :style="{ height: '70%' }" closeable round>
<h3 class="text-center pt-3 text-base">收货地址管理</h3>
<h4 class="flex justify-between items-center px-4 py-4 text-sm font-bold">
<span>常用地址</span>
<p>
<b @click="isManage = !isManage" class="font-bold">{{ !isManage ? '管理' : '退出管理' }}</b
><b class="ml-3 theme-color font-bold" @click="showAddAddressFormDialog = true">新增地址</b>
</p>
</h4>
<div class="flex-1 overflow-y-scroll">
<van-cell-group class="address-list__container">
<van-cell :class="['address-list__item']" v-for="item in addressList" :key="item.id">
<template #title>
<div class="item__info">
<h6 class="text-[.1rem]">浙江省 杭州市 萧山区 宁围街道</h6>
<p class="item__info__detail">湘湖路与事假达到交叉口 新希望宁问哦小区2幢2单元202</p>
<p class="item__info__subtitle">张丹 15100001111</p>
</div>
</template>
<template #value>
<div class="item__editor text-base">
<van-icon v-if="!isManage" name="edit" color="#999" @click="onEditAddress" />
<van-icon v-else name="delete-o" />
</div>
</template>
</van-cell>
</van-cell-group>
<div class="bg-white w-full h-12 p-2 box-border">
<button class="block w-full h-full border-none bg-red color-white rounded-[.04rem]">确定</button>
<div>
<van-popup id="address-list" class="flex flex-col" v-model:show="model" position="bottom" :style="{ height: '70%' }" closeable round>
<h3 class="text-center pt-3 text-base">收货地址管理</h3>
<h4 class="flex justify-between items-center px-4 py-4 text-sm">
<span>常用地址</span>
<p>
<b @click="isManage = !isManage">{{ !isManage ? '管理' : '退出管理' }}</b
><b class="ml-3 theme-color" @click="onAddOrEditAddress('add')">新增地址</b>
</p>
</h4>
<div class="flex-1 overflow-y-scroll">
<van-cell-group class="address-list__container">
<van-cell :class="['address-list__item']" v-for="item in addressList" :key="item.id">
<template #title>
<div class="item__info">
<h6 class="text-[.1rem]">{{ item.area }}</h6>
<p class="item__info__detail">{{ item.detail }}</p>
<p class="item__info__subtitle text-[.13rem]">{{ item.name }} {{ item.tel }}</p>
</div>
</template>
<template #value>
<div class="item__editor text-base">
<van-icon v-if="!isManage" name="edit" color="#999" @click="onAddOrEditAddress('edit', item.originData)" />
<van-icon v-else name="delete-o" />
</div>
</template>
</van-cell>
</van-cell-group>
<div class="bg-white w-full h-12 p-2 box-border">
<button class="block w-full h-full border-none bg-red color-white rounded-[.04rem]">确定</button>
</div>
</div>
</div>
</van-popup>
</van-popup>
</div>
</template>
<script setup lang="ts">
import api from '@/api'
const model = defineModel<boolean>()
const addressFormType = defineModel<'add' | 'edit'>('addressFormType')
const showAddAddressFormDialog = defineModel<boolean>('showAddAddressFormDialog')
const addressList = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 5 }]
const isUpDateAddressList = defineModel<boolean>('isUpDateAddressList')
const addressList = ref([])
const emits = defineEmits(['edit', 'confirm'])
watch(model, (val) => {
val && handleGetAddressData()
})
//
watch(isUpDateAddressList, (val) => {
val && handleGetAddressData()
isUpDateAddressList.value = false
})
const isManage = ref(false)
const onEditAddress = () => {
const onAddOrEditAddress = (type: 'edit' | 'add', data?: any) => {
showAddAddressFormDialog.value = true
addressFormType.value = 'edit'
if (type === 'edit') {
emits('edit', data)
}
addressFormType.value = type
}
const handleGetAddressData = () => {
api.user.getAddressList.post<any>().then((res) => {
addressList.value = res.data.rows.map((item) => {
const originData = { ...item }
return {
id: item.id,
name: item.buyerName,
tel: item.buyerPhone,
area: item.contry + item.province + item.city + item.district,
detail: item.detail,
isDefault: item.status === 'default',
originData,
}
})
})
}
</script>
@ -62,7 +99,6 @@ const onEditAddress = () => {
width: 0;
}
.item__info__detail {
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

View File

@ -50,14 +50,22 @@
<button class="theme-bg-color color-white" @click="onPayNowBtn">立即购买</button>
</div>
</footer>
<shopping-cart v-model="showShopCart" v-model:showAddressList="showAddressList"></shopping-cart>
<shopping-cart v-model="showShopCart" v-model:showAddressList="showAddressList" v-model:curAddressData="curAddressData"></shopping-cart>
<all-review v-model="showAllReview"></all-review>
<address-list v-model="showAddressList" v-model:showAddAddressFormDialog="showAddAddressFormDialog" v-model:addressFormType="addressFormType"></address-list>
<address-form v-model="showAddAddressFormDialog" :isEdit="addressFormType === 'edit'"></address-form>
<address-list
v-model="showAddressList"
v-model:isUpDateAddressList="isUpDateAddressList"
v-model:showAddAddressFormDialog="showAddAddressFormDialog"
v-model:addressFormType="addressFormType"
@edit="(val) => (editAddressData = val)"
@confirm="(val) => (curAddressData = val)"
></address-list>
<address-form v-model="showAddAddressFormDialog" v-model:editData="editAddressData" :isEdit="addressFormType === 'edit'" @confirm="isUpDateAddressList = true"></address-form>
</div>
</template>
<script lang="ts" setup>
import api from '@/api'
import allReview from './components/all-review.vue'
import reviewSingle from './components/review-single.vue'
import infoTable from './components/info-table.vue'
@ -71,6 +79,15 @@ const showAllReview = ref(false) // 展示全部评价
const showAddressList = ref(false) //
const showAddAddressFormDialog = ref(false) // form
const addressFormType = ref<'add' | 'edit'>('add')
const isUpDateAddressList = ref(false) //
const editAddressData = ref({}) //
const curAddressData = ref({}) //
const route = useRoute()
const init = () => {
handleGetReviews(+route.query.id)
}
//
const onShowAllReview = () => {
showAllReview.value = true
@ -79,6 +96,13 @@ const onShowAllReview = () => {
const onPayNowBtn = () => {
showShopCart.value = true
}
const handleGetReviews = (productId: number) => {
api.shop.getReviewList.post({ productId }).then((res) => {
console.log(res)
})
}
init()
</script>
<style scope lang="scss">

View File

@ -35,7 +35,10 @@
</template>
<script setup lang="ts">
import api from '@/api'
const tabActive = ref('0')
const reviews = ref([
{ id: 1, name: '新疆大柚子水电费水电费水电费手动阀是是否神鼎飞丹砂', rate: 0 },
{ id: 2, name: '新疆大柚子水电费水电费水电费手动阀是是否神鼎飞丹砂', rate: 0 },
@ -52,6 +55,9 @@ const nums = ref([
{ name: '已评价', num: 0 },
{ name: '待评价', num: 0 },
])
const init = () => {}
const router = useRouter()
const onGoReview = (id: number) => {
router.push({ name: 'review/write', query: { id } })

View File

@ -1,27 +1,30 @@
<template>
<div id="search-container" class="h-screen bg-[#f2f2f2] flex flex-col">
<header class="search-header flex items-center p-2">
<van-search class="flex-1 p-0!" background="#f2f2f2" v-model="searchValue" placeholder="请输入搜索关键词" />
<van-search class="flex-1 p-0!" background="#f2f2f2" v-model="searchValue" placeholder="请输入搜索关键词" @search="onSearch(searchValue)" />
<van-icon name="shopping-cart-o" size=".2rem" class="ml-2" />
</header>
<van-tabs v-model:active="searchSort" sticky :color="appStore.themeColor">
<van-tabs v-model:active="searchSort" sticky :color="appStore.themeColor" @change="onChangeSort">
<van-tab title="综合" name="0"></van-tab>
<van-tab title="销量" name="1"></van-tab>
<van-tab title="价格" name="2"></van-tab>
</van-tabs>
<ul class="search-condition bg-white pt-2 flex-1 overflow-y-scroll">
<li v-for="item in searchList" :key="item.id" class="flex item-center p-2 relative" @click="onGoDetail(item.id)">
<img src="" alt="" class="w-24 h-24" />
<img :src="item.mainImageUrl" alt="" class="w-24 h-24" />
<div class="ml-2">
<h4 class="text-[.13rem]">{{ item.name }}</h4>
<p class="text-[#888] mt-1">新鲜优质 | 酸甜多汁 | 足斤足两</p>
<h4 class="text-[.13rem]">{{ item.title }}</h4>
<!-- <p class="text-[#888] mt-1">新鲜优质 | 酸甜多汁 | 足斤足两</p>
<p class="mt-1">
<van-tag plain type="primary" color="#ec4d3c">标签1</van-tag>
<van-tag plain type="primary" color="#ec4d3c">标签2</van-tag>
<van-tag plain type="primary" color="#ec4d3c">标签3</van-tag>
</p>
</p> -->
<p class="mt-3">
<span class="text-[#e71e1e] font-bold">¥<b class="font-bold text-base ml-0.5">0</b>.01</span>
<span class="text-[#e71e1e] font-bold"
>¥<b class="font-bold text-base ml-0.5">{{ item.price.split('.')[0] }}</b
>{{ '.' + item.price.split('.')[0] }}</span
>
</p>
</div>
<van-icon name="add" :color="appStore.themeColor" size=".2rem" class="absolute! right-3 bottom-6 h-5" />
@ -31,20 +34,13 @@
</template>
<script setup lang="ts">
import { useAppStore } from '@/stores/app.ts'
const searchValue = ref('')
import api from '@/api'
import useAppStore from '@/stores/modules/app'
const route = useRoute()
const searchValue = ref(route.query.initValue as string)
const searchSort = ref('0')
const searchList = ref([
{ id: 1, name: '新疆大柚子水电费水电费水电费手动阀是是否神鼎飞丹砂' },
{ id: 2, name: '新疆大柚子水电费水电费水电费手动阀是是否神鼎飞丹砂' },
{ id: 3, name: '新疆大柚子水电费水电费水电费手动阀是是否神鼎飞丹砂' },
{ id: 1, name: '新疆大柚子水电费水电费水电费手动阀是是否神鼎飞丹砂' },
{ id: 2, name: '新疆大柚子水电费水电费水电费手动阀是是否神鼎飞丹砂' },
{ id: 3, name: '新疆大柚子水电费水电费水电费手动阀是是否神鼎飞丹砂' },
{ id: 1, name: '新疆大柚子水电费水电费水电费手动阀是是否神鼎飞丹砂' },
{ id: 2, name: '新疆大柚子水电费水电费水电费手动阀是是否神鼎飞丹砂' },
{ id: 3, name: '新疆大柚子水电费水电费水电费手动阀是是否神鼎飞丹砂' },
])
const searchList = ref([])
const appStore = useAppStore()
@ -52,6 +48,24 @@ const router = useRouter()
const onGoDetail = (id: number) => {
router.push({ name: 'commodity-detail', query: { id } })
}
const onSearch = async (searchValue: string, sort = '0') => {
searchSort.value = sort
const result = await api.shop.searchCommodityList.post<any>({ productName: searchValue, frontPage: 0, isTest: 0, sort: searchSort.value }) // testzc
searchList.value = result.data.rows.map((item) => ({
id: item.id,
title: item.title,
mainImageUrl: item.mainImageUrl,
originPrice: String(item.showSalePrice),
price: String(item.showPromotionPrice),
}))
}
const onChangeSort = async (val: string) => {
onSearch(searchValue.value, val)
}
onSearch(searchValue.value)
</script>
<style scoped lang="scss">

View File

@ -3,10 +3,12 @@
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"strict": false,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"typeRoots": ["./node_modules/@types", "./types"],
"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"#/*": ["./types/*"]
}
}
}

View File

@ -1,19 +1,18 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "nightwatch.conf.*", "playwright.config.*", "eslint.config.*", "types/**/*.ts"],
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"strict": false,
"moduleResolution": "Bundler",
"types": ["node"]
"types": ["node"],
"baseUrl": "./",
"paths": {
"@/*": ["src/*"],
"#/*": ["types/*"]
}
}
}

11
types/axios.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { AxiosRequestConfig } from 'axios'
// import { Ref } from 'vue'
declare module 'axios' {
export interface AxiosRequestConfig {
toast?: boolean
third?: boolean
isLoading?: boolean
}
}

10
types/config.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
export type PrefixType = 'dcd'
export type EnvType = 'online' | 'test' | 'dev'
export type PlatType = 'h5' | 'app' | 'xcx'
export interface AppConfigType {
name: string
code?: string
prefix: PrefixType
}

100
types/global.d.ts vendored Normal file
View File

@ -0,0 +1,100 @@
import type { ComponentPublicInstance, FunctionalComponent } from 'vue'
import type { ContainsDash } from '@/utils/use-point/index'
declare global {
type Recordable<T = any> = Record<string, T>
type TOption = { label: string; value: string | number }
type HTMLElementEvent<T extends HTMLElement> = Event & {
target: T
currentTarget: T
}
type LoadingType = { show: boolean; type: 'enter' | 'submit' }
// vue
interface ImportMetaEnv extends ViteEnv {
__: unknown
}
interface Array {
post?: <T>(data?: Recordable<any>) => Promise<{
code: number
msg: string
data: T
}>
get?: <T>(data?: Recordable<any>) => Promise<{
code: number
msg: string
data: T
}>
}
interface Window {
isBgWhite: Ref<boolean>
useTipDialog: (
callback: (result: boolean) => void | Promise<any>,
config: {
title: string
html: string
btnName: string
showCloseIcon?: boolean
showCancelBtn?: boolean
cancelBtnName?: string
}
) => void
useLoading: (ob?: { type: 'enter' | 'submit'; show?: boolean }) => {
if: boolean | null
type: string
show: boolean
} & {
type: 'enter' | 'submit'
show: boolean
}
useSubmitBtn: (
callback: (check: Ref<boolean>) => Promise<boolean> | ToastWrapperInstance,
ob?: {
type?: 'normal' | 'fixed' | 'fixed-bg' | 'absolute'
text: string
protocols?: TProtocols
id?: string
countdownTime?: number
checkFn?: () => void
protocolFn?: () => void
check?: Ref<boolean>
}
) => {
check: Ref<boolean>
base: {
show: boolean
type: any
text: string
protocols?: TProtocols
checkFn?: () => void
protocolFn?: () => void
check?: Ref<boolean>
showForceRead: boolean
callback: (check: Ref<boolean>) => Promise<boolean> | ToastWrapperInstance
}
}
useProtocolDialog: (config?: {
title?: string
html?: string
protocolName: string
protocolFn?: () => void
protocolUrl: string
callback: (result: boolean) => void
}) => void
base64: object
[key: string]: any
point: <T extends string>(str: ContainsDash<T>, EXTEND_DATA?: any) => Promise<void>
}
interface ViteEnv {
VITE_APP_CODE: string
VITE_APP_HOST_SIGN: string
}
type Callback = () => any
}
declare module 'vue' {
export type JSXComponent<Props = any> =
| { new (): ComponentPublicInstance<Props> }
| FunctionalComponent<Props>
}

View File

@ -26,10 +26,20 @@ export default defineConfig({
server: {
host: '0.0.0.0',
port: 3000,
proxy: {
'/test/api-interface': {
target: 'https://api.1024api.com', // 后端接口地址
changeOrigin: true,
rewrite: (path: string) => {
return path.replace(/^\/test/, ``)
},
},
},
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'#': fileURLToPath(new URL('./types', import.meta.url)),
},
},
css: {

View File

@ -1552,6 +1552,11 @@ caniuse-lite@^1.0.30001735:
resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz#8292bb7591932ff09e9a765f12fdf5629a241ccc"
integrity sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==
capture-request-log@^0.0.4:
version "0.0.4"
resolved "https://registry.npmmirror.com/capture-request-log/-/capture-request-log-0.0.4.tgz#48525dd2772eb5aef1b5ce8efe0df9d346909e44"
integrity sha512-qAaJhbmpBe33qVWQuhCSv9PufuplekCHBCzK5hX07M9wOuAFzTTgOUBuwsC/S5tSZ4ZQCKClKXpuu2qW9UjUGQ==
chalk@^4.0.0:
version "4.1.2"
resolved "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
@ -2336,6 +2341,11 @@ jiti@^2.4.2, jiti@^2.5.1:
resolved "https://registry.npmmirror.com/jiti/-/jiti-2.5.1.tgz#bd099c1c2be1c59bbea4e5adcd127363446759d0"
integrity sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==
js-base64@^3.7.8:
version "3.7.8"
resolved "https://registry.npmmirror.com/js-base64/-/js-base64-3.7.8.tgz#af44496bc09fa178ed9c4adf67eb2b46f5c6d2a4"
integrity sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==
js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@ -2429,6 +2439,11 @@ lodash.merge@^4.6.2:
resolved "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lru-cache@^5.1.1:
version "5.1.1"
resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"