Building an Enterprise After‑Sales SaaS Portal with Vue 3, NutUI 3.0 and Taro
This article details the development of a JD enterprise after‑sales SaaS portal using Vue 3, NutUI 3.0, Vite, TypeScript and Taro, covering project background, cross‑domain login handling, customizable themes, component migrations, state management with Vuex 4, routing, and decorator‑based loading solutions.
NutUI is a lightweight JD‑style mobile Vue component library. After Vue 3.0 was released, NutUI launched NutUI 3.0 for Vue 3, and the team used it to build a new enterprise after‑sales portal.
The portal aims to reduce R&D cost by unifying after‑sales data and operations across enterprise procurement channels, providing features such as after‑sales application, policy display, and reporting.
Technical stack : Vue 3.0 + Vuex4 + Vue‑Router4.x + Vite2.2.x + NutUI 3.0 + TypeScript. Vite uses native ESM, giving a 30× speed boost over traditional bundlers, and Vue 3’s Composition API enables functional programming style.
Cross‑domain login handling is required because the H5/App and mini‑programs run under different domains (a.jd.com and b.jd.com). Cookies cannot be shared across domains unless the domain=.jd.com attribute is set.
Example of setting a cookie for cross‑domain sharing:
document.cookie = "testCookie=hello; domain=.jd.com; path=/";In H5, an <iframe src={webViewUrl}></iframe> can embed the third‑party page, while in mini‑programs a <WebView src={webViewUrl}></WebView> is used. Since mini‑programs do not automatically send cookies, the portal passes an encrypted pt_key via URL parameters and sets the cookie on the after‑sales side:
tmpUrl = this.afterSaleUrl + `&pt_key=${pt_key}`;
Utils.openWebView(tmpUrl); // In after‑sales page
const pt_key = this.queryString("pt_key") || "";
if (pt_key) {
document.cookie = `pt_key=${encodeURIComponent(pt_key)};domain=.jd.com;path=/`;
}Customizable header configuration allows hiding the after‑sales header, using the host’s header, or providing a custom title. Communication between the host and after‑sales pages is achieved via postMessage :
window.parent.postMessage({ title }, "*"); window.addEventListener('vsptitle', this._customEvent);
_customEvent = () => {};
customEvent(event) {
this.setState({ title: event.detail });
document.title = event.detail;
}Theme customization uses CSS variables and Vue 3’s v‑bind syntax. Example SCSS variables:
$primary-color: #f0250f;
$gradient-color: linear-gradient(135deg, rgba(240,22,14,1) 0%, rgba(240,37,14,1) 70%, rgba(240,78,14,1) 100%);
$bg-color-selected: #fef4f3;
$bg-color-disabled: #fcd4cf;
$bg-gradient-color: $gradient-color;
$bg-color-white: #fff;
$border-color: #ececec;Component migration highlights:
Popup mounting: NutUI 2.x used get-container prop; Vue 3 replaces this with Teleport ( <Teleport :to="'teleport'">... ).
Toast: Vue 2 used Vue.use(Toast) and this.$toast.text(...) ; Vue 3 registers via app.use(Toast) and accesses via proxy.$toast.text(...) or app.config.globalProperties.$toast .
Switch: Vue 3 supports async control with v-model or :model-value and emits update:value for external state sync.
State management adopts Vuex 4 with TypeScript support. Store definition includes an InjectionKey for typed useStore usage:
// store.ts
import { InjectionKey } from "vue";
import { createStore, Store } from "vuex";
export interface State { orderItem: any; customerExpectResultList: any; orderSkuApplyItem: any; }
export const key: InjectionKey
> = Symbol();
// main.ts
import { createApp } from "vue";
import { store, key } from "@/store";
const app = createApp(App);
app.use(store, key);
app.mount("#app");
// usage
import { useStore } from "vuex";
import { key } from "@/store";
const store = useStore(key);Routing is configured with Vue‑Router 4 using function‑based API and hash history:
export const routes: RouteRecordRaw[] = [
{ path: paths.saleindex, name: "saleindex", component: SaleIndex, meta: { title: "退换/售后", keepAlive: true, backurl: "", style: {} } },
{ path: "/:path(.*)+", redirect: () => paths.saleindex }
];
import { createRouter, createWebHashHistory } from "vue-router";
import { routes } from "./routes";
const router = createRouter({ history: createWebHashHistory(), routes });
export default router;Navigation helper demonstrates a typed push method:
export const useRouterHelper = () => {
const router = useRouter();
const push =
(path: T, params?: ParamsMap[K]) => {
router.push({ path, query: params });
};
return { push };
};Loading decorator example shows how to wrap API methods with a loading UI using a class decorator:
export const loading = () => {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
descriptor.value = async function (...args: any[]): Promise
{
_Toast.loading("加载中");
let res: any;
try { res = await method.apply(this, args); }
finally { _Toast.hide(); }
return res;
};
return descriptor;
};
};The article concludes that the after‑sales SaaS portal delivers a comprehensive front‑end system with unified data management, leveraging Vue 3, NutUI 3.0, and related modern web technologies, and that continued adoption of Vue 3 features will further enhance development efficiency.
JD Retail Technology
Official platform of JD Retail Technology, delivering insightful R&D news and a deep look into the lives and work of technologists.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.